现在的位置: 首页 > 综合 > 正文

详谈高性能TCP服务器开发

2013年10月24日 ⁄ 综合 ⁄ 共 10379字 ⁄ 字号 评论关闭

 作者:司强
发布者:司强
发布日期:2008/07/22 

  对于开发一款高性能服务器程序,广大服务器开发人员在
一直为之奋斗和努力.其中一个影响服务器的重要瓶颈就是服务器的网络处理模块.如果一款服务器程序不能及时的处理用户的数据.则服务器的上层业务逻辑再高
效也是徒劳.所以一个服务器程序的网络处理能力直接影响到整个服务器的性能, 本文主要介绍在windows平台下开发高性能的网络处理模块以及自己在设
计开发服务器网络模块遇到的一些问题和开发心得.本篇主要介绍TCP服务器的设计, 下一篇将主要介绍UDP服务器的设计。

  众所周
知, 对于服务器来说windows下网络I/O处理的最佳方式就是完成端口, 因此本服务器的开发主要基于完成端口的模式.完成端口
(completion port)是应用程序使用线程池处理异步I/O请求的一种机制.将创建好的socket和完成端口绑定后就可以向该socket
上投递相应的I/O操作, 当操作完成后I/O系统会向完成端口发送一个通知包;应用程序通过GetQueuedCompletionStatus()函
数获取这些通知包并进行相应的处理.下面切入正题谈谈TCP服务器的开发。

  本人在开发TCP服务器的经过了两个阶段, 第一次设计出来
的TCP服务器网络层只能支持5000 – 8000个在线用户同时和服务器交互, 但是会出现一些莫名其妙的系统异常.所以网络层不是很稳定.这次开发
主要用到一个系统的I/O线程池函数BindIoCompletionCallback() 该函数在win2000以后都支
持, BindIoCompletion-
Callback()是一个I/O线程池函数,其主要功能是采用系统线程池进行I/O处理,优点是用户
无需自己创建完成端口和线程池,完成端口和工作者线程池的创建和管理都由系统维护.给用户带了很大的方便.用户只需要将自己创建的socket和I/O回
调函数传递给BindIoCompletionCallback()函数即可, 然后就可以在该socket上投递相应的操作.当操作完成后系统会调用用
户的回调函数通知用户.这种方式给开发者带来了很大的方便, 开发者甚至不需要去了解完成端口的工作机制就可以开发出一个较高性能的网络程序.但同时也带
来了很多麻烦,用户无法知道在完成端口上到底有多少个工作者线程, 而且当连接到服务器上的用户量过大时会出现线程堆栈错误等异常,同时有
1000-2000个用户断开连接后, 服务器就无法让后续用户连接到服务器. 在这种方式下的服务器网络层最多只支持4000 – 5000用户同时连
接到服务器.用户量再大时就会出现一些系统异常而且无法解决。

  借鉴于第一次开发的经验和教训, 在第二次开发服务器TCP层时决定自己创建完成端口和工作者线程池, 并对其进行维护和管理.这样做的好处是出了问题好定位和处理.下面将我开发的代码拿出来和大家切磋切磋, 如果什么地方写得问题还希望能够指正出来, 

1.首先介绍网络上下文(NET_CONTEXT)的定义:
 class NET_CONTEXT 
 {
 public:
  WSAOVERLAPPED m_ol;
  SOCKET m_hSock;  
  CHAR* m_pBuf;  //接收或发送数据的缓冲区
  INT m_nOperation; //在该网络上下文上进行的操作 OP_ACCEPT…
  static DWORD S_PAGE_SIZE;  //缓冲区的最大容量  
  NET_CONTEXT();
  virtual ~NET_CONTEXT();

  static void InitReource();
  static void ReleaseReource();

 private:
  void* operator new (size_t nSize);
  void operator delete(void* p);
  static HANDLE s_hDataHeap; 
  static vector<char * > s_IDLQue;  //无效数据缓冲区的队列
  static CRITICAL_SECTION s_IDLQueLock;  //访问s_IDLQue的互斥锁
 };

 
 NET_CONTEXT 是所有网络上下文的基类, 对于TCP的recv, send, accep, connect的上下文都继承自该类.UDP
的send和recv的网络上下文也继承自该类. m_ol 必须放置在第一个位置否则当从完成封包取net_context不能得到正确的结
果. S_PAGE_SIZE 为数据缓冲区m_pBuf的大小,其大小和相应的操作系统平台有关, win32下其值为4096, win64下其值为
8192, 即为操作系统的一个内存页的大小.设置为一个内存页的原因是因为在投递重叠操作时系统会锁定投递的缓冲区, 在锁定时是按照内存页的边界来锁
定的.因此即使你只发送一个1K字节数据系统也会锁定整个内存页(4096/8192). s_hDataHeap 为自定义的BUF申请的堆.其优点是
用户可以自己对堆进行管理和操作. s_IDLQue 为用过的BUF队列, 当用户用完相应的NET_CONTEXT后在执行析构操作时并不会真正把
m_pBuf所占的内存释放掉而是将其放入到s_IDLQue队列中, 当下次申请新的NET_CONTEXT时只需从s_IDLQue中取出就可以使
用, 避免频繁的new和delete操作。

2.数据包头的定义:
 struct PACKET_HEAD
 {
  LONG nTotalLen;   //数据包的总长度
  ULONG nSerialNum;  //数据包的序列号
  WORD nCurrentLen;  //当前数据包的长度
  WORD nType;    //数据包的类型
 };
数据包头位于每一个接收到的或待发送的数据包的首部,用于确定接收到的数据包是否合法以及该数据包是做什么用的.用户可以定义自己包头。

3.TCP_CONTEXT主要用于定义接收和发送数据的缓冲区, 继承自NET_CONTEXT
 class TCP_CONTEXT : public NET_CONTEXT
 {
  friend class TcpServer;
 protected:
  DWORD m_nDataLen;  //TCP模式下累计发送和接收数据的长度

  TCP_CONTEXT()
   : m_nDataLen(0)
  {

  }
  virtual ~TCP_CONTEXT() {}

  void* operator new(size_t nSize);
  void operator delete(void* p);

  enum
  {
   E_TCP_HEAP_SIZE = 1024 * 1024* 10,
   MAX_IDL_DATA = 20000,
  };
 private:
  static vector<TCP_CONTEXT* > s_IDLQue;  //无效的数据队列
  static CRITICAL_SECTION s_IDLQueLock;  //访问s_IDLQue的互斥锁
  static HANDLE s_hHeap; //TCP_CONTEXT的数据申请堆
 };

TCP_CONTEXT
类主要用在网络上发送和接收数据的上下文.每个连接到服务器的SOCKET都会有一个发送和接收数据的TCP_CONTEXT.这里重载了new和
delete函数.这样做的优点在于当申请一个新的TCP_CONTEXT对象时会先判断无效的数据队列中是否有未使用的TCP_CONTEXT,若有则
直接取出来使用否则从s_hHeap堆上新申请一个.new 函数的定义如下

 void* TCP_CONTEXT::operator new(size_t nSize)
 {
  void* pContext = NULL;

  try
  {
   if (NULL == s_hHeap)
   {
    throw ((long)(__LINE__));
   }

   //为新的TCP_CONTEXT申请内存, 先从无效队列中找, 如无效队列为空则从堆上申请     

   EnterCriticalSection(&s_IDLQueLock);
   vector<TCP_CONTEXT* >::iterator iter = s_IDLQue.begin();

   if (iter != s_IDLQue.end())
   {
    pContext = *iter;
    s_IDLQue.erase(iter);    
   }
   else
   {
    pContext = HeapAlloc(s_hHeap, HEAP_ZERO_MEMORY | HEAP_NO_SERIALIZE, nSize);
   }
   LeaveCriticalSection(&s_IDLQueLock);

   if (NULL == pContext)
   {
    throw ((long)(__LINE__));
   }
  }
  catch (const long& iErrCode)
  {
   pContext = NULL;
   _TRACE("/r/nExcept : %s--%ld", __FILE__, iErrCode);
  }

  return pContext;
 }

当使用完TCP_CONTEXT时调用delete函数进行对内存回收, 在进行内存回收时先查看无效队列已存放的数据是否达到MAX_IDL_DATA, 若没有超过MAX_IDL_DATA则将其放入到s_IDLQue中否则将其释放掉.delete函数的实现如下:

 void TCP_CONTEXT::operator delete(void* p)
 {
  if (p)  
  {
   //若空闲队列的长度小于MAX_IDL_DATA, 则将其放入无效队列中否则释
//放之

   EnterCriticalSection(&s_IDLQueLock);
   const DWORD QUE_SIZE = (DWORD)(s_IDLQue.size());
   TCP_CONTEXT* const pContext = (TCP_CONTEXT*)p;

   if (QUE_SIZE <= MAX_IDL_DATA)
   {
    s_IDLQue.push_back(pContext);
   }
   else
   {
    HeapFree(s_hHeap, HEAP_NO_SERIALIZE, p);
   }
   LeaveCriticalSection(&s_IDLQueLock);  
  }

  return;
 }

4.ACCEPT_CONTEXT 主要用于投递AcceptEx操作, 继承自NET_CONTEXT类
 class ACCEPT_CONTEXT : public NET_CONTEXT
 {
  friend class TcpServer;
 protected:
  SOCKET m_hRemoteSock;   //连接本服务器的客户端SOCKET

  ACCEPT_CONTEXT()
   : m_hRemoteSock(INVALID_SOCKET)
  {

  }

  virtual ~ACCEPT_CONTEXT() {}

  void* operator new(size_t nSize);
  void operator delete(void* p);
 private:
  static vector<ACCEPT_CONTEXT* > s_IDLQue;  //无效的数据队列
  static CRITICAL_SECTION s_IDLQueLock;   //访问s_IDLQue&micro;互斥锁
  static HANDLE s_hHeap; //ACCEPT_CONTEXT的自定义堆
 };

5.TCP_RCV_DATA, 当服务器的某个socket从网络上收到数据后并且数据合法便为收到的数据申请一个新的TCP_RCV_DATA实例存储收到的数据.其定义如下:
 class DLLENTRY TCP_RCV_DATA
 {
  friend class TcpServer;
 public:
  SOCKET m_hSocket;  //与该数据相关的socket
  CHAR* m_pData;   //数据缓冲区地址
  INT m_nLen;    //收到的数据的长度

  TCP_RCV_DATA(SOCKET hSock, const CHAR* pBuf, INT nLen);
  ~TCP_RCV_DATA();

  void* operator new(size_t nSize);
  void operator delete(void* p);

  enum
  {
   HEAP_SIZE = 1024 *1024* 50,  
   DATA_HEAP_SIZE = 1024 *1024 * 100,
   MAX_IDL_DATA = 100000,
  };

 private:
  static vector<TCP_RCV_DATA* > s_IDLQue;  //无效数据队列
  static CRITICAL_SECTION s_IDLQueLock;  //访问s_IDLQue的互斥锁
  static HANDLE s_hHeap;  
  static HANDLE s_DataHeap; 
 };

6.前面讲的相关的数据结构都是为下面要探讨的TcpServer类服务的. TcpServer类是本文要探讨的核心数据结构;主要用于启动服务, 管理连接等操作.
class DLLENTRY TcpServer
 {
 public:
  TcpServer();
  ~TcpServer();

  /************************************************************************
  * Desc : 初始化相关静态资源,在申请TCP实例之前必须先调用该方法对相关资
  * 源进行初始化
  ************************************************************************/
  static void InitReource();

  /************************************************************************
  * Desc : 释放相应的静态资源
  ************************************************************************/
  static void ReleaseReource();

  /****************************************************
  * Name : StartServer()
  * Desc : 启动TCP服务
  ****************************************************/
  BOOL StartServer(
   const char *szIp //要启动服务的本地地址, 若为NULL则采用默认地址
   , INT nPort //要启动服务的端口
   , LPCLOSE_ROUTINE pCloseFun  //客户端socket关闭的通知函数
   , LPVOID pParam     //close函数的参数
   );

  /****************************************************
  * Name : CloseServer()
  * Desc : 关闭TCP服务
  ****************************************************/
  void CloseServer();

  /****************************************************
  * Name : SendData()
  * Desc : 对客户端hSock发送长度为nDataLen的数据
  ****************************************************/
  BOOL SendData(SOCKET hSock, const CHAR* szData, INT nDataLen);

  /****************************************************
  * Name : GetRcvData()
  * Desc : 从接收数据队列中获取一个接收数据包
  * pQueLen 不为NULL时返回其长度
  ****************************************************/
  TCP_RCV_DATA* GetRcvData(
   DWORD* const pQueLen
   );
 protected:
  enum
  {
   LISTEN_EVENTS = 2,     //监听socket的事件个数
   MAX_ACCEPT = 50,      //每次最多投递的accept操作的个数
   _SOCK_NO_RECV = 0xf0000000, //客户端socket已连接上但为发送数据
   _SOCK_RECV = 0xf0000001 //客户端socket已连接上并也收到数据  
  };

  vector<TCP_RCV_DATA* > m_RcvDataQue;  //接收到的数据缓冲区队列
  CRITICAL_SECTION m_RcvQueLock; //访问m_RcvDataQue的互斥锁

  vector<SOCKET> m_SocketQue; //连接本服务器的客户端socket队列
  CRITICAL_SECTION m_SockQueLock;  //访问m_SocketQue的互斥锁

  LPCLOSE_ROUTINE m_pCloseFun; //客户端socket关闭的通知函数
  LPVOID m_pCloseParam; //传递给m_pCloseFun的用户参数

  SOCKET m_hSock;    //要进行服务器监听的socket
  long volatile m_bThreadRun; //是否允许后台线程继续运行
  long volatile m_nAcceptCount;    //当前已投递的accept操作的个数
  BOOL m_bSerRun; //服务是否正在运行

  //accept的事件
  HANDLE m_ListenEvents[LISTEN_EVENTS];
  HANDLE *m_pThreads;    //创建的后台线程的句柄
  HANDLE m_hCompletion;     //完成端口句柄

  static LPFN_ACCEPTEX s_pfAcceptEx;    //AcceptEx地址
  // GetAcceptExSockaddrs的地址
  static LPFN_GETACCEPTEXSOCKADDRS s_pfGetAddrs; 

  /****************************************************
  * Name : AcceptCompletionProc()
  * Desc : acceptEx操作完成后回调函数
  ****************************************************/
  void AcceptCompletionProc(BOOL bSuccess, DWORD dwNumberOfBytesTransfered, LPOVERLAPPED lpOverlapped);

  /****************************************************
  * Name : RecvCompletionProc()
  * Desc : 接收操作完成后的回调函数
  ****************************************************/
  void RecvCompletionProc(BOOL bSuccess, DWORD dwNumberOfBytesTransfered, LPOVERLAPPED lpOverlapped);

  /****************************************************
  * Name : SendCompletionProc()
  * Desc : 发送操作完成后的回调函数
  ****************************************************/
   void SendCompletionProc(BOOL bSuccess, DWORD dwNumberOfBytesTransfered, LPOVERLAPPED lpOverlapped);

  /****************************************************
  * Name : ListenThread()
  * Desc : 监听线程
  ****************************************************/
  static UINT WINAPI ListenThread(LPVOID lpParam);

  /****************************************************
  * Name : WorkThread()
  * Desc : 在完成端口上工作的后台线程
  ****************************************************/
  static UINT WINAPI WorkThread(LPVOID lpParam);

  /****************************************************
  * Name : AideThread()
  * Desc :  后台辅助线程
  ****************************************************/
  static UINT WINAPI AideThread(LPVOID lpParam);
 };

下面将对相关实现细节作详细介绍。

 
 也许您已经注意到本类只提供了客户端socket关闭的接口, 而没有提供客户端连接到服务器的相关接口;这样做的主要原因是因为当一个客户端连接成功
需要在完成端口的I/O线程中进行通知, 若用户在该接口中进行复杂的运算操作将会使I/O工作线程阻塞.所以此处没有提供连接成功的通知接口, 其实用
户可以根据客户端发来的特定数据包(例如登陆数据包)确定用户是否连接到本服务器。

  当有客户端连接服务器投递的accept操作就会完
成, m_ListenEvents[1] 事件对象就会授信这时ListenThread线程将被唤醒并投递一个accept操作. 若有大量的客户端
连接到本服务器而没有足够的accept接受连接此时m_ListenEvents[0]事件就会受信此时ListenThread线程会再次投递
MAX_ACCEPT个accept操作已接受更多的连接。

  ListenThread线程主要用来投递aeecptex操作, 当m_ListenEvents[0]或者m_ListenEvents[1]受信时就会投递一定量的AcceptEx操作以接受更多的客户端连接。

 
 WorkThread 线程工作在完成端口上, 当相关的操作完成时该线程组负责从完成端口队列上取得相应的完成封包进行处理. AideThread
线程主要用于维护连接本服务器的socket队列, 如果客户端连接到服务器但长时间没有进行发送数据便断开该客户端, 防止客户端恶意连接.当有客户端
断开连接时也在该线程中调用关闭接口通知用户。

  由于代码量太大无法全部详解, 如有需要欢迎邮件联系我: siqiang0312@163.com, QQ: 24633959, MSN: beifangying@hotmail.com

  本服务器的测试程序在WindowsXP的32位平台下测试时可以同时接受20K个客户端同时连接到服务器, CPU只占10%, 内存占20M左右。

【上篇】
【下篇】

抱歉!评论已关闭.