完成端口封装(修复Windows 网络与通信程序设计 可伸缩IOCP模型的bug)

前言

看过《Windows网络与通信程序设计》的人都知道,里面有一段有关于IOCP的经典封装。大大方便了“伸手党”服务器端程序的开发(我也是其中之一)。但是应用到实际程序中你会发现经常出现一个莫名奇妙的问题:一旦客户端发送的字节数过多,服务器端接受其中几条后就“死掉”了,我也深受其害,于是乎 今天花了2小时时间通读了代码,把其中的bug找到(PS:不敢保证是否还是其他bug,暂时还未发现)


问题

引起上述现象的是由于作者的小疏忽导致的,我们知道采用完成端口时要开辟多个线程(一般为CPU核心数)来监听请求,如果客户端在短时间内发送了一大堆字节,
这些字节在客户端上肯定是分多次顺序进行发送。服务端接受的时候却是多个线程(单线程CPU例外)各自接受各自的原来的顺序就被打乱了,如果只是简单拼凑起来肯定会出现问题,如何解决这个问题呢,作者已经帮我想好了。虽然各个线程处理数据的顺序可能不一直,但是投递读请求的顺序肯定是与客户端发送的顺序是一直的,于是作者CIOCPBuffer中添加了一个顺序标识 nSequenceNumber 用来标识当前读取数据的发送顺序,并建立了一个队列将当前接受CIOCPBuffer联系起来
   
   
  1. // 这是per-I/O数据。它包含了在套节字上处理I/O操作的必要信息
  2. struct CIOCPBuffer
  3. {
  4. WSAOVERLAPPED ol;
  5. SOCKET sClient; // AcceptEx接收的客户方套节字
  6. char *buff; // I/O操作使用的缓冲区
  7. int nLen; // buff缓冲区(使用的)大小
  8. ULONG nSequenceNumber; // 此I/O的序列号
  9. int nOperation; // 操作类型
  10. #define OP_ACCEPT 1
  11. #define OP_WRITE 2
  12. #define OP_READ 3
  13. CIOCPBuffer *pNext;
  14. };

读取数据的时候先判断读取数据CIOCPBuffer的nSequenceNumber与当前队列头的CIOCPBuffer是否一致,一致则循环将队列中的所有Buffer按顺序传递给用户,不一致就将它按顺序加入队列。
这种设计是不是很巧妙。


作者的代码:
   
   
  1. CIOCPBuffer *CIOCPServer::GetNextReadBuffer(CIOCPContext *pContext, CIOCPBuffer *pBuffer)
  2. {
  3. if(pBuffer != NULL)
  4. {
  5. // 如果与要读的下一个序列号相等,则读这块缓冲区
  6. if(pBuffer->nSequenceNumber == pContext->nCurrentReadSequence)
  7. {
  8. return pBuffer;
  9. }
  10. // 如果不相等,则说明没有按顺序接收数据,将这块缓冲区保存到连接的pOutOfOrderReads列表中
  11. // 列表中的缓冲区是按照其序列号从小到大的顺序排列的
  12. pBuffer->pNext = NULL;
  13. CIOCPBuffer *ptr = pContext->pOutOfOrderReads;
  14. CIOCPBuffer *pPre = NULL;
  15. while(ptr != NULL)
  16. {
  17. if(pBuffer->nSequenceNumber < ptr->nSequenceNumber)
  18. break;
  19. pPre = ptr;
  20. ptr = ptr->pNext;
  21. }
  22. if(pPre == NULL) // 应该插入到表头
  23. {
  24. pBuffer->pNext = pContext->pOutOfOrderReads;
  25. pContext->pOutOfOrderReads = pBuffer;
  26. }
  27. else // 应该插入到表的中间
  28. {
  29. pBuffer->pNext = pPre->pNext;
  30. pPre->pNext = pBuffer->pNext;
  31. }
  32. }
  33. // 检查表头元素的序列号,如果与要读的序列号一致,就将它从表中移除,返回给用户
  34. CIOCPBuffer *ptr = pContext->pOutOfOrderReads;
  35. if(ptr != NULL && (ptr->nSequenceNumber == pContext->nCurrentReadSequence))
  36. {
  37. pContext->pOutOfOrderReads = ptr->pNext;
  38. return ptr;
  39. }
  40. return NULL;
  41. }

我处理后的代码
  
  
  1. CIOCPBuffer *CIOCPServer::GetNextReadBuffer(CIOCPContext *pContext, CIOCPBuffer *pBuffer)
  2. {
  3. if(pBuffer != NULL)
  4. {
  5. // 如果与要读的下一个序列号相等,则读这块缓冲区
  6. if(pBuffer->nSequenceNumber == pContext->nCurrentReadSequence)
  7. {
  8. return pBuffer;
  9. }
  10. // 如果不相等,则说明没有按顺序接收数据,将这块缓冲区保存到连接的pOutOfOrderReads列表中
  11. // 列表中的缓冲区是按照其序列号从小到大的顺序排列的
  12. pBuffer->pNext = NULL;
  13. CIOCPBuffer *ptr = pContext->pOutOfOrderReads;
  14. CIOCPBuffer *pPre = NULL;
  15. while(ptr != NULL)
  16. {
  17. if(pBuffer->nSequenceNumber < ptr->nSequenceNumber)
  18. break;
  19. pPre = ptr;
  20. ptr = ptr->pNext;
  21. }
  22. if(pPre == NULL) // 应该插入到表头
  23. {
  24. pBuffer->pNext = pContext->pOutOfOrderReads;
  25. pContext->pOutOfOrderReads = pBuffer;
  26. }
  27. else // 应该插入到表的中间
  28. {
  29. pBuffer->pNext = pPre->pNext;
  30. pPre->pNext = pBuffer;
  31. }
  32. }
  33. // 检查表头元素的序列号,如果与要读的序列号一致,就将它从表中移除,返回给用户
  34. CIOCPBuffer *ptr = pContext->pOutOfOrderReads;
  35. if(ptr != NULL && (ptr->nSequenceNumber == pContext->nCurrentReadSequence))
  36. {
  37. pContext->pOutOfOrderReads = ptr->pNext;
  38. return ptr;
  39. }
  40. return NULL;
  41. }

对,你没有看错,仅仅是这么简单一句代码,就造成了整个程序的错误。 看不懂的同学自己去看看如何向链表中见插入一个元素吧。

废话不多说上一段完整程序:
   
   
  1. ////////////////////////////////////////
  2. // IOCP.h文件
  3. #ifndef __IOCP_H__
  4. #define __IOCP_H__
  5. #include <winsock2.h>
  6. #include <windows.h>
  7. #include <Mswsock.h>
  8. #define BUFFER_SIZE 1024*2 // I/O请求的缓冲区大小
  9. // 这是per-I/O数据。它包含了在套节字上处理I/O操作的必要信息
  10. struct CIOCPBuffer
  11. {
  12. WSAOVERLAPPED ol;
  13. SOCKET sClient; // AcceptEx接收的客户方套节字
  14. char *buff; // I/O操作使用的缓冲区
  15. int nLen; // buff缓冲区(使用的)大小
  16. ULONG nSequenceNumber; // 此I/O的序列号
  17. int nOperation; // 操作类型
  18. #define OP_ACCEPT 1
  19. #define OP_WRITE 2
  20. #define OP_READ 3
  21. CIOCPBuffer *pNext;
  22. };
  23. // 这是per-Handle数据。它包含了一个套节字的信息
  24. struct CIOCPContext
  25. {
  26. SOCKET s; // 套节字句柄
  27. SOCKADDR_IN addrLocal; // 连接的本地地址
  28. SOCKADDR_IN addrRemote; // 连接的远程地址
  29. BOOL bClosing; // 套节字是否关闭
  30. int nOutstandingRecv; // 此套节字上抛出的重叠操作的数量
  31. int nOutstandingSend;
  32. ULONG nReadSequence; // 安排给接收的下一个序列号
  33. ULONG nCurrentReadSequence; // 当前要读的序列号
  34. CIOCPBuffer *pOutOfOrderReads; // 记录没有按顺序完成的读I/O
  35. CRITICAL_SECTION Lock; // 保护这个结构
  36. CIOCPContext *pNext;
  37. };
  38. class CIOCPServer // 处理线程
  39. {
  40. public:
  41. CIOCPServer();
  42. ~CIOCPServer();
  43. // 开始服务
  44. BOOL Start(int nPort = 4567, int nMaxConnections = 2000,
  45. int nMaxFreeBuffers = 200, int nMaxFreeContexts = 100, int nInitialReads = 4);
  46. // 停止服务
  47. void Shutdown();
  48. // 关闭一个连接和关闭所有连接
  49. void CloseAConnection(CIOCPContext *pContext);
  50. void CloseAllConnections();
  51. // 取得当前的连接数量
  52. ULONG GetCurrentConnection() { return m_nCurrentConnection; }
  53. // 向指定客户发送文本
  54. BOOL SendText(CIOCPContext *pContext, char *pszText, int nLen);
  55. // 获得本机处理器的数量
  56. static int _GetNoOfProcessors();
  57. protected:
  58. // 申请和释放缓冲区对象
  59. CIOCPBuffer *AllocateBuffer(int nLen);
  60. void ReleaseBuffer(CIOCPBuffer *pBuffer);
  61. // 申请和释放套节字上下文
  62. CIOCPContext *AllocateContext(SOCKET s);
  63. void ReleaseContext(CIOCPContext *pContext);
  64. // 释放空闲缓冲区对象列表和空闲上下文对象列表
  65. void FreeBuffers();
  66. void FreeContexts();
  67. // 向连接列表中添加一个连接
  68. BOOL AddAConnection(CIOCPContext *pContext);
  69. // 插入和移除未决的接受请求
  70. BOOL InsertPendingAccept(CIOCPBuffer *pBuffer);
  71. BOOL RemovePendingAccept(CIOCPBuffer *pBuffer);
  72. // 取得下一个要读取的
  73. CIOCPBuffer *GetNextReadBuffer(CIOCPContext *pContext, CIOCPBuffer *pBuffer);
  74. // 投递接受I/O、发送I/O、接收I/O
  75. BOOL PostAccept(CIOCPBuffer *pBuffer);
  76. BOOL PostSend(CIOCPContext *pContext, CIOCPBuffer *pBuffer);
  77. BOOL PostRecv(CIOCPContext *pContext, CIOCPBuffer *pBuffer);
  78. void HandleIO(DWORD dwKey, CIOCPBuffer *pBuffer, DWORD dwTrans, int nError);
  79. // 事件通知函数
  80. // 建立了一个新的连接
  81. virtual void OnConnectionEstablished(CIOCPContext *pContext, CIOCPBuffer *pBuffer);
  82. // 一个连接关闭
  83. virtual void OnConnectionClosing(CIOCPContext *pContext, CIOCPBuffer *pBuffer);
  84. // 在一个连接上发生了错误
  85. virtual void OnConnectionError(CIOCPContext *pContext, CIOCPBuffer *pBuffer, int nError);
  86. // 一个连接上的读操作完成
  87. virtual void OnReadCompleted(CIOCPContext *pContext, CIOCPBuffer *pBuffer);
  88. // 一个连接上的写操作完成
  89. virtual void OnWriteCompleted(CIOCPContext *pContext, CIOCPBuffer *pBuffer);
  90. protected:
  91. // 记录空闲结构信息
  92. CIOCPBuffer *m_pFreeBufferList;
  93. CIOCPContext *m_pFreeContextList;
  94. int m_nFreeBufferCount;
  95. int m_nFreeContextCount;
  96. CRITICAL_SECTION m_FreeBufferListLock;
  97. CRITICAL_SECTION m_FreeContextListLock;
  98. // 记录抛出的Accept请求
  99. CIOCPBuffer *m_pPendingAccepts; // 抛出请求列表。
  100. long m_nPendingAcceptCount;
  101. CRITICAL_SECTION m_PendingAcceptsLock;
  102. // 记录连接列表
  103. CIOCPContext *m_pConnectionList;
  104. int m_nCurrentConnection;
  105. CRITICAL_SECTION m_ConnectionListLock;
  106. // 用于投递Accept请求
  107. HANDLE m_hAcceptEvent;
  108. HANDLE m_hRepostEvent;
  109. LONG m_nRepostCount;
  110. int m_nThread;
  111. int m_nPort; // 服务器监听的端口
  112. int m_nInitialAccepts;
  113. int m_nInitialReads;
  114. int m_nMaxAccepts;
  115. int m_nMaxSends;
  116. int m_nMaxFreeBuffers;
  117. int m_nMaxFreeContexts;
  118. int m_nMaxConnections;
  119. HANDLE m_hListenThread
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值