之所以想写这个库,源于2015年,当时我在长沙一家手游戏公司做后端服务。公司原有的服务器代码,虽然基于知名的开源事件库libev,但是所有的底层函数和业务逻辑全部耦合在一个大文件里,动辄几千上万行的代码,基本上没有明显的层次关系,开发质量经常得不到保障。鉴于此,我仍然基于libev,并结合以前我对网络引擎的理解,编写了这个库,应用于团队后续的项目开发中,目前已有3年多时间。
libevwork的名称也有此而来,基于libev底层事件库,实现更高一层的连接管理和收发缓冲管理,并且提供多种应用层协议支持。它整体的工作流程是这样的:
libev事件 -> 连接管理 -> 收发包缓冲 -> 应用层协议解包 -> 消息队列 -> 消息分派 -> 消息处理。
用好libevwork,使用者不需要过多的关注底层(libev及至以上各流程)的实现细节,只需要简单定义好消息结构,并注册好消息回调函数既可,并且提供友好的MFC编程风格。
编译libevwork前,需要安装以下依赖库:
- libev
- libboost-dev
- protobuf
- jsoncpp
其中,protobuf和jsoncpp是可选的,如果在你的应用程序中用不着protobuf和json协议,可以暂时跳过下面protobuf和jsoncpp库的安装。不过,在编译libevwork的时候,需要修改下Makefile文件,这个后面会讲。
源码安装:
wget http://dist.schmorp.de/libev/libev-4.15.tar.gz
tar -zxvf libev-4.15.tar.gz
cd libev-4.15
./configure
make
sudo make install
包安装:
centos下也可以直接yum安装:
sudo yum install libev-devel
包安装:
ubuntu -> sudo apt-get install libboost-dev
centos -> sudo yum install boost-devel
有可能默认源的版本与需要的不符,这时请登录官网下载需要的版本,网址:
http://sourceforge.net/projects/boost/files/
编译安装:
首先下载源码,解压,进行源码根目录,然后执行:
-
创建boost自己的编译工具bjam,执行:
./bootstrap.sh
注:也可以使用前缀选项指定安装目录,如
./bootstrap.sh --prefix=/home/kdjie/boost_1_43_0/boost_install,不指定prefix默认安装到/usr/local/include和/usr/local/lib。 -
编译boost,执行:
./bjam
或
./bjam -a 重新编译
默认生成的中间文件在./bin.v2目录,所有的库会被收集在stage/lib目录下。
如果编译过程中出现找不到头文件bzlib.h,需要先安装:
sudo apt-get install libbz2-dev
下载: https://github.com/google/protobuf/releases
编译:
./configuare
make
sudo make install
安装后头文件位置: /usr/local/include/google/protobuf/
库文件位置: /usr/local/lib/
编写协议文件:
vim MHello.proto,内容如下:
message MHello
{
required string str = 1;
required int32 number = 2;
}
编译协议文件:
protoc -I=. --cpp_out=. MHello.proto
使用:
#include "MHello.pb.h"
序列化:
MHello he; <br>
he.set_str("hello"); <br>
he.set_number(1); <br>
std::string str; <br>
he.SerializeToString(&str); <br>
反序列化:
MHello he2; <br>
he2.ParseFromString(str); <br>
大概性能:
protobuf 200W次序列及反序列/S
msgpack 50W次序列及反序列/S
jsoncpp 5W次序列及反序列/S
下载jsoncpp:http://sourceforge.net/projects/jsoncpp
文档:http://json.org/json-zh.html
由于jsoncpp需要使用scons编译,需要下载scons(scons采用python编写)
http://www.scons.org
编译:
先解压jsoncpp和scon压缩包,解后进入jsoncpp,如:
cd /home/kdjie/jsoncpp-src-0.6.0-rc2
python /home/kdjie/scons-2.3.1/script/scons platform=linux-gcc
在jsoncpp-src-0.6.0/libs/linux-gcc-4.8.8(随gcc版本不同)目录下生成
libjson_linux-gcc-4.8.5_libmt.a
libjson_linux-gcc-4.8.5_libmt.so
一些旧版本的编译方法:
export MYSCONS=/home/kdjie/scons-2.1.0
export SCONS_LIB_DIR=$MYSCONS/engine
python $MYSCONS/script/scons platform=linux-gcc
安装:
sudo cp include/json /usr/local/include/ -r
sudo cp libs/linux-gcc-4.8.5/* /usr/local/lib/
创建符号链接:
cd /usr/local/lib
sudo ln -s libjson_linux-gcc-4.8.5_libmt.so libjsoncpp.so
sudo ln -s libjson_linux-gcc-4.8.5_libmt.a libjsoncpp.a
sudo ldconfig
在以上库都安装好之后,进入到libevwork目录下,执行如下命令:
make 或 make all
即可生成libevwork.a。
如果前面你跳过了jsoncpp和protobuf的安装,那么你需要修改Makefile,找到这一行:
SRC_FILES = $(wildcard .cpp jsmfc/.cpp pbmfc/.cpp dsmfc/.cpp)
修改为:
SRC_FILES = $(wildcard .cpp dsmfc/.cpp)
然后重新make即可。
- 编写服务器代码,如SimpleEcho.cpp,输入以下内容:
#include "libevwork/EVWork.h"
#include "libevwork/ListenConn.h"
using namespace evwork;
class CDataEvent
: public IDataEvent
{
public:
virtual int onData(IConn* pConn, const char* pData, size_t uSize)
{
pConn->sendBin(pData, uSize);
return uSize;
}
};
int main(int argc, char* argv[])
{
//-------------------------------------------------------------------------
// libevwork初使化
signal(SIGPIPE, SIG_IGN);
CSyslogReport LG;
CEVLoop LP;
CConnManager CM;
CWriter WR;
CEnv::getThreadEnv()->setLogger(&LG);
CEnv::getThreadEnv()->setEVLoop(&LP);
CEnv::getThreadEnv()->setLinkEvent(&CM);
CEnv::getThreadEnv()->setConnManager(&CM);
CEnv::getThreadEnv()->setWriter(&WR);
CEnv::getThreadEnv()->getEVParam().uConnTimeout = 300;
LP.init();
//-------------------------------------------------------------------------
// 应用程序初使化
CDataEvent DE;
CEnv::getThreadEnv()->setDataEvent(&DE);
CListenConn listenConn(1982);
//-------------------------------------------------------------------------
// 启动事件循环
LP.runLoop();
return 0;
}
编译,执行命令:
g++ -o SimpleEcho SimpleEcho.cpp -I../../.. -L../../../libevwork -levwork -lev -lboost_thread
这里 -I../../.. -L../../../libevwork 表示设置头文件和库文件的搜索路径。
测试,启动服务:
[kdjie@localhost test.d]$ ./SimpleEcho
启动客户端:
[kdjie@localhost ~]$ telnet 0 1982
Trying 0.0.0.0...
Connected to 0.
Escape character is '^]'.
hello,world
hello,world
- 利用Json协议,重新实现简单服务回显,如SimpleJsonEcho.cpp,输入以下内容:
#include "libevwork/EVWork.h"
#include "libevwork/ListenConn.h"
#include "libevwork/jsmfc/FormDef.h"
#include "libevwork/jsmfc/Sender.h"
#include "libevwork/jsmfc/DataHandler.h"
#include "libevwork/jsmfc/MfcAppContext.h"
using namespace evwork;
using namespace js;
// 消息命令字定义
#define MESSAGE_ID_1 1
#define MESSAGE_ID_2 2
class CMessageLogic
: public js::PHClass
{
public:
DECLARE_JS_FORM_MAP;
// 协议处理
void onMessage1(Json::Value* pJson, evwork::IConn* pConn);
};
BEGIN_JS_FORM_MAP(CMessageLogic)
ON_JS_REQUEST_CONN(MESSAGE_ID_1, &CMessageLogic::onMessage1)
END_JS_FORM_MAP()
// 协议处理
void CMessageLogic::onMessage1(Json::Value* pJson, evwork::IConn* pConn)
{
//LOG(Info, "[CMessageLogic::%s] conn:[%d] call...", __FUNCTION__, pConn->getcid());
printf("[CMessageLogic::%s] conn:[%d] call...", __FUNCTION__, pConn->getcid());
// 这里可以分析Json格式,取出数据做相应处理
// 这里简单打印Json内容
std::string strJsonText;
{
Json::FastWriter writer;
strJsonText = writer.write(*pJson);
//LOG(Debug, "[CMessageLogic::%s] message1:[%s]", __FUNCTION__, strJsonText.c_str());
printf("[CMessageLogic::%s] message1:[%s]", __FUNCTION__, strJsonText.c_str());
}
// 发回响应,这里简单将原包发回
{
js::Sender sdr(MESSAGE_ID_2, strJsonText);
pConn->sendBin(sdr.Data(), sdr.Size());
}
}
int main(int argc, char* argv[])
{
//-------------------------------------------------------------------------
// libevwork初使化
signal(SIGPIPE, SIG_IGN);
CSyslogReport LG;
CEVLoop LP;
CConnManager CM;
CWriter WR;
CEnv::getThreadEnv()->setLogger(&LG);
CEnv::getThreadEnv()->setEVLoop(&LP);
CEnv::getThreadEnv()->setLinkEvent(&CM);
CEnv::getThreadEnv()->setConnManager(&CM);
CEnv::getThreadEnv()->setWriter(&WR);
LP.init();
//-------------------------------------------------------------------------
// 应用程序初使化
// 设置连接超时,单位S
CEnv::getThreadEnv()->getEVParam().uConnTimeout = 300;
// 定义数据包处理器
js::CDataHandler __DE;
// 数据包处理器限制请求包的最大长度,单位字节
__DE.setPacketLimit(16*1024);
// 关联数据包处理器
CEnv::getThreadEnv()->setDataEvent(&__DE);
// 设置MFC对象
js::CMfcAppContext __MFC;
// 将MFC对象关联到数据包处理器
__DE.setAppContext(&__MFC);
// 定义逻辑主对象
CMessageLogic __logic;
// 将主对象的消息映射表装载到MFC对象
// 同样的,可以定义别的逻辑对象,也装载到MFC对象中
__MFC.addEntry(CMessageLogic::getFormEntries(), &__logic);
// 创建服务套接口对象
CListenConn __listenConn(1982);
//-------------------------------------------------------------------------
// 启动事件循环
LP.runLoop();
return 0;
}
编译,执行命令:
g++ -o SimpleJsonEcho SimpleJsonEcho.cpp -I../../.. -L../../../libevwork -levwork -lev -lboost_thread -ljsoncpp
测试,启动服务:
[kdjie@localhost test.d]$ ./SimpleJsonEcho
客户端见samples/SimpleJsonEcho下的源码文件JsonClient.cpp,编译:
g++ -o JsonClient JsonClient.cpp -I../../.. -L../../../libevwork -levwork -lev -lboost_thread -ljsoncpp
启动:
[kdjie@localhost test.d]$ ./JsonClient
[CEchoSender::__SenderFunc] send -> {"hello":",world!"}
[CEchoSender::onMessage2] conn:[1] call...
[CEchoSender::onMessage2] echo message2:[{"hello":",world!"}
][CEchoSender::__SenderFunc] send -> {"hello":",world!"}
[CEchoSender::onMessage2] conn:[1] call...
[CEchoSender::onMessage2] echo message2:[{"hello":",world!"}
在前面的例子中,我们用到了以下类,列举如下:
CSyslogReport -- Syslog日志输出类,通常在进程启动时,首先初使化这个对象。
CEVLoop -- 事件循环类,也是对libev的封装,在这里添加/删除事件对象,并且管理事件循环,相当于整个框架的发动机。
CConnManager -- 连接管理器,负责管理接入和连出的TCP连接。
CWriter -- 发包器,使用它可以向指定的IP:Port建立连接,并发送数据。
CDataEvent -- 数据事件接收器,TCP连接收到传输流后,交给它对数据流按具体的传输协议进行消息拆分。它是IDataEvent接口的实现。
CListenConn -- 服务端监听端口工作类,通过它创建服务器端口。
js::CDataHandler -- 也是IDataEvent接口的实现,负责对Json数据流进行消息拆分。
js::CMfcAppContext -- Vistual MFC风格的消息分派器,负责接受具体的应用消息类型回调注册,并进行消息分派。
IConn* -- 所有的TCP连接基类,通过它可以直接向网络发包。它的客户端实现是CClientConn。
它们的组织关系是这样的:
——————应用层回调
| ^
| |
| js::CMfcAppContext
| ^
V |
CWriter js::CDataHandler
| ^
V |
CConnManager |
^ ^ |
/ \ |
CListenConn CClientConn
^ ^
\ /
CEVLoop
^
|
libev
libevwork的设计思想,就是尽可能的减小代码,还能适配大部分TCP服务器开发的场景。在上面这个类组织关系图中,实际上它们之间是以接口方式互相组织的,接口的对应关系如下:
IConn -> CClientConn
IConnManager -> CConnManager
ILinkEvent -> CConnManager
IDataEvent -> CDataEvent、js::CDataHandler
IWriter -> CWriter
IAppContext -> js::CMfcAppContext
所以,根据业务的需要,只要继承这些接口,并重载实现来满足业务多样化的需求。例如,为了支持多线程,本类库实现了CAsyncWriter和CAsyncDataHandler,它们分别继承自IWriter、IDataEvent。
首先,让我们来看一下libevwork的文件树结构:
libevwork
├── EVComm.h
├── EVLoop.cpp
├── EVWork.cpp
├── EVWork.h
├── ExceptionErrno.h
├── FuncHelper.h
├── AsyncWriter.cpp
├── AsyncWriter.h
├── Buffer.h
├── ClientConn.cpp
├── ClientConn.h
├── ConnManager.cpp
├── ConnManager.h
├── ListenConn.cpp
├── ListenConn.h
├── Logger.h
├── TimerHandler.h
├── Writer.cpp
├── Writer.h
├── Makefile
├── README.md
├── dsmfc
│?? ├── AsyncDataHandler.cpp
│?? ├── AsyncDataHandler.h
│?? ├── DataHandler.cpp
│?? ├── DataHandler.h
│?? ├── ds
│?? │?? ├── dsbuffer.h
│?? │?? ├── dspacket.h
│?? │?? └── dstypes.h
│?? ├── FormDef.h
│?? ├── MfcAppContext.cpp
│?? ├── MfcAppContext.h
│?? ├── Request.h
│?? └── Sender.h
├── jsmfc
│?? ├── AsyncDataHandler.cpp
│?? ├── AsyncDataHandler.h
│?? ├── DataHandler.cpp
│?? ├── DataHandler.h
│?? ├── FormDef.h
│?? ├── MfcAppContext.cpp
│?? ├── MfcAppContext.h
│?? ├── Request.h
│?? └── Sender.h
├── pbmfc
│?? ├── AsyncDataHandler.cpp
│?? ├── AsyncDataHandler.h
│?? ├── DataHandler.cpp
│?? ├── DataHandler.h
│?? ├── FormDef.h
│?? ├── MfcAppContext.cpp
│?? ├── MfcAppContext.h
│?? ├── Request.h
│?? └── Sender.h
可以看到整体是一个以两层目录结构的树,其中许多文件都可以顾名思义。第一层为通用实现,提供基础连接管理和收发包的支持。第二层包启dsmfc、jsmfc、pbmfc三个目录,分别对三种流传输协议提供压解包和消息分派支持。
在介绍使用方法之前,先看几个文件:
EVComm.h >>>>>>>>>>>
namespace evwork
{
// 连接对象
struct IConn
{
public:
IConn() : m_fd(-1), m_cid(0) {}
virtual ~IConn() {}
void setcid(uint32_t cid) { m_cid = cid; }
uint32_t getcid() { return m_cid; }
virtual void getPeerInfo(std::string& strPeerIp, uint16_t& uPeerPort16) = 0;
virtual bool sendBin(const char* pData, size_t uSize) = 0;
protected:
int m_fd;
uint32_t m_cid;
};
// 连接事件
struct ILinkEvent
{
public:
virtual ~ILinkEvent() {}
virtual void onConnected(IConn* pConn) = 0;
virtual void onClose(IConn* pConn) = 0;
};
// 数据事件
struct IDataEvent
{
public:
virtual ~IDataEvent() {}
virtual int onData(IConn* pConn, const char* pData, size_t uSize) = 0;
};
// 事件句柄
struct IHandle
{
public:
IHandle() : m_fd(-1), m_ev(0) { m_evio.data = this; }
virtual ~IHandle() {}
void setFd(int fd) { m_fd = fd; }
int getFd() { return m_fd; }
void setEv(int ev) { m_ev = ev; }
int getEv() { return m_ev; }
ev_io& getEvIo() { return m_evio; }
virtual void cbEvent(int revents) = 0;
static void evCallBack(struct ev_loop *loop, struct ev_io *w, int revents)
{
IHandle* pThis = (IHandle*)w->data;
pThis->cbEvent(revents);
}
protected:
int m_fd;
int m_ev;
ev_io m_evio;
};
// 模板事件句柄
template <typename T, void (T::*fn)(int revents)>
class THandle
: public IHandle
{
public:
THandle(T* p) : m_pObj(p) {}
virtual ~THandle() {}
void cbEvent(int revents)
{
(m_pObj->*fn)(revents);
}
protected:
T* m_pObj;
};
// EVLook事件循环对象
class CEVLoop
{
public:
CEVLoop();
virtual ~CEVLoop();
bool init();
void destroy();
void runLoop();
void breakLoop();
void setHandle(IHandle* p);
void delHandle(IHandle* p);
struct ev_loop* getLoop();
private:
struct ev_loop* m_pEVLoop;
std::set<IHandle*> m_setHandle;
};
// TCP发包接口
struct IWriter
{
public:
virtual ~IWriter() {}
virtual void send(const std::string& strIp, uint16_t uPort, const char* pData, size_t uSize) = 0;
virtual void flush() = 0;
};
// TCP连接管理
struct IConnManager
{
public:
virtual ~IConnManager() {}
virtual IConn* getConnById(uint32_t uConnId) = 0;
virtual IConn* getConnByIpPort(const std::string& strIp, uint16_t uPort) = 0;
};
}
这个文件定义了IConn、ILinkEvent、IDataEvent、IHandle、CEVLoop、IWriter、IConnManager等基础的接口及相应的方法。
EVWork.h >>>>>>>>>>>
namespace evwork
{
struct SEVParam
{
uint32_t uConnTimeout;
SEVParam()
{
uConnTimeout = (uint32_t)-1;
}
};
class CThreadEnv
{
public:
CThreadEnv();
virtual ~CThreadEnv();
void setEVLoop(CEVLoop* p);
CEVLoop* getEVLoop();
void setLogger(ILogReport* p);
ILogReport* getLogger();
void setLinkEvent(ILinkEvent* p);
ILinkEvent* getLinkEvent();
void setDataEvent(IDataEvent* p);
IDataEvent* getDataEvent();
void setWriter(IWriter* p);
IWriter* getWriter();
void setConnManager(IConnManager* p);
IConnManager* getConnManager();
SEVParam& getEVParam();
private:
CEVLoop* m_pEVLoop;
ILogReport* m_pLogger;
ILinkEvent* m_pLinkEvent;
IDataEvent* m_pDataEvent;
IWriter* m_pWriter;
IConnManager* m_pConnManager;
SEVParam m_evParam;
};
class CEnv
{
public:
static CThreadEnv* getThreadEnv();
private:
static boost::thread_specific_ptr<CThreadEnv> m_tssEnv;
};
#define LOG(l,f,...) CEnv::getThreadEnv()->getLogger()->log(l, f, ##__VA_ARGS__)
}
这个文件定义了CThreadEnv结构,它包含了全部基础对象的指针,我们可以把它看成一个线程内部的黑盒子,通过初使化时为黑盒子设置基础对象的指针,它们可以有效地组织成一个整体。文件同时还定义了CEnv,通过调用它的getThreadEnv()方法,可以返回调用线程的CThreadEnv结构,对于单进程(单反应器)结构,我们通常只在主线程调用CEnv::getThreadEnv()方法,如果需要创建多反应器模型,我们可以在每个线程内调用CEnv::getThreadEnv()创建多个CThreadEnv结构,并且为它们分别设置相应的基础对象。
现在可以明白这段代码的含义了吗?
//-------------------------------------------------------------------------
// libevwork初使化
signal(SIGPIPE, SIG_IGN);
CSyslogReport LG;
CEVLoop LP;
CConnManager CM;
CWriter WR;
CEnv::getThreadEnv()->setLogger(&LG);
CEnv::getThreadEnv()->setEVLoop(&LP);
CEnv::getThreadEnv()->setLinkEvent(&CM);
CEnv::getThreadEnv()->setConnManager(&CM);
CEnv::getThreadEnv()->setWriter(&WR);
CEnv::getThreadEnv()->getEVParam().uConnTimeout = 300;
LP.init();
//-------------------------------------------------------------------------
// 应用程序初使化
CDataEvent DE;
CEnv::getThreadEnv()->setDataEvent(&DE);
CListenConn listenConn(1982);
//-------------------------------------------------------------------------
// 启动事件循环
LP.runLoop();
请参考另一款开源工具:极简轻量级可防CC反向代理
地址:https://github.com/kdjie/portmap