前言网络
多年前开发了一套网络库,底层实现采用IOCP(完成端口)。该库已在公司多个程序中应用;通过屡次修改,长时间检验,已经很是稳定高效。socket
最近把之前的代码梳理了一下,又加进了一些新的思路。代码结构更加合理,性能也有所提高。打算将该库一些的知识点写出来,以供参考。函数
服务端要在多个端口监听,这种场合并很少见。但做为一个完善的网络库,彷佛有必要支持此功能的。性能
传统实现方法优化
若是监听端口个数不多,也能够采用传统的方法。由于accept函数是阻塞的,因此要实如今n个端口监听,就须要n个线程。若是监听端口个数很少,这也不是多大问题。若是监听端口多达几十个,这种方法就有些不妥。线程也是一种资源,线程过多占用资源会增长;也会致使系统负担加剧。spa
更可行的实现方法.net
实现方法有些曲折,须要一步一步分析;基本的原理就是将socket句柄与事件(event)相关联。Windows有相关的函数能够对多个事件监听,当某个事件被触发,就知道相应的socket有事件到达。能够对该socket作accept,由于已经肯定该socket有事件了,因此accept函数会当即返回。这样就达到对多个端口同时监听的目的。线程
1)生成socket,并与某个端口绑定指针
struct LISTEN_SOCKET_INFO { UINT16 listenPort; //监听端口 SOCKET listenSocket;//句柄 WSAEVENT netEvent; //socket对应事件 }; int IocpAccept::CreateListenInfo() { //m_listListenPort存储要监听的端口;总个数不超过64个 std::vector<UINT16>::iterator pos = m_listListenPort.begin(); for (;pos != m_listListenPort.end();++pos) { //生成socket UINT16 listenPort = *pos; LISTEN_SOCKET_INFO socketInfo; socketInfo.listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); socketInfo.listenPort = listenPort; //绑定端口 sockaddr_in InetAddr; InetAddr.sin_family = AF_INET; InetAddr.sin_addr.s_addr = htonl(INADDR_ANY); InetAddr.sin_port = htons(listenPort); int ret = bind(socketInfo.listenSocket, (SOCKADDR *)&InetAddr, sizeof(InetAddr)); if (SOCKET_ERROR == ret) { ::closesocket(socketInfo.listenSocket); //绑定失败 continue; } //生成事件 socketInfo.netEvent = WSACreateEvent(); //将socket句柄与事件关联起来。只监视socket的accept和close消息 ret = WSAEventSelect(socketInfo.listenSocket, socketInfo.netEvent, FD_ACCEPT | FD_CLOSE); if (SOCKET_ERROR == ret) { ::closesocket(socketInfo.listenSocket); continue; } // 启动监听 ret = listen(socketInfo.listenSocket, 1000); if (SOCKET_ERROR == ret) { ::closesocket(socketInfo.listenSocket); continue; } m_listListenInfo.push_back(socketInfo); } return 0; }
该函数已将须要的数据存储在列表m_listListenInfo中。code
2)启动监听线程,对多个事件监听
对多个事件监听用到以下函数:
DWORD WSAAPI WSAWaitForMultipleEvents( DWORD cEvents, const WSAEVENT *lphEvents, BOOL fWaitAll, DWORD dwTimeout, BOOL fAlertable );该函数最多能够对64个事件作跟踪,因此一个线程最多能够对64个端口作监听。(同时对超过64个端口监听的场合很是少见。本文不考虑。)
//生成事件地址指针 int nEventTotal; WSAEVENT* pEventArray = CreateNetEventArray(&nEventTotal); if (nEventTotal == 0) return 0; assert(nEventTotal <= WSA_MAXIMUM_WAIT_EVENTS); MSG msg; while (m_bServerStart) { // 同时对多个事件作监听 DWORD index = WSAWaitForMultipleEvents(nEventTotal, pEventArray, FALSE, 10000, FALSE); if (!m_bServerStart) return 0; //查看是哪一个事件触发函数返回 index = index - WSA_WAIT_EVENT_0; //客户端链接事件 if ((index != WSA_WAIT_FAILED) && (index != WSA_WAIT_TIMEOUT)) { //pEventArray排序与m_listListenInfo同样,因此能够根据index找到对应的socket。 //就是该socket致使函数返回 LISTEN_SOCKET_INFO socketInfo = m_listListenInfo[index]; //查看具体是什么事件致使函数返回 WSANETWORKEVENTS NetworkEvents; WSAEnumNetworkEvents(socketInfo.listenSocket, pEventArray[index], &NetworkEvents); //若是是accept事件,说明有客户端链接此端口 if (NetworkEvents.lNetworkEvents == FD_ACCEPT && NetworkEvents.iErrorCode[FD_ACCEPT_BIT] == 0) { //这时调用accept函数,会当即返回 AcceptListenPort(socketInfo.listenSocket, socketInfo.listenPort); } if (NetworkEvents.lNetworkEvents == FD_CLOSE && NetworkEvents.iErrorCode[FD_CLOSE_BIT] == 0) { assert(false); } } else { //由于超时等其余缘由引发函数返回 } }
下文accept函数调用,并不会阻塞。
UINT IocpAccept::AcceptListenPort(SOCKET hListenSocket, UINT16 nListenPort) { SOCKET hClient = 0; SOCKADDR_IN localAddr; int iaddrSize = sizeof(SOCKADDR_IN); hClient = accept(hListenSocket, (struct sockaddr *)&localAddr, &iaddrSize); if (INVALID_SOCKET == hClient) { int nAccepetError = WSAGetLastError(); if (nAccepetError == WSAECONNRESET) { return 1; } else { return 0; } } else { //获取了一个客户端链接 OnAcceptClient(hClient, nListenPort); } return 0; }
后记:同时对多个端口作监听,可能还有更好的方法。若是对几百个以上端口作监听,此方法可能就不太合适。一般状况下,对多个端口监听的场景比较少见,因此对更优化的处理方法也没深究。
代码下载地址: https://download.csdn.net/download/qq_29939347/10691921