相关UML:
//
重叠结构类
class COverLapped { // 变量定义 public : WSABUF m_WSABuffer; // 数据指针 OVERLAPPED m_OverLapped; // 重叠结构 const enOperationType m_OperationType; // 操作类型 // 函数定义 public : // 构造函数 COverLapped(enOperationType OperationType); // 析构函数 virtual ~ COverLapped(); // 信息函数 public : // 获取类型 enOperationType GetOperationType() { return m_OperationType; } }; // 接收重叠结构 class COverLappedSend : public COverLapped { // 数据变量 public : BYTE m_cbBuffer[SOCKET_BUFFER]; // 数据缓冲 // 函数定义 public : // 构造函数 COverLappedSend(); // 析构函数 virtual ~ COverLappedSend(); }; // 重叠结构模板 template < enOperationType OperationType > class CATLOverLapped : public COverLapped { // 函数定义 public : // 构造函数 CATLOverLapped() : COverLapped(OperationType) {} // 析构函数 virtual ~ CATLOverLapped() {} }; 先复习下基础,Windows下的网络模型有很多种,这里只拿出三种来说: EventSelect:基于信号机制,以socket为单位绑定信号量,当socket上有指定的事件发生时激发信号,然后查询事件处理事件重设事件,继续在信号量上等待。其实也是在伯克利select模型上的换不换药的加强。 OverLapped:分两种工作模式完成回调,和完成事件。重叠IO监视每次操作,每次IO都绑定一个重叠对象,当操作完成以后激发信号或者调用回调。 IOCP:和overlapped类似,不过结果经过了Windows的预处理以队列的形式挂在完成端口上 根据上面的复习,可以得出一个结论,IOCP环境中每一次IO操作都需要一个重叠结构,那么一个CServerSocketItem至少需要如些这些东东: 他要接受数据,所以必须有一个接受数据的 OverLapped 它要发送数据,说以必须有一个发送数据的 OverLapped netFox对OverLapped做了使用了类似池的的管理手段,他的Send都是不等待上一次完成就直接投递下一个请求了,,,这是很操蛋的做法,,, 然后继续复习下基础: 在EventSelect模型中获处理件类型流程是这样: event受信,使用::WSAEnumNetworkEvents查询和这个event关联的socket发生的事件,根据查询到的事件类型去处理事件 在以每一次IO为查询对象重叠IO、IOCP模型中是这样: 使用GetOverlappedResult 或者 GetQueuedCompletionStatus然后根据重叠结构去查询投递的是什么类型的操作,然后找到关联的socket去操作,,, 这样必然要给OverLapped做个扩展,提供一种通过OverLapped查询操作类型和socket的能力。 通过分析代码,netFox关联socket是通过在创建完成端口的时候绑定SocketItem对象指针完成的,操作类型是通过对OverLapped结构加强完成的。 通过GetQueuedCompletionStatus获取到完成OverLapped以后使用一个宏: (这是COverLapped类型) pSocketLapped=CONTAINING_RECORD(pOverLapped,COverLapped,m_OverLapped); 来获取包装后的OverLapped,然后获取操作类型,然后执行具体操作。 其实宏的展开如下: (COverLapped*)((BYTE*)pOverLapped - (COverLapped*)(0)->m_OverLapped); pOverLapped是获取到的某个COverLapped中的成员变量,(COverLapped*)(0)->m_OverLapped是到在COverLapped中的偏移,((BYTE*)pOverLapped - (COverLapped*)(0)->m_OverLapped) 就是根据pOverLapped推算出来的包含地址为pOverLapped作为成员变量m_OverLapped的COverLapped对象的地址。 然后就分别调用: //发送完成函数 bool CServerSocketItem::OnSendCompleted(COverLappedSend * pOverLappedSend, DWORD dwThancferred); //接收完成函数 bool CServerSocketItem::OnRecvCompleted(COverLappedRecv * pOverLappedRecv, DWORD dwThancferred); 为毛要区分Send OverLapped 和 Recv OverLapped呢,,, 应为投递一次Send不一定是瞬间完成的,在处理的过程中存储数据的内存应该是锁定的,也就是不允许修改的,,,所以OverLapped应该自己管理内存。 而recv应该也是需要有一片内存直接接受数据的,很奇怪netFox没有提供,,, recv居然是在投递接受请求的时候给了一个空的buffer,然后在完成回调中自己再次调用recv方法接受数据。 接受有关的成员变量如下:
//
状态变量
int iRetCode=recv(m_hSocket,(char *)m_cbRecvBuf+m_wRecvSize,sizeof(m_cbRecvBuf)-m_wRecvSize,0);
protected : bool m_bNotify; // 通知标志 bool m_bRecvIng; // 接收标志 bool m_bCloseIng; // 关闭标志 bool m_bAllowBatch; // 接受群发 WORD m_wRecvSize; // 接收长度 BYTE m_cbRecvBuf[SOCKET_BUFFER * 5 ]; // 接收缓冲 难道这么蠢的做法只是为了躲开分包算法? 具体的看看接受代码:
//
接收完成函数
bool CServerSocketItem::OnRecvCompleted(COverLappedRecv * pOverLappedRecv, DWORD dwThancferred) { // 效验数据 ASSERT(m_bRecvIng == true ); // 设置变量 m_bRecvIng = false ; m_dwRecvTickCount = GetTickCount(); // 判断关闭 if (m_hSocket == INVALID_SOCKET) { CloseSocket(m_wRountID); return true ; } // 接收数据 int iRetCode = recv(m_hSocket,( char * )m_cbRecvBuf + m_wRecvSize, sizeof (m_cbRecvBuf) - m_wRecvSize, 0 ); if (iRetCode <= 0 ) { CloseSocket(m_wRountID); return true ; } // 接收完成 m_wRecvSize += iRetCode; BYTE cbBuffer[SOCKET_BUFFER]; CMD_Head * pHead = (CMD_Head * )m_cbRecvBuf; // 处理数据 try { while (m_wRecvSize >= sizeof (CMD_Head)) { // 效验数据 WORD wPacketSize = pHead -> CmdInfo.wDataSize; if (wPacketSize > SOCKET_BUFFER) throw TEXT( " 数据包超长 " ); if (wPacketSize < sizeof (CMD_Head)) throw TEXT( " 数据包非法 " ); if (pHead -> CmdInfo.cbMessageVer != SOCKET_VER) throw TEXT( " 数据包版本错误 " ); if (m_wRecvSize < wPacketSize) break ; // 提取数据 CopyMemory(cbBuffer,m_cbRecvBuf,wPacketSize); WORD wRealySize = CrevasseBuffer(cbBuffer,wPacketSize); ASSERT(wRealySize >= sizeof (CMD_Head)); m_dwRecvPacketCount ++ ; // 解释数据 WORD wDataSize = wRealySize - sizeof (CMD_Head); void * pDataBuffer = cbBuffer + sizeof (CMD_Head); CMD_Command Command = ((CMD_Head * )cbBuffer) -> CommandInfo; // 内核命令 if (Command.wMainCmdID == MDM_KN_COMMAND) { switch (Command.wSubCmdID) { case SUB_KN_DETECT_SOCKET: // 网络检测 { break ; } default : throw TEXT( " 非法命令码 " ); } } else { // 消息处理 m_pIServerSocketItemSink -> OnSocketReadEvent(Command,pDataBuffer,wDataSize, this ); } // 删除缓存数据 m_wRecvSize -= wPacketSize; MoveMemory(m_cbRecvBuf,m_cbRecvBuf + wPacketSize,m_wRecvSize); } } catch ( ![]() { CloseSocket(m_wRountID); return false ; } return RecvData(); } 这是还是有分包算法的,总的来说接受流程如下: 直接使用recv把数据接受到SocketItem的缓冲区中,当长度大于CMD_HEAD之后,进入处理阶段,处理head数据各种判断,然后将数据扔出去,再调整缓冲区,,, 简单的说: Send完全不考虑同步问题,不管一个劲的网队列投递Send请求,,,这边处理队列也是直接Send完事,完全不考虑上一次是否send成功,,, Recv更是莫名其妙的使用完成端口绕一圈还回到recv直接接受了,,, 很狗血的做法,,, 更正下我自己狗血的不理解: 如果一个服务器提交了非常多的重叠的receive在每一个连接上,那么限制会随着连接数的增长而变化。如果一个服务器能够预先估计可能会产生的最大并发连接数,服务器可以投递一个使用零缓冲区的receive在每一个连接上。因为当你提交操作没有缓冲区时,那么也不会存在内存被锁定了。使用这种办法后,当你的receive操作事件完成返回时,该socket底层缓冲区的数据会原封不动的还在其中而没有被读取到receive操作的缓冲区来。此时,服务器可以简单的调用非阻塞式的recv将存在socket缓冲区中的数据全部读出来,一直到recv返回 WSAEWOULDBLOCK 为止。 这种设计非常适合那些可以牺牲数据吞吐量而换取巨大 并发连接数的服务器。当然,你也需要意识到如何让客户端的行为尽量避免对服务器造成影响。在上一个例子中,当一个零缓冲区的receive操作被返回后使 用一个非阻塞的recv去读取socket缓冲区中的数据,如果服务器此时可预计到将会有爆发的数据流,那么可以考虑此时投递一个或者多个receive 来取代非阻塞的recv来进行数据接收。(这比你使用1个缺省的8K缓冲区来接收要好的多。) 源码中提供了一个简单实用的解决WSAENOBUF错误的办法。我们执行了一个零字节缓冲的异步WSARead(...)(参见 OnZeroByteRead(..))。当这个请求完成,我们知道在TCP/IP栈中有数据,然后我们通过执行几个有MAXIMUMPACKAGESIZE缓冲的异步WSARead(...)去读,解决了WSAENOBUFS问题。但是这种解决方法降低了服务器的吞吐量。 总结: |
转载于:https://www.cnblogs.com/HeroZearin/articles/2554658.html