首先说明,下面的代码仅是一个IOCP的demo,很多地方的设计非常差,当然也有一些设计还算可以:)。此篇仅供对IOCP有些了解但又不深入的、需要一个稍微完整示例的、对网络编程感兴趣的同学参考。点击这里下载代码
整个程序的流程如下:
流程完全是无阻塞的,主线程里,将收到的消息全都一次性取出后,然后派发。所有欲发送的消息都缓存起来,等到更新的时候一起发送。有些地方代码没有完善,比如断开连接后,socket、内存等资源的关闭回收。要注意MAXRECEIVEDBUFFLENGTH这个宏,它是定义每个socket消息收发时缓冲区的大小。如果很大,会非常吃内存的。我在这里也没有做粘包、分包的情况处理。工程还带了一个C#写的测试客户端。在我的机器上,能有12W的连接。
关键代码如下:
void MyIOCP::Execute() { PerHandleData* pPerHandleData = nullptr; PerIOData* pPerIOData = nullptr; LPOVERLAPPED lpOverLapped = nullptr; DWORD byteTransferred; BOOL bRet = FALSE; while (true) { bRet = GetQueuedCompletionStatus(mCompletionPort, &byteTransferred, (PULONG_PTR)&pPerHandleData, /*(LPOVERLAPPED*)&pPerIOData*/&lpOverLapped, INFINITE); pPerIOData = (PerIOData*)CONTAINING_RECORD(lpOverLapped, PerIOData, overlapped); if (bRet == FALSE) { if (pPerIOData == nullptr) { // 退出线程。服务器要关了。。 break; } // 对方主动断开了 continue; } // 处理成功的完成端口请求 switch (pPerIOData->operationType) { case CDH::E_INVALID: { } break; case CDH::E_ACCEPT: { /************************************************************************/ /* inet_ntoa(ClientAddr->sin_addr) 是客户端IP地址 ntohs(ClientAddr->sin_port) 是客户端连入的端口 inet_ntoa(LocalAddr ->sin_addr) 是本地IP地址 ntohs(LocalAddr -AZ>sin_port) 是本地通讯的端口 pIoContext->m_wsaBuf.buf 是存储客户端发来第一组数据的缓冲区 */ /************************************************************************/ SOCKADDR_IN* ClientAddr = NULL; SOCKADDR_IN* LocalAddr = NULL; int remoteLen = sizeof(SOCKADDR_IN), localLen = sizeof(SOCKADDR_IN); mlpfnGetAcceptExSockAddrs(pPerIOData->buffer, 0, sizeof(SOCKADDR_IN)+16, sizeof(SOCKADDR_IN)+16, (LPSOCKADDR*)&LocalAddr, &localLen, (LPSOCKADDR*)&ClientAddr, &remoteLen); WrapSocket* connectedSocket = GetSocketAndEraseFromTheMap(pPerIOData->remoteSocket, WRAPESOCKETTYPE::E_READYTOBECONNECTED); if (connectedSocket == nullptr) { closesocket(pPerIOData->remoteSocket); continue; } if (!AssociateDeviceWithCompletionPort(connectedSocket)) { closesocket(pPerIOData->remoteSocket); continue; } // 添加到已经连接列表 InsertWrapSocketToMap(connectedSocket, WRAPESOCKETTYPE::E_CONNECTED); PostReceive(connectedSocket); // 这次消耗了一个准备好了的socket, 现在再生成一个socket待连接。 PostAcceptEx(connectedSocket->GetPerHandleData().hasListenSocket); } break; case CDH::E_RECV: { int socket = pPerHandleData->selfSocket; WrapSocket* wrapSocket = nullptr; GetSocketForSendOrRecvData(socket, wrapSocket); if (wrapSocket != nullptr) { wrapSocket->GetPerIODataReceive().bufferLen = byteTransferred; Receive(wrapSocket); } } break; case CDH::E_SEND: { int socket = pPerHandleData->selfSocket; WrapSocket* wrapSocket = nullptr; GetSocketForSendOrRecvData(socket, wrapSocket); if (wrapSocket != nullptr) { wrapSocket->SendingData(false); } } break; case CDH::E_CONNECT: break; default: break; } } }
注意CDH::E_RECV,收到消息后,将消息缓存,然后直接又进行投递PostReceive(),这其实是非常不好的。应该分情况来选择是否立即投递。
以上,整个代码,基本是用C++的格式写C代码。希望以后能有机会与大家共同分享一个比较完整的、面向对象风格的IOCP。