一个简单易用的轻量化的c++网络库

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;

  • 16
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: 轻量化插件开发框架C,是一种基于C语言的插件开发框架,旨在提升插件开发的效率和可扩展性。该框架的核心思想是将插件拆成多个小模块,并通过一个轻量级的内核将它们集成在一起。 使用该框架进行插件开发,可以轻松地实现插件的加载和卸载,同时也能方便地管理插件之间的依赖关系。此外,该框架还支持插件之间的消息传递和事件通知,使得插件之间的协作更加简单和高效。 值得特别强调的是,该框架的轻量化设计非常符合现代软件开发的趋势,不仅具有出色的性能和稳定性,还具备很高的可移植性和可复用性。因此,无论是开发单机软件还是分布式系统,使用该框架都能够大大简化开发流程,提高开发效率。 总之,轻量化插件开发框架C是一项非常有价值的技术,为插件开发者提供了一个高效、稳定、可扩展的开发环境。随着越来越多的软件系统开始采用插件化架构,该框架在未来的发展中将扮演着越来越重要的角色。 ### 回答2: 轻量化插件开发框架是一种基于C语言开发工具,旨在提供一套简单易用开发平台,用于开发轻量级插件。它的主要特点是代码轻量级,模块化设计,易于扩展和定制化。 轻量化插件开发框架C是针对嵌入式系统等资源受限环境而设计的,具有小巧、高效、灵活等特点。它可以帮助开发者快速地创建功能齐全、可定制的插件。同时,它也提供了一些常用的插件服务,如配置管理、日志记录、网络通信等,开发者可以直接调用这些服务,从而减少开发的工作量。 使用轻量化插件开发框架C,开发者可以快速构建插件,并通过模块化的方式管理代码,便于维护和扩展。此外,由于轻量化插件开发框架C采用了C语言作为开发语言,它可以与其他C语言开发的项目无缝集成,方便开发者利用现有的代码资源,减少重复开发的工作。 总而言之,轻量化插件开发框架C是一种高效、灵活、易于扩展的开发工具,具有小巧、高效、模块化设计等特点,能够帮助开发者快速构建插件,并使其更易于维护和扩展。 ### 回答3: 轻量化插件开发框架C是一种基于C语言的轻量级软件开发框架,旨在优化软件开发的效率并减少开发成本。该框架主要用于为主流操作系统和软件平台开发各种类型的插件和模块,包括嵌入式应用程序、浏览器插件、GUI插件、游戏插件、多媒体插件、系统工具和驱动程序等。其本质是一个用于插件开发的API集合,它提供了一种标准化的方式,使应用程序能够通过简单的接口调用来访问各种插件。 轻量化插件开发框架C的主要优点是灵活性和可扩展性。它可以被轻松地集成到任何C/C++软件项目中,并且通过支持各种自定义插件类型和数据格式,可以满足几乎所有开发需求。此外,该框架还具有开发周期短、代码量少、运行速度快等特点,这使得它成为开发人员首选的插件开发框架之一。 轻量化插件开发框架C的实现需要遵循一些核心原则。首先,插件必须尽可能简单,接口必须明确,以确保插件可以灵活地进行配置和管理。其次,框架必须提供一些必要的运行时功能,如内存管理、日志分发、线程池等,以保证插件的运行稳定性和可靠性。最后,插件必须高度可扩展,以支持各种新的插件类型、数据格式和操作系统平台。 总之,轻量化插件开发框架C是一种高效、灵活和易于使用的插件开发框架,它可以大大提高软件开发的效率和质量,适用于各种大小规模的软件项目。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值