BindIoCompletionCallback

四、编写线程池回调函数:

 

在讨论扩展定义OVERLAPPED结构体时,给出了非线程池版的线程函数的大概框架,也就是传统IOCP使用的自建线程使用方式,这种方式要自己创建完成端口句柄,自己将SOCKET句柄绑定到完成端口,这里就不在赘述,主要介绍下调用BindIoCompletionCallback函数时,应如何编写这个线程池的回调函数,其实它与前面那个线程函数是很类似的。先来看看回调函数长个什么样子:

VOID CALLBACKFileIOCompletionRoutine(

 [in]                DWORD dwErrorCode,

 [in]                DWORDdwNumberOfBytesTransfered,

 [in]                LPOVERLAPPED lpOverlapped

);

第一个参数就是一个错误码,如果是0恭喜你,操作一切ok,如果有错也不要慌张,前一篇文章中已经介绍了如何翻译和看懂这个错误码。照着做就是了。

第二个参数就是说这次IO操作一共完成了多少字节的数据传输任务,这个字段有个特殊含义,如果你发现一个Recv操作结束了,并且这个参数为0,那么就是说,客户端断开了连接(注意针对的是TCP方式,整个SOCKET池就是为TCP方式设计的)。如果这个情况发生了,在SOCKET池中就该回收这个SOCKET句柄。

第三个参数现在不用多说了,立刻就知道怎么用它了。跟刚才调用GetQueuedCompletionStatus函数得到的指针是一个含义。

下面就来看一个实现这个回调的例子:

VOID CALLBACKMyIOCPThread(DWORD dwErrorCode

,DWORD dwBytesTrans,LPOVERLAPPEDlpOverlapped)

      {//IOCP回调函数

             .....................

             if( NULL == lpOverlapped )

             {//没有真正的完成

                    SleepEx(20,TRUE);//故意置成可警告状态

                    return;

             }

             //找回火车头以及后面的所有东西

             MYOVERLAPPED*  pOL = CONTAINING_RECORD(lpOverlapped

, MYOVERLAPPED, m_ol);

             switch(pOL->m_iOpType)

{

case 0: //AcceptEx结束

{//有链接进来了 SOCKET句柄就是pMyOL->m_skClient

   

}

break;

............................

}

........................

      }//end fun

 

看起来很简单吧?好像少了什么?对了那个该死的循环,这里不用了,因为这个是由线程池回调的一个函数而已,线程的活动状态完全由系统内部控制,只管认为只要有IO操作完成了,此函数就会被调用。这里关注的焦点就完全的放到了完成之后的操作上,而什么线程啊,完成端口句柄啊什么的就都不需要了(甚至可以忘记)。

这里要注意一个问题,正如在《IOCP编程之双节棍》中提到的,这个函数执行时间不要过长,否则会出现掉线啊,连接不进来啊等等奇怪的事情。

另一个要注意的问题就是,这个函数最好套上结构化异常处理,尽可能的多拦截和处理异常,防止系统线程池的线程因为你糟糕的回调函数而壮烈牺牲,如果加入了并发控制,还要注意防止死锁,不然你的服务器会的很难看。

理论上来说,你尽可以把这个函数看做一个与线程池函数等价的函数,只是他要尽可能的(指执行时间)而紧凑(结构清晰少出错)。

最后,回调函数定义好了,就可以调用BindIoCompletionCallback函数,将一个SOCKET句柄丢进完成端口的线程池了:

BindIoCompletionCallback((HANDLE)skClient,MyIOCPThread,0);

注意最后一个参数到目前为止,你就传入0吧。这个函数的神奇就是不见了CreateIoCompletionPort的调用,不见了CreateThread的调用,不见了GetQueuedCompletionStatus等等的调用,省去了n多繁琐且容易出错的步骤,一个函数就全部搞定了。

 

五、服务端调用:

 

以上的所有步骤在完全理解后,最终让我们看看SOCKET池如何实现之。

1、按照传统,要先监听到某个IP的指定端口上:

SOCKADDR_IN   saServer = {0};

//创建监听Socket

SOCKET skServer =::WSASocket(AF_INET, SOCK_STREAM, IPPROTO_IP

, NULL, 0,WSA_FLAG_OVERLAPPED);

//把监听SOCKET扔进线程池,这个可以省略              ::BindIoCompletionCallback((HANDLE)skServer,MyIOCPThread, 0);

//必要时打开SO_REUSEADDR属性,重新绑定到这个监听地址

BOOL  bReuse=TRUE;                ::setsockopt(m_skServer,SOL_SOCKET,SO_REUSEADDR

,(LPCSTR)&bReuse,sizeof(BOOL));

saServer.sin_family = AF_INET;

saServer.sin_addr.s_addr =INADDR_ANY;

// INADDR_ANY这个值的魅力是监听所有本地IP的相同端口

saServer.sin_port =htons(80);      //80得永生

::bind(skServer,(LPSOCKADDR)&saServer,sizeof(SOCKADDR_IN));

//监听,队列长为默认最大连接SOMAXCONN

listen(skServer, SOMAXCONN);

 

2、就是发出一大堆的AcceptEx调用:

for(UINT i = 0; i < 1000;i++)

{//调用1000

//创建与客户端通讯的SOCKET,注意SOCKET的创建方式

skAccept = ::WSASocket(AF_INET,

                                             SOCK_STREAM,

                                             IPPROTO_TCP,

                                             NULL,

                                             0,

                                             WSA_FLAG_OVERLAPPED);

//丢进线程池中

BindIoCompletionCallback((HANDLE)skAccept,MyIOCPThread,0);

//创建一个自定义的OVERLAPPED扩展结构,使用IOCP方式调用

pMyOL= new MYOVERLAPPED;

pMyOL->m_iOpType = 0;       //AcceptEx操作

pMyOL->m_skServer =skServer;

pMyOL->m_skClient =skClient;

BYTE* pBuf = new BYTE[256];//一个缓冲

ZeroMemory(pBuf,256*sizeof(BYTE));

pMyOL->m_pBuf = pBuf;

//发出AcceptEx调用

//注意将AcceptEx函数接收连接数据缓冲的大小设定成了0

//这将导致此函数立即返回,虽然与不设定成0的方式而言,

//这导致了一个较低下的效率,但是这样提高了安全性

//所以这种效率牺牲是必须的

AcceptEx(skServer,skAccept,pBuf,

    0,//将接收缓冲置为0,AcceptEx直接返回,防止拒绝服务攻击

   256,256,NULL,(LPOVERLAPPED)pMyOL);

 

}

这样就有1000AcceptEx在提前等着客户端的连接了,即使1000个并发连接也不怕了,当然如果再BT点那么就放1w个,什么你要放2w个?那就要看看你的这个IP段的端口还够不够了,还有你的系统内存够不够用。一定要注意同一个IP地址上理论上端口最大值是65535,也就是6w多个,这个要合理的分派,如果并发管理超过6w个以上的连接时,怎么办呢?那就再插块网卡租个新的IP,然后再朝那个IP端绑定并监听即可。因为使用了INADDR_ANY,所以一监听就是所有本地IP的相同端口,如果服务器的IP有内外网之分,为了安全和区别起见可以明确指定监听哪个IP,单IP时就要注意本IP空闲端口的数量问题了。

 

3AcceptEx返回后,也就是线程函数中,判定是AcceptEx操作返回后,首先需要的调用就是:

GetAcceptExSockaddrs(pBuf,0,sizeof(sockaddr_in)+ 16,

      sizeof(sockaddr_in) + 16,(LPSOCKADDR*) &addrHost,&lenHost,

      (LPSOCKADDR*) &addrClient,&lenClient);

int nRet =::setsockopt(pOL->m_skClient, SOL_SOCKET,

      SO_UPDATE_ACCEPT_CONTEXT,(char *)&m_skServer,sizeof(m_skServer));

之后就可以WSASend或者WSARecv了。

      4、这些调用完后,就可以在这个m_skClient上收发数据了,如果收发数据结束或者IO错误,那么就回收SOCKET进入SOCKET池:

      DisconnectEx(m_skClient,&pData->m_ol, TF_REUSE_SOCKET, 0);

      5、当DisconnectEx函数完成操作之后,在回调的线程函数中,像下面这样重新让这个SOCKET进入监听状态,等待下一个用户连接进来,至此组建SOCKET池的目的就真正达到了:

//创建一个自定义的OVERLAPPED扩展结构,使用IOCP方式调用

pMyOL= new MYOVERLAPPED;

pMyOL->m_iOpType =0;        //AcceptEx操作

pMyOL->m_skServer =skServer;

pMyOL->m_skClient =skClient;

BYTE* pBuf = new BYTE[256];//一个缓冲

ZeroMemory(pBuf,256*sizeof(BYTE));

pMyOL->m_pBuf = pBuf;

AcceptEx(skServer,skClient,pBuf , 0,256,256,NULL,

   (LPOVERLAPPED)pMyOL);

//注意在这个SOCKET被重新利用后,后面的再次捆绑到完成端口的操作会返回一个已设置//的错误,这个错误直接被忽略即可

        ::BindIoCompletionCallback((HANDLE)skClient,Server_IOCPThread, 0);

      至此服务端的线程池就算搭建完成了,这个SOCKET池也就是围绕AcceptExDisconnectEx展开的,而创建操作就全部都在服务启动的瞬间完成,一次性投递一定数量的SOCKET进入SOCKET池即可,这个数量也就是通常所说的最大并发连接数,你喜欢多少就设置多少吧,如果连接多数量就大些,如果IO操作多,连接断开请求不多就少点,剩下就是调试了。

 

六、客户端调用:

 

1 主要是围绕利用ConnectEx开始调用:

SOCKET skConnect = ::WSASocket(AF_INET,SOCK_STREAM,IPPROTO_IP,

                           NULL,0,WSA_FLAG_OVERLAPPED);

//SOCKET扔进IOCP

BindIoCompletionCallback((HANDLE)skConnect,MyIOCPThread,0);

//本地随便绑个端口

SOCKADDR_IN LocalIP = {};

LocalIP.sin_family = AF_INET;

LocalIP.sin_addr.s_addr =INADDR_ANY;

LocalIP.sin_port = htons((short)0 );    //使用0让系统自动分配

int result=::bind(skConnect,(LPSOCKADDR)&LocalIP,sizeof(SOCKADDR_IN));

pMyOL= new MYOVERLAPPED;

pMyOL->m_iOpType =2;           //ConnectEx操作

pMyOL->m_skServer =NULL;    //没有服务端的SOCKET

pMyOL->m_skClient =skConnect;

ConnectEx(skConnect,(constsockaddr*)pRemoteAddr,sizeof(SOCKADDR_IN),

      NULL,0,NULL,(LPOVERLAPPED)pOL) )

如果高兴就可以把上面的过程放到循环里面去,pRemoteAddr就是远程服务器的IP和端口,你可以重复连接n多个,然后疯狂下载东西(别说我告诉你的哈,人家的服务器宕机了找你负责)。注意那个绑定一定要有,不然调用会失败的。

2 接下来就在线程函数中判定是ConnectEx操作,通过判定m_iOpType== 2就可以知道,然后这样做:

setsockopt( pOL->m_skClient,SOL_SOCKET, SO_UPDATE_CONNECT_CONTEXT,

                    NULL, 0 );

然后就是自由的按照需要调用WSASend或者WSARecv

3 最后使用和服务端相似的逻辑调用DisconnectEx函数,收回SOCKET并直接再次调用ConnectEx连接到另一服务器或相同的同一服务器即可。

至此客户端的SOCKET池也搭建完成了,创建SOCKET的工作也是在一开始的一次性就完成了,后面都是利用ConnectExDisconnectEx函数不断的连接-收发数据-回收-再连接来进行的。客户端的这个SOCKET池可以用于HTTP下载文件的客户端或者FTP下载的服务端(反向服务端)或者客户端,甚至可以用作一个网游的机器人系统,也可以作为一个压力测试的客户端核心的模型。

 

七、总结和提高:

以上就是比较完整的如何具体实现SOCKET池的全部内容,因为篇幅的原因就不贴全部的代码了,我相信各位看客看完之后心中应该有个大概的框架,并且也可以进行实际的代码编写工作了。可以用纯c来实现也可以用C++来实现。但是这里要说明一点就是DisconnectEx函数和ConnectEx函数似乎只能在XP SP2以上和2003Server以上的平台上使用,对于服务端来说这不是什么问题,但是对于客户端来说,使用SOCKET池时还要考虑一个兼容性问题,不得已还是要放弃在客户端使用SOCKET池。

SOCKET池的全部精髓就在于提前创建一批SOCKET,然后就是不断的重复回收再利用,比起传统的非SOCKET池方式,节省了大量的不断创建和销毁SOCKET对象的内核操作,同时借用IOCP函数AcceptExConnectExDisconnectEx等的异步IO完成特性提升了整体性能,非常适合用于一些需要大规模TCP连接管理的场景,如:HTTPServer FTP Server和游戏服务器等。

SOCKET池的本质就是充分的利用了IOCP模型的几乎所有优势,因此要用好SOCKET池就要深入的理解IOCP模型,这是前提。有问题请跟帖讨论。

 

 

WinSock2编程之打造完整的SOCKET

 

Winodows平台上,网络编程的主要接口就是WinSock,目前大多数的Windows平台上的WinSock平台已经升级到2.0版,简称为WinSock2。在WinSock2中扩展了很多很有用的Windows味很浓的SOCKET专用API,为Windows平台用户提供高性能的网络编程支持。这些函数中的大多数已经不再是标准的“Berkeley”套接字模型的API了。使用这些函数的代价就是你不能再将你的网络程序轻松的移植到尤里平台(我给Unix+Linux平台的简称)下,反过来因为Windows平台支持标准的“Berkeley”套接字模型,所以你可以将大多数尤里平台下的网络应用移植到Windows平台下。

如果不考虑可移植性(或者所谓的跨平台性),而是着重于应用的性能时,尤其是注重服务器性能时,对于Windows的程序,都鼓励使用WinSock2扩展的一些API,更鼓励使用IOCP模型,因为这个模型是目前Windows平台上比较完美的一个高性能IO编程模型,它不但适用于SOCKET编程,还适用于读写硬盘文件,读写和管理命名管道、邮槽等等。如果再结合Windows线程池,IOCP几乎可以利用当今硬件所有可能的新特性(比如多核,DMA,高速总线等等),本身具有先天的扩展性和可用性。

今天讨论的重点就是SOCKET池。很多VC程序员也许对SOCKET池很陌生,也有些可能很熟悉,那么这里就先讨论下这个概念。

Windows平台上SOCKET实际上被视作一个内核对象的句柄,很多WindowsAPI在支持传统的HANDLE参数的同时也支持SOCKET,比如有名的CreateIoCompletionPort就支持将SOCKET句柄代替HANDLE参数传入并调用。熟悉Windows内核原理的读者,立刻就会发现,这样的话,我们创建和销毁一个SOCKET句柄,实际就是在系统内部创建了一个内核对象,对于Windows来说这牵扯到从Ring3层到Ring0层的耗时操作,再加上复杂的安全审核机制,实际创建和销毁一个SOCKET内核对象的成本还是蛮高的。尤其对于一些面向连接的SOCKET应用,服务端往往要管理n多个代表客户端通信的SOCKET对象,而且因为客户的变动性,主要面临的大量操作除了一般的收发数据,剩下的就是不断创建和销毁SOCKET句柄,对于一个频繁接入和断开的服务器应用来说,创建和销毁SOCKET的性能代价立刻就会体现出来,典型的例如WEB服务器程序,就是一个需要频繁创建和销毁SOCKET句柄的SOCKET应用。这种情况下我们通常都希望对于断开的SOCKET对象,不是简单的销毁了之(很多时候断开的含义不一定就等价于销毁,可以仔细思考一下),更多时候希望能够重用这个SOCKET对象,这样我们甚至可以事先创建一批SOCKET对象组成一个,在需要的时候重用其中的SOCKET对象,不需要的时候将SOCKET对象重新丢入池中即可,这样就省去了频繁创建销毁SOCKET对象的性能损失。在原始的“Berkeley”套接字模型中,想做到这点是没有什么办法的。而幸运的是在Windows平台上,尤其是支持WinSock2的平台上,已经提供了一套完整的API接口用于支持SOCKET池。

对于符合以上要求的SOCKET池,首先需要做到的就是对SOCKET句柄的回收,因为创建函数无论在那个平台上都是现成的,而最早能够实现这个功能的WinSock函数就是TransmitFile,如果代替closesocket函数像下面这样调用就可以回收一个SOCKET句柄,而不是销毁:(注意回收这个功能对于TransmitFile函数来说只是个副业。)

TransmitFile(hSocket,NULL,0,0,NULL,NULL,TF_DISCONNECT| TF_REUSE_SOCKET );

注意上面函数的最后一个参数,使用了标志TF_DISCONNECTTF_REUSE_SOCKET,第一个值表示断开,第二个值则明确的表示重用实际上也就是回收这个SOCKET,经过这个处理的SOCKET句柄,就可以直接再用于connect等操作,但是此时我们会发现,这个回收来的SOCKET似乎没什么用,因为其他套接字函数没法直接利用这个回收来的SOCKET句柄。

这时就要WinSock2的一组专用API上场了。我将它们按传统意义上的服务端和客户端分为两组:

一、        服务端:

SOCKET WSASocket(

 __in          int af,

 __in          int type,

 __in          int protocol,

 __in          LPWSAPROTOCOL_INFOlpProtocolInfo,

  __in         GROUP g,

 __in          DWORD dwFlags

);

BOOLAcceptEx(

 __in          SOCKETsListenSocket,

 __in          SOCKETsAcceptSocket,

 __in          PVOIDlpOutputBuffer,

 __in          DWORDdwReceiveDataLength,

 __in          DWORD dwLocalAddressLength,

 __in          DWORDdwRemoteAddressLength,

 __out         LPDWORDlpdwBytesReceived,

 __in          LPOVERLAPPEDlpOverlapped

);

BOOLDisconnectEx(

 __in          SOCKET hSocket,

 __in          LPOVERLAPPEDlpOverlapped,

  __in         DWORD dwFlags,

 __in          DWORD reserved

);

二、        客户端:

SOCKET WSASocket(

 __in          int af,

 __in          int type,

 __in          int protocol,

 __in          LPWSAPROTOCOL_INFOlpProtocolInfo,

 __in          GROUP g,

  __in         DWORD dwFlags

);

BOOLPASCAL ConnectEx(

 __in          SOCKET s,

 __in          const structsockaddr* name,

 __in          int namelen,

 __in_opt      PVOID lpSendBuffer,

 __in          DWORDdwSendDataLength,

 __out         LPDWORD lpdwBytesSent,

 __in          LPOVERLAPPEDlpOverlapped

);

BOOLDisconnectEx(

 __in          SOCKET hSocket,

 __in          LPOVERLAPPEDlpOverlapped,

 __in          DWORD dwFlags,

 __in          DWORD reserved

);

注意观察这些函数,似乎和传统的“Berkeley”套接字模型中的一些函数大同小异,其实仔细观察他们的参数,就已经可以发现一些调用他们的玄机了。

首先我们来看AcceptEx函数,与accept函数不同,它需要两个SOCKET句柄作为参数,头一个参数的含义与accept函数的相同,而第二个参数的意思就是accept函数返回的那个代表与客户端通信的SOCKET句柄,在传统的accept内部,实际在返回那个代表客户端的SOCKET时,是在内部调用了一个SOCKET的创建动作,先创建这个SOCKET然后再“accept”让它变成代表客户端连接的SOCKET,而AcceptEx函数就在这里扩展(实际上是阉割才对)accept函数,省去了内部那个明显的创建SOCKET的动作,而将这个创建动作交给最终的调用者自己来实现。AcceptEx要求调用者创建好那个sAcceptSocket句柄然后传进去,这时我们立刻发现,我们回收的那个SOCKET是不是也可以传入呢?答案是肯定的,我们就是可以利用这个函数传入那个回收来的SOCKET句柄,最终实现服务端的SOCKET重用。

这里需要注意的就是,AcceptEx函数必须工作在非阻塞的IOCP模型下,同时即使AcceptEx函数返回了,也不代表客户端连接进来或者连接成功了,我们必须依靠它的完成通知才能知道这个事实,这也是AcceptEx函数区别于accept这个阻塞方式函数的最大之处。通常可以利用AcceptEx的非阻塞特性和IOCP模型的优点,一次可以预先发出成千上万个AcceptEx调用,等待客户端的连接。对于习惯了accept阻塞方式的程序员来说,理解AcceptEx的工作方式还是需要费一些周折的。下面的例子就演示了如何一次调用多个AcceptEx

//批量创建SOCKET,并调用对应的AcceptEx

for(UINT i = 0; i < 1000;i++)

{//调用1000

//创建与客户端通讯的SOCKET,注意SOCKET的创建方式

skAccept = ::WSASocket(AF_INET,

                  SOCK_STREAM,

                  IPPROTO_TCP,

                  NULL,

                  0,

                  WSA_FLAG_OVERLAPPED);

if (INVALID_SOCKET == skAccept)

{

    throwCGRSException((DWORD)WSAGetLastError());

}

//创建一个自定义的OVERLAPPED扩展结构,使用IOCP方式调用

pAcceptOL = newCGRSOverlappedData(GRS_OP_ACCEPT

,this,skAccept,NULL);

pAddrBuf =pAcceptOL->GetAddrBuf();

//4、发出AcceptEx调用

//注意将AcceptEx函数接收连接数据缓冲的大小设定成了0,这将导致此函数立即返回,虽然与

//不设定成0的方式而言,这导致了一个较低下的效率,但是这样提高了安全性,所以这种效率

//牺牲是必须的

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

    && WSAECONNRESET != iError )

{

    if(INVALID_SOCKET != skAccept)

     {

        ::closesocket(skAccept);

        skAccept = INVALID_SOCKET;

     }

     if( NULL !=pAcceptOL)

     {

            GRS_ISVALID(pAcceptOL,sizeof(CGRSOverlappedData));

delete pAcceptOL;

    pAcceptOL = NULL;

     }

  }

}

}

以上的例子只是简单的演示了AcceptEx的调用,还没有涉及到真正的回收重用这个主题,那么下面的例子就演示了如何重用一个SOCKET句柄:

if(INVALID_SOCKET == skClient)

{

throwCGRSException(_T("SOCKET句柄是无效的!"));

}

OnPreDisconnected(skClient,pUseData,0);

CGRSOverlappedData*pData

= newGRSOverlappedData(GRS_OP_DISCONNECTEX

,this,skClient,pUseData);

//回收而不是关闭后再创建大大提高了服务器的性能

DisconnectEx(skClient,&pData->m_ol,TF_REUSE_SOCKET,0); 

......

     //在接收到DisconnectEx函数的完成通知之后,我们就可以重用这个SOCKET

CGRSAddrbuf*pBuf = NULL;

pNewOL = newCGRSOverlappedData(GRS_OP_ACCEPT

,this,skClient,pUseData);

pBuf = pNewOL->GetAddrBuf();

//把这个回收的SOCKET重新丢进连接池

if(!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

       && WSAECONNRESET != iError )

    {

       throw CGRSException((DWORD)iError);

     }

}

//注意在这个SOCKET被重新利用后,重新与IOCP绑定一下,该操作会返回一个已设置的错误,这个错误直接被忽略即可

::BindIoCompletionCallback((HANDLE)skClient

,Server_IOCPThread, 0);

 

至此回收重用SOCKET的工作也就结束了,以上的过程实际理解了IOCP之后就比较好理解了,例子的最后我们使用了BindIoCompletionCallback函数重新将SOCKET丢进了IOCP线程池中,实际还可以再次使用CreateIoCompletionPort函数达到同样的效果,这里列出这一步就是告诉大家,不要忘了再次绑定一下完成端口和SOCKET

    对于客户端来说,可以使用ConnectEx函数来代替connect函数,与AcceptEx函数相同,ConnectEx函数也是以非阻塞的IOCP方式工作的,唯一要注意的就是在WSASocket调用之后,在ConnectEx之前要调用一下bind函数,将SOCKET提前绑定到一个本地地址端口上,当然回收重用之后,就无需再次绑定了,这也是ConnectEx较之connect函数高效的地方之一。

   AcceptEx函数类似,也可以一次发出成千上万个ConnectEx函数的调用,可以连接到不同的服务器,也可以连接到相同的服务器,连接到不同的服务器时,只需提供不同的sockaddr即可。

    通过上面的例子和讲解,大家应该对SOCKET池概念以及实际的应用有个大概的了解了,当然核心仍然是理解了IOCP模型,否则还是寸步难行。

在上面的例子中,回收SOCKET句柄主要使用了DisconnectEx函数,而不是之前介绍的TransmitFile函数,为什么呢?因为TransmitFile函数在一些情况下会造成死锁,无法正常回收SOCKET,毕竟不是专业的回收重用SOCKET函数,我就遇到过好几次死锁,最后偶然的发现了DisconnectEx函数这个专用的回收函数,调用之后发现比TransmitFile专业多了,而且不管怎样都不会死锁。

最后需要补充的就是这几个函数的调用方式,不能像传统的SOCKETAPI那样直接调用它们,而需要使用一种间接的方式来调用,尤其是AcceptExDisconnectEx函数,下面给出了一个例子类,用于演示如何动态载入这些函数并调用之:

class CGRSMsSockFun

{

public:

CGRSMsSockFun(SOCKET skTemp =INVALID_SOCKET)

{

     if(INVALID_SOCKET != skTemp )

     {

      LoadAllFun(skTemp);

     }

}

public:

virtual ~CGRSMsSockFun(void)

{

}

protected:

BOOL LoadWSAFun(SOCKET&skTemp,GUID&funGuid,void*&pFun)

{

     DWORDdwBytes = 0;

     BOOLbRet = TRUE;

     pFun =NULL;

     BOOLbCreateSocket = FALSE;

     try

     {

      if(INVALID_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,

               &funGuid,sizeof(funGuid),

               &pFun,sizeof(pFun),&dwBytes,NULL,

               NULL))

      {

            pFun = NULL;

            throw CGRSException((DWORD)WSAGetLastError());

      }

  }

  catch(CGRSException&e)

  {

     if(bCreateSocket)

     {

       ::closesocket(skTemp);

     }

  }

  return NULL != pFun;

}

protected:

LPFN_ACCEPTEX m_pfnAcceptEx;

LPFN_CONNECTEX m_pfnConnectEx;

LPFN_DISCONNECTEXm_pfnDisconnectEx;

LPFN_GETACCEPTEXSOCKADDRSm_pfnGetAcceptExSockaddrs;

LPFN_TRANSMITFILEm_pfnTransmitfile;

LPFN_TRANSMITPACKETSm_pfnTransmitPackets;

LPFN_WSARECVMSGm_pfnWSARecvMsg;

protected:

BOOL LoadAcceptExFun(SOCKET&skTemp)

{

     GUIDGuidAcceptEx = WSAID_ACCEPTEX;

     returnLoadWSAFun(skTemp,GuidAcceptEx

,(void*&)m_pfnAcceptEx);

}

BOOL LoadConnectExFun(SOCKET&skTemp)

{

     GUID GuidAcceptEx= WSAID_CONNECTEX;

     returnLoadWSAFun(skTemp,GuidAcceptEx

,(void*&)m_pfnConnectEx);

}

BOOLLoadDisconnectExFun(SOCKET&skTemp)

{

     GUIDGuidDisconnectEx = WSAID_DISCONNECTEX;

     returnLoadWSAFun(skTemp,GuidDisconnectEx

,(void*&)m_pfnDisconnectEx);

}

BOOLLoadGetAcceptExSockaddrsFun(SOCKET &skTemp)

{

    GUID GuidGetAcceptExSockaddrs

= WSAID_GETACCEPTEXSOCKADDRS;

     returnLoadWSAFun(skTemp,GuidGetAcceptExSockaddrs

,(void*&)m_pfnGetAcceptExSockaddrs);

}

BOOL LoadTransmitFileFun(SOCKET&skTemp)

{

     GUIDGuidTransmitFile = WSAID_TRANSMITFILE;

     returnLoadWSAFun(skTemp,GuidTransmitFile

,(void*&)m_pfnTransmitfile);

}

BOOLLoadTransmitPacketsFun(SOCKET&skTemp)

{

     GUIDGuidTransmitPackets = WSAID_TRANSMITPACKETS;

     returnLoadWSAFun(skTemp,GuidTransmitPackets

,(void*&)m_pfnTransmitPackets);

}

BOOLLoadWSARecvMsgFun(SOCKET&skTemp)

{

     GUIDGuidTransmitPackets = WSAID_TRANSMITPACKETS;

     returnLoadWSAFun(skTemp,GuidTransmitPackets

,(void*&)m_pfnWSARecvMsg);

}

public:

BOOL LoadAllFun(SOCKET skTemp)

{//注意这个地方的调用顺序,是根据服务器的需要,并结合了表达式副作用

  //而特意安排的调用顺序

  return(LoadAcceptExFun(skTemp) &&

            LoadGetAcceptExSockaddrsFun(skTemp) &&

            LoadTransmitFileFun(skTemp) &&

            LoadTransmitPacketsFun(skTemp) &&

            LoadDisconnectExFun(skTemp) &&

            LoadConnectExFun(skTemp) &&

            LoadWSARecvMsgFun(skTemp));

}

 

public:

GRS_FORCEINLINE BOOL AcceptEx (

         SOCKET sListenSocket,

         SOCKET sAcceptSocket,

         PVOID lpOutputBuffer,

         DWORD dwReceiveDataLength,

         DWORD dwLocalAddressLength,

         DWORD dwRemoteAddressLength,

         LPDWORD lpdwBytesReceived,

         LPOVERLAPPED lpOverlapped

         )

{

    GRS_ASSERT(NULL != m_pfnAcceptEx);

     returnm_pfnAcceptEx(sListenSocket,

            sAcceptSocket,lpOutputBuffer,

            dwReceiveDataLength,dwLocalAddressLength,

           dwRemoteAddressLength,lpdwBytesReceived,

            lpOverlapped);

}

GRS_FORCEINLINEBOOL ConnectEx(

         SOCKET s,const struct sockaddr FAR *name,

         int namelen,PVOID lpSendBuffer,

         DWORD dwSendDataLength,LPDWORD lpdwBytesSent,

         LPOVERLAPPED lpOverlapped

         )

{

    GRS_ASSERT(NULL != m_pfnConnectEx);

     returnm_pfnConnectEx(

            s,name,namelen,lpSendBuffer,

            dwSendDataLength,lpdwBytesSent,

            lpOverlapped

            );

}

GRS_FORCEINLINEBOOL DisconnectEx(

         SOCKET s,LPOVERLAPPED lpOverlapped,

         DWORD  dwFlags,DWORD  dwReserved

         )

{

    GRS_ASSERT(NULL != m_pfnDisconnectEx);

     returnm_pfnDisconnectEx(s,

            lpOverlapped,dwFlags,dwReserved);

}

GRS_FORCEINLINEVOID GetAcceptExSockaddrs (

         PVOID lpOutputBuffer,

         DWORD dwReceiveDataLength,

         DWORD dwLocalAddressLength,

         DWORD dwRemoteAddressLength,

         sockaddr **LocalSockaddr,

         LPINT LocalSockaddrLength,

         sockaddr **RemoteSockaddr,

         LPINT RemoteSockaddrLength

         )

{

     GRS_ASSERT(NULL!= m_pfnGetAcceptExSockaddrs);

     returnm_pfnGetAcceptExSockaddrs(

         lpOutputBuffer,dwReceiveDataLength,

         dwLocalAddressLength,dwRemoteAddressLength,

         LocalSockaddr,LocalSockaddrLength,

         RemoteSockaddr,RemoteSockaddrLength

         );

}

GRS_FORCEINLINEBOOL TransmitFile(

     SOCKEThSocket,HANDLE hFile,

     DWORDnNumberOfBytesToWrite,

     DWORDnNumberOfBytesPerSend,

    LPOVERLAPPED lpOverlapped,

    LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers,

     DWORDdwReserved

     )

{

    GRS_ASSERT(NULL != m_pfnTransmitfile);

     returnm_pfnTransmitfile(

            hSocket,hFile,nNumberOfBytesToWrite,

            nNumberOfBytesPerSend,lpOverlapped,

            lpTransmitBuffers,dwReserved

            );

}

GRS_FORCEINLINEBOOL TransmitPackets(

     SOCKEThSocket,                            

    LPTRANSMIT_PACKETS_ELEMENTlpPacketArray,                              

     DWORDnElementCount,DWORDnSendSize,               

    LPOVERLAPPED lpOverlapped,DWORDdwFlags                              

     )

{

    GRS_ASSERT(NULL != m_pfnTransmitPackets);

     returnm_pfnTransmitPackets(

            hSocket,lpPacketArray,nElementCount,

nSendSize,lpOverlapped,dwFlags

            );

}

GRS_FORCEINLINEINT WSARecvMsg(

         SOCKET s,LPWSAMSG lpMsg,

         LPDWORD lpdwNumberOfBytesRecvd,

         LPWSAOVERLAPPED lpOverlapped,

         LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine

         )

{

    GRS_ASSERT(NULL != m_pfnWSARecvMsg);

     returnm_pfnWSARecvMsg(

            s,lpMsg,lpdwNumberOfBytesRecvd,

            lpOverlapped,lpCompletionRoutine

            );

}

/*WSAID_ACCEPTEX

 WSAID_CONNECTEX

 WSAID_DISCONNECTEX

 WSAID_GETACCEPTEXSOCKADDRS

  WSAID_TRANSMITFILE

  WSAID_TRANSMITPACKETS

  WSAID_WSARECVMSG

  WSAID_WSASENDMSG */

};

这个类的使用非常简单,只需要声明一个类的对象,然后调用其成员AcceptExDisconnectEx函数等即可,参数与这些函数的MSDN声明方式完全相同,除了本文中介绍的这些函数外,这个类还包含了很多其他的Winsock2函数,那么都应该按照这个类中演示的这样来动态载入后再行调用,如果无法载入通常说明你的环境中没有Winsock2函数库,或者是你初始化的不是2.0版的Winsock环境。

这个类是本人完整类库的一部分,如要使用需要自行修改一些地方,如果不知如何修改或遇到什么问题,可以直接跟帖说明,我会不定期回答大家的问题,这个类可以免费使用、分发、修改,可以用于任何商业目的,但是对于使用后引起的任何问题,本人概不负责,有问题请跟帖。关于AcceptEx以及其他一些函数,包括本文中没有介绍到得函数,我会在后续的一些专题文章中进行详细深入的介绍,敬请期待。如果你有什么疑问,或者想要了解什么也请跟帖说明,我会在后面的文章中尽量说明。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值