1、WinSock2 编程之打造完整的 SOCKET 池在 Winodows 平台上,网络编程的主要接口就是 WinSock,目前大多数的 Windows 平台上的WinSock 平台已经升级到 2.0 版,简称为 WinSock2。在 WinSock2 中扩展了很多很有用的 Windows 味很浓的 SOCKET 专用 API,为 Windows 平台用户提供高性能的网络编程支持。这些函数中的大多数已经不再是标准的“Berkeley” 套接字模型的 API 了。使用这些函数的代价就是你不能再将你的网络程序轻松的移植到“尤里平台”(我给 Unix +Linux 平台的简称)下,反过来因为 Wind
2、ows 平台支持标准的“Berkeley”套接字模型,所以你可以将大多数尤里平台下的网络应用移植到 Windows 平台下。如果不考虑可移植性(或者所谓的跨平台性),而是着重于应用的性能时,尤其是注重服务器性能时,对于 Windows 的程序,都鼓励使用 WinSock2 扩展的一些 API,更鼓励使用 IOCP 模型,因为这个模型是目前 Windows 平台上比较完美的一个高性能 IO 编程模型,它不但适用于 SOCKET 编程,还适用于读写硬盘文件,读写和管理命名管道、邮槽等等。如果再结合 Windows 线程池,IOCP 几乎可以利用当今硬件所有可能的新特性(比如多核,DMA,高速总线等
3、等),本身具有先天的扩展性和可用性。今天讨论的重点就是 SOCKET 池。很多 VC 程序员也许对 SOCKET 池很陌生,也有些可能很熟悉,那么这里就先讨论下这个概念。在 Windows 平台上 SOCKET 实际上被视作一个内核对象的句柄,很多 Windows API 在支持传统的 HANDLE 参数的同时也支持 SOCKET,比如有名的 CreateIoCompletionPort 就支持将 SOCKET 句柄代替 HANDLE 参数传入并调用。熟悉 Windows 内核原理的读者,立刻就会发现,这样的话,我们创建和销毁一个 SOCKET 句柄,实际就是在系统内部创建了一个内核对象,对于
4、 Windows 来说这牵扯到从Ring3 层到 Ring0 层的耗时操作,再加上复杂的安全审核机制,实际创建和销毁一个 SOCKET 内核对象的成本还是蛮高的。尤其对于一些面向连接的 SOCKET 应用,服务端往往要管理 n 多个代表客户端通信的 SOCKET 对象,而且因为客户的变动性,主要面临的大量操作除了一般的收发数据,剩下的就是不断创建和销毁 SOCKET 句柄,对于一个频繁接入和断开的服务器应用来说,创建和销毁 SOCKET 的性能代价立刻就会体现出来,典型的例如 WEB 服务器程序,就是一个需要频繁创建和销毁 SOCKET 句柄的SOCKET 应用。这种情况下我们通常都希望对于断
5、开的 SOCKET 对象,不是简单的“销毁”了之(很多时候“断开”的含义不一定就等价于“ 销毁” ,可以仔细思考一下),更多时候希望能够重用这个 SOCKET 对象,这样我们甚至可以事先创建一批 SOCKET 对象组成一个“ 池”,在需要的时候“重用” 其中的 SOCKET 对象,不需要的时候将 SOCKET 对象重新丢入池中即可,这样就省去了频繁创建销毁 SOCKET 对象的性能损失。在原始的“Berkeley” 套接字模型中,想做到这点是没有什么办法的。而幸运的是在 Windows 平台上,尤其是支持 WinSock2 的平台上,已经提供了一套完整的 API 接口用于支持 SOCKET 池
6、。对于符合以上要求的 SOCKET 池,首先需要做到的就是对 SOCKET 句柄的“回收”,因为创建函数无论在那个平台上都是现成的,而最早能够实现这个功能的 WinSock 函数就是 TransmitFile,如果代替closesocket 函数像下面这样调用就可以“ 回收”一个 SOCKET 句柄,而不是销毁:(注意“回收”这个功能对于 TransmitFile 函数来说只是个“ 副业”。)TransmitFile(hSocket,NULL,0,0,NULL,NULL,TF_DISCONNECT | TF_REUSE_SOCKET );注意上面函数的最后一个参数,使用了标志 TF_DISCO
7、NNECT 和 TF_REUSE_SOCKET,第一个值表示断开,第二个值则明确的表示“ 重用” 实际上也就是回收这个 SOCKET,经过这个处理的 SOCKET句柄,就可以直接再用于 connect 等操作,但是此时我们会发现,这个回收来的 SOCKET 似乎没什么用,因为其他套接字函数没法直接利用这个回收来的 SOCKET 句柄。这时就要 WinSock2 的一组专用 API 上场了。我将它们按传统意义上的服务端和客户端分为两组:一、 服务端:SOCKET WSASocket(_in int af,_in int type,_in int protocol,_in LPWSAPROTOCO
8、L_INFO lpProtocolInfo,_in GROUP g,_in DWORD dwFlags);BOOL AcceptEx(_in SOCKET sListenSocket,_in SOCKET sAcceptSocket,_in PVOID lpOutputBuffer,_in DWORD dwReceiveDataLength,_in DWORD dwLocalAddressLength,_in DWORD dwRemoteAddressLength,_out LPDWORD lpdwBytesReceived,_in LPOVERLAPPED lpOverlapped);BOO
9、L DisconnectEx(_in SOCKET hSocket,_in LPOVERLAPPED lpOverlapped,_in DWORD dwFlags,_in DWORD reserved);二、 客户端:SOCKET WSASocket(_in int af,_in int type,_in int protocol,_in LPWSAPROTOCOL_INFO lpProtocolInfo,_in GROUP g,_in DWORD dwFlags);BOOL PASCAL ConnectEx(_in SOCKET s,_in const struct sockaddr* na
10、me,_in int namelen,_in_opt PVOID lpSendBuffer,_in DWORD dwSendDataLength,_out LPDWORD lpdwBytesSent,_in LPOVERLAPPED lpOverlapped);BOOL DisconnectEx(_in SOCKET hSocket,_in LPOVERLAPPED lpOverlapped,_in DWORD dwFlags,_in DWORD reserved);注意观察这些函数,似乎和传统的“Berkeley”套接字模型中的一些函数“ 大同小异”,其实仔细观察他们的参数,就已经可以发现一
11、些调用他们的“玄机” 了。首先我们来看 AcceptEx 函数,与 accept 函数不同,它需要两个 SOCKET 句柄作为参数,头一个参数的含义与 accept 函数的相同,而第二个参数的意思就是 accept 函数返回的那个代表与客户端通信的SOCKET 句柄,在传统的 accept 内部,实际在返回那个代表客户端的 SOCKET 时,是在内部调用了一个 SOCKET 的创建动作,先创建这个 SOCKET 然后再“accept”让它变成代表客户端连接的 SOCKET,而 AcceptEx 函数就在这里“扩展”(实际上是“阉割” 才对)accept 函数,省去了内部那个明显的创建SOCKE
12、T 的动作,而将这个创建动作交给最终的调用者自己来实现。 AcceptEx 要求调用者创建好那个sAcceptSocket 句柄然后传进去,这时我们立刻发现,我们回收的那个 SOCKET 是不是也可以传入呢?答案是肯定的,我们就是可以利用这个函数传入那个“回收” 来的 SOCKET 句柄,最终实现服务端的SOCKET 重用。这里需要注意的就是,AcceptEx 函数必须工作在非阻塞的 IOCP 模型下,同时即使 AcceptEx 函数返回了,也不代表客户端连接进来或者连接成功了,我们必须依靠它的“完成通知” 才能知道这个事实,这也是 AcceptEx 函数区别于 accept 这个阻塞方式函数
13、的最大之处。通常可以利用 AcceptEx 的非阻塞特性和 IOCP 模型的优点,一次可以“预先” 发出成千上万个 AcceptEx 调用,“ 等待”客户端的连接。对于习惯了accept 阻塞方式的程序员来说,理解 AcceptEx 的工作方式还是需要费一些周折的。下面的例子就演示了如何一次调用多个 AcceptEx:/批量创建 SOCKET,并调用对应的 AcceptExfor(UINT i = 0; i GetAddrBuf();/4、发出 AcceptEx 调用/注意将 AcceptEx 函数接收连接数据缓冲的大小设定成了 0,这将导致此函数立即返回,虽然与/不设定成 0 的方式而言,这
14、导致了一个较低下的效率,但是这样提高了安全性,所以这种效率/牺牲是必须的if(!AcceptEx(m_skServer, skAccept,pAddrBuf-m_pBuf, 0,/将接收缓冲置为 0,令 AcceptEx 直接返回,防止拒绝服务攻击GRS_ADDRBUF_SIZE, GRS_ADDRBUF_SIZE, NULL,(LPOVERLAPPED)pAcceptOL)int iError = WSAGetLastError();if( ERROR_IO_PENDING != iError skAccept = INVALID_SOCKET;if( NULL != pAcceptOL)G
15、RS_ISVALID(pAcceptOL,sizeof(CGRSOverlappedData);delete pAcceptOL;pAcceptOL = NULL;以上的例子只是简单的演示了 AcceptEx 的调用,还没有涉及到真正的“回收重用” 这个主题,那么下面的例子就演示了如何重用一个 SOCKET 句柄:if(INVALID_SOCKET = skClient)throw CGRSException(_T(“SOCKET 句柄是无效的!“);OnPreDisconnected(skClient,pUseData,0);CGRSOverlappedData*pData= new GRS
16、OverlappedData(GRS_OP_DISCONNECTEX,this,skClient,pUseData);/回收而不是关闭后再创建大大提高了服务器的性能DisconnectEx(skClient, ./在接收到 DisconnectEx 函数的完成通知之后,我们就可以重用这个 SOCKET 了CGRSAddrbuf*pBuf = NULL;pNewOL = new CGRSOverlappedData(GRS_OP_ACCEPT,this,skClient,pUseData);pBuf = pNewOL-GetAddrBuf();/把这个回收的 SOCKET 重新丢进连接池if(!
17、AcceptEx(m_skServer,skClient,pBuf-m_pBuf, 0,/将接收缓冲置为 0,令 AcceptEx 直接返回,防止拒绝服务攻击GRS_ADDRBUF_SIZE, GRS_ADDRBUF_SIZE, NULL,(LPOVERLAPPED)pNewOL)int iError = WSAGetLastError();if( ERROR_IO_PENDING != iError /注意在这个 SOCKET 被重新利用后,重新与 IOCP 绑定一下,该操作会返回一个已设置的错误,这个错误直接被忽略即可:BindIoCompletionCallback(HANDLE)skC
18、lient,Server_IOCPThread, 0);至此回收重用 SOCKET 的工作也就结束了,以上的过程实际理解了 IOCP 之后就比较好理解了,例子的最后我们使用了 BindIoCompletionCallback 函数重新将 SOCKET 丢进了 IOCP 线程池中,实际还可以再次使用 CreateIoCompletionPort 函数达到同样的效果,这里列出这一步就是告诉大家,不要忘了再次绑定一下完成端口和 SOCKET。对于客户端来说,可以使用 ConnectEx 函数来代替 connect 函数,与 AcceptEx 函数相同,ConnectEx 函数也是以非阻塞的 IOCP
19、 方式工作的,唯一要注意的就是在 WSASocket 调用之后,在ConnectEx 之前要调用一下 bind 函数,将 SOCKET 提前绑定到一个本地地址端口上,当然回收重用之后,就无需再次绑定了,这也是 ConnectEx 较之 connect 函数高效的地方之一。与 AcceptEx 函数类似,也可以一次发出成千上万个 ConnectEx 函数的调用,可以连接到不同的服务器,也可以连接到相同的服务器,连接到不同的服务器时,只需提供不同的 sockaddr 即可。通过上面的例子和讲解,大家应该对 SOCKET 池概念以及实际的应用有个大概的了解了,当然核心仍然是理解了 IOCP 模型,否
20、则还是寸步难行。在上面的例子中,回收 SOCKET 句柄主要使用了 DisconnectEx 函数,而不是之前介绍的TransmitFile 函数,为什么呢?因为 TransmitFile 函数在一些情况下会造成死锁,无法正常回收SOCKET,毕竟不是专业的回收重用 SOCKET 函数,我就遇到过好几次死锁,最后偶然的发现了DisconnectEx 函数这个专用的回收函数,调用之后发现比 TransmitFile 专业多了,而且不管怎样都不会死锁。最后需要补充的就是这几个函数的调用方式,不能像传统的 SOCKET API 那样直接调用它们,而需要使用一种间接的方式来调用,尤其是 AcceptE
21、x 和 DisconnectEx 函数,下面给出了一个例子类,用于演示如何动态载入这些函数并调用之:class CGRSMsSockFunpublic:CGRSMsSockFun(SOCKET skTemp = INVALID_SOCKET)if( INVALID_SOCKET != skTemp )LoadAllFun(skTemp);public:virtual CGRSMsSockFun(void)protected:BOOL LoadWSAFun(SOCKETBOOL bRet = TRUE;pFun = NULL;BOOL bCreateSocket = FALSE;tryif(IN
22、VALID_SOCKET = skTemp)skTemp = :WSASocket(AF_INET,SOCK_STREAM, IPPROTO_TCP,NULL,0,WSA_FLAG_OVERLAPPED);bCreateSocket = (skTemp != INVALID_SOCKET);if(INVALID_SOCKET = skTemp)throw CGRSException(DWORD)WSAGetLastError();if(SOCKET_ERROR = :WSAIoctl(skTemp,SIO_GET_EXTENSION_FUNCTION_POINTER, throw CGRSException(DWORD)WSAGetLastError();catch(CGRSExceptionreturn NULL != pFun;protected:LPFN_ACCEPTEX m_pfnAcceptEx;