c++的网络库有很多,作为一个合格的后端开发工程师,很多人或多或少都手撸过TCP通信模块,原因都是因为网上的轮子多是面向通用化的需求,在各自项目中还是需要对轮子进行一定的加工,比如著名的asio,它只实现了对网络字节流的高效收发方面的处理,剩下的事情需要自行完成,很难使用它快速开发网络服务。
一个简单易用的网络库,应该可以使用少量的代码实现一个网络服务,不需要过多关注网络通信层面的细节,只需要实现自己的业务逻辑。
一个高效的网络库,通常是多线程的,这样才能发挥多核服务器的性能,从而可以面对高并发、大吞吐量的网络通信请求,比如网络游戏的服务器,或者互联网后端的基础服务模块, 当然也有单线程的,网络业务之间没有相关性的情形,这种情况通常用端口复用(SO_REUSEPORT)的多进程模式解决。
不过多废话,直接上干货,见开源库:https://github.com/laiyongcong/cppfoundation
我们以实现一个简单的ping-pong测试为例,首先我们结合反射定义客户端和服务器的消息处理函数:
class ServerMsg {
public:
MSG_HANDLER_FUNC(Ping);
};
class ClientMsg {
public:
MSG_HANDLER_FUNC(Pong);
};
其中MSG_HANDLER_FUNC 宏的定义如下:
#define MSG_HANDLER_FUNC(FUNC) \
REF_EXPORT static int FUNC(cppfd::Connecter* pConn, const char* szBuff, uint32_t uBuffLen)
该宏定义了一个静态的函数,并进行反射导出(工具扫描后生成反射代码,见之前的文章:c++实现反射功能-CSDN博客)。接下来,我们在cpp文件中实现这两个函数:
int ServerMsg::Ping(Connecter* pConn, const char* szBuff, uint32_t uBuffLen) {
LOG_TRACE("Recv Ping from %s msg:%s", pConn->Info().c_str(), szBuff);
static String strMsg = "Hello Client!!!!!!!";
pConn->Send("Pong", strMsg.c_str(), (uint32_t)strMsg.size());
return 0;
}
std::atomic_int gCounter(0);
int ClientMsg::Pong(Connecter* pConn, const char* szBuff, uint32_t uBuffLen) {
LOG_TRACE("Recv Pong from %s msg:%s", pConn->Info().c_str(), szBuff);
static String strMsg = "Hello Server!!!!!!!";
pConn->Send("Ping", strMsg.c_str(), (uint32_t)strMsg.size());
gCounter++;
return 0;
}
在开始写最终实现之前,我们先看看网络引擎的一些基本特性。开源库中TcpEngine是一个TCP的网络引擎,客户端和服务器都可以使用,内部实现采用epoll(windows下采用select模拟epoll),其定义如下:
class TcpEngine : public NonCopyable {
friend class NetThread;
friend class ConnecterWorkerThread;
public:
TcpEngine(uint32_t uNetThreadNum, BaseNetDecoder* pDecoder, const std::type_info& tMsgClass, int nPort, const String& strHost = "0.0.0.0");
virtual ~TcpEngine();
void SetCrypto(NetCryptoFunc SendCryptoFunc, NetCryptoFunc RecvCryptoFunc, uint64_t uSendCrypto, uint64_t uRecvCrypto);//设置字节混淆方法,只能在start之前调用
bool Start();
bool Connect(const char* szHost, int nPort, int* pMicroTimeout, bool bLingerOn = true, uint32_t uLinger = 0, int nClientPort = 0);
public:
virtual void OnConnecterCreate(Connecter* pConn); // 被网络线程调用,
virtual void OnConnecterClose(std::shared_ptr<Connecter> pConn, const String& szErrMsg); // 被网络线程调用,
virtual int OnRecvMsg(Connecter* pConn, Pack* pPack); // 被网络线程调用
virtual Connecter* AllocateConnecter() { return new (std::nothrow) Connecter; } //若用户继承并扩展了connecter,需要override此函数
private:
NetThread* AllocateNetThread();
protected:
uint32_t mNetThreadNum;
String mHost;
int mPort;
NetThread* mNetThreads;
const Class* mMsgClass;
uint64_t mSendCryptoSeed;
uint64_t mRecvCryptoSeed;
BaseNetDecoder* mDecoder;
NetCryptoFunc mSendCryptoFunc;
NetCryptoFunc mRecvCryptoFunc;
};
这里tcp引擎主要有下面的特性:
- 支持多线程;
- 支持自定义包头;
- 支持自定义消息处理;
- 支持通信过程加密(字节混淆)
- 支持自行扩展Connecter实现
- 支持扩展链接创建、关闭和网络消息的处理
自定义消息处理采用反射实现,我们看看消息回调函数的实现:
int TcpEngine::OnRecvMsg(Connecter* pConn, Pack* pPack) {
const char* pBuff = pPack->GetBuff();
const String& strCmd = mDecoder->GetCmd(pBuff);
const StaticMethod* pMethod = mMsgClass->GetStaticMethod(strCmd.c_str());
if (pMethod == nullptr) {
LOG_FATAL("unknow msg:%s", strCmd.c_str());
return -1;
}
return pMethod->Invoke<int, Connecter*, const char*, uint32_t>(pConn, pBuff + mHeaderSize, pPack->GetDataLen() - mHeaderSize);
}
这里从包头获得了一个函数名,从反射的类中获得静态处理方法,然后把body的内容交给该静态方法进行处理。
在Ping-Pong测试中,我们由客户端发起Ping,因此需要在链接创建时发送Ping包,我们需要扩展客户端的实现(可以不做扩展,已支持Connect后获得connecter指针,直接Send):
class TestClient : public TcpEngine {
public:
TestClient(uint32_t uNetThreadNum, BaseNetDecoder* pDecoder, const std::type_info& tMsgClass)
: TcpEngine(uNetThreadNum, pDecoder, tMsgClass, -1){}
void OnConnecterCreate(Connecter* pConn) override {
TcpEngine::OnConnecterCreate(pConn);
String strMsg = "Hello Server!!!!!!!";
pConn->Send("Ping", strMsg.c_str(), (uint32_t)strMsg.size());
}
};
接下来我们只需要创建服务器和客户端, 创建必要的链接,ping-pong测试就完成了。
void NetTest() {
SockInitor initor;
LogConfig cfg;
cfg.ProcessName = "testLog";
cfg.LogLevel = ELogLevel_Warning;
Log::Init(cfg);
TcpEngine testServer(2, (BaseNetDecoder*)&g_NetHeaderDecoder, typeid(ServerMsg), 9100);
TestClient testClient(2, (BaseNetDecoder*)&g_NetHeaderDecoder, typeid(ClientMsg));
testServer.SetCrypto(DefaultNetCryptoFunc, DefaultNetCryptoFunc, 12345, 12345);
testClient.SetCrypto(DefaultNetCryptoFunc, DefaultNetCryptoFunc, 12345, 12345);
testServer.Start();
testClient.Start();
int nTimeout = 10000000;
testClient.Connect("127.0.0.1", 9100, &nTimeout);
testClient.Connect("127.0.0.1", 9100, &nTimeout);
testClient.Connect("127.0.0.1", 9100, &nTimeout);
testClient.Connect("127.0.0.1", 9100, &nTimeout);
testClient.Connect("127.0.0.1", 9100, &nTimeout);
testClient.Connect("127.0.0.1", 9100, &nTimeout);
testClient.Connect("127.0.0.1", 9100, &nTimeout);
testClient.Connect("127.0.0.1", 9100, &nTimeout);
testClient.Connect("127.0.0.1", 9100, &nTimeout);
testClient.Connect("127.0.0.1", 9100, &nTimeout);
int nCounter = 0;
while (nCounter < 100)
{
Thread::Milisleep(1000);
nCounter++;
}
nCounter = gCounter;
LOG_WARNING("total:%d", nCounter);
Log::Destroy();
}
这里创建了10个链接,服务器和客户端开启了两个线程,并且采用了默认的字节混淆加密进行ping-pong测试,具体的性能可以关注一下计数器的大小。
在很多对公网开放的服务中,除了防止抓包外,还需要防止重放,因此网络通信的秘钥应该是动态变化的,一般的解决方案可能是引入openssl(这种情况也不需要字节混淆了),如果不想使用openssl,服务器可以在链接创建时,把新的随机好的混淆key发给客户端,双方重设混淆key,然后再进行通信。
关于自定义包头,可以看代码库中当前包头的实现,如果希望自定义包头,需要自己再实现一个新的NetDecoder:
#pragma pack(push, 1)
struct NetHeader {
char mCmd[32]; // 函数
uint32_t mBodyLen; // 包体长度,不包括包头
};
#pragma pack(pop)
//保留用户自定义包头的能力
class BaseNetDecoder {
public:
virtual ~BaseNetDecoder() {}
virtual uint32_t GetBodyLen(const void* pHeader) = 0; // 获取头长度字段
virtual bool SetBodyLen(void* pHeader, unsigned int bdlen) = 0; // 设置body长度
virtual uint32_t GetHeaderSize() = 0; // 获取头长度
virtual const String GetCmd(const void* pHeader) = 0; //获得包头的命令
virtual bool SetCmd(void* pHeader, const String& strCmd) = 0; //设置包头的命令
};
#define MAX_PACK_LEN (64 * 1024 * 1024) // 64M
class NetDecoder : BaseNetDecoder {
public:
virtual uint32_t GetBodyLen(const void* pHeader) {
if (pHeader == nullptr) return 0;
return ntohl(((NetHeader*)pHeader)->mBodyLen);
}
virtual bool SetBodyLen(void* pHeader, uint32_t bdlen) {
if (pHeader == nullptr || bdlen > MAX_PACK_LEN - sizeof(NetHeader)) return false;
((NetHeader*)pHeader)->mBodyLen = htonl(bdlen);
return true;
}
virtual uint32_t GetHeaderSize() { return (uint32_t)sizeof(NetHeader); }
virtual const String GetCmd(const void* pHeader) {
if (pHeader == nullptr) return "";
NetHeader* pH = (NetHeader*)pHeader;
pH->mCmd[sizeof(pH->mCmd) - 1] = 0; //截断保护
return pH->mCmd;
}
virtual bool SetCmd(void* pHeader, const String& strCmd) {
if (pHeader == nullptr) return false;
NetHeader* pH = (NetHeader*)pHeader;
safe_printf(pH->mCmd, sizeof(pH->mCmd), "%s", strCmd.c_str());
return true;
}
};
extern NetDecoder g_NetHeaderDecoder;