zoukankan      html  css  js  c++  java
  • Windows 套接字I/O模型

      

    一、套接字模型
    Winsock以两种模式执行I/O操作:阻塞和非阻塞。
    在阻塞模式下,执行 I/O 的 Winsock 调用(如 send 和 recv)
    直到操作完成才返回。在非阻塞模式下,Winsock 函数会立即返回。

    1)阻塞模式
    套接字创建时,默认工作在阻塞模式下。例如,对 recv 函数的调用会使程序进入等待状
    态,直到接收到数据才返回。
    阻塞套接字的好处是使用简单,但是当需要处理多个套接字连接时,就必须创建多个线
    程,即典型的一个连接使用一个线程的问题,这给编程带来了许多不便。所以实际开发中使
    用最多的还是下面要讲述的非阻塞模式。

    2)非阻塞模式
    非阻塞套接字使用起来比较复杂,但是却有许多优点。应用程序可以调用 ioctlsocket 函
    数显式地让套接字工作在非阻塞模式下,如下代码所示。
    u_long ul = 1;
    SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
    ioctlsocket(s, FIONBIO, (u_long *)&ul);
    一旦套接字被置于非阻塞模式,处理发送和接收数据或者管理连接的 Winsock 调用将会
    立即返回。

    二、选择模型
    select 模型是一个广泛在 Winsock 中使用的 I/O 模型。称它为 select 模型,是因为它主要是使用 select 函数来管理 I/O 的。这个模式的设计源于 UNIX 系统,目的是允许那些想要避免在套接字调用上阻塞的应用程序有能力管理多个套接字


    select 函数可以确定一个或者多个套接字的状态。如果套接字上没有网络事件发生,便
    进入等待状态,以便执行同步 I/O。函数定义如下。
    int select(
      int nfds, // 忽略,仅是为了与Berkeley 套接字兼容
      fd_set* readfds, // 指向一个套接字集合,用来检查其可读性
      fd_set* writefds, // 指向一个套接字集合,用来检查其可写性
      fd_set* exceptfds, // 指向一个套接字集合,用来检查错误
      const struct timeval* timeout // 指定此函数等待的最长时间,如果为NULL,则最长时间为无限大
    );
    1.套接字集合
    fd_set 结构可以把多个套接字连在一起,形成一个套接字集合。select 函数可以测试这个
    集合中哪些套接字有事件发生。下面是这个结构在 WINSOCK2.h 中的定义。
    typedef struct fd_set {
      u_int fd_count; // 下面数组的大小
      SOCKET fd_array[FD_SETSIZE]; // 套接字句柄数组
    } fd_set;
    下面是 WINSOCK 定义的 4 个操作 fd_set 套接字集合的宏。
    FD_ZERO(*set) 初始化 set 为空集合。集合在使用前应该总是清空
    FD_CLR(s, *set) 从 set 移除套接字 s
    FD_ISSET(s, *set) 检查 s 是不是 set 的成员,如果是返回 TRUE
    FD_SET(s, *set) 添加套接字到集合

     1 CInitSock theSock;  // 初始化Winsock库
     2 int main()
     3 {
     4     USHORT nPort = 4567; // 此服务器监听的端口号
     5     // 创建监听套接字
     6     SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 
     7     sockaddr_in sin;
     8     sin.sin_family = AF_INET;
     9     sin.sin_port = htons(nPort);
    10     sin.sin_addr.S_un.S_addr = INADDR_ANY;
    11     // 绑定套接字到本地机器
    12     if(::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
    13     {  
    14         printf(" Failed bind() 
    ");
    15         return -1;
    16     }
    17     // 进入监听模式
    18     ::listen(sListen, 5);
    19     // select 模型处理过程
    20     // 1)初始化一个套接字集合fdSocket,添加监听套接字句柄到这个集合
    21     
    22     fd_set fdSocket; // 所有可用套接字集合
    23     FD_ZERO(&fdSocket);
    24     FD_SET(sListen, &fdSocket);
    25     while(TRUE)
    26     {  
    27         // 2)将fdSocket集合的一个拷贝fdRead传递给select 函数,
    28         // 当有事件发生时,select 函数移除fdRead集合中没有未决I/O操作的套接字句柄,然后返回。
    29         fd_set fdRead = fdSocket;
    30         int nRet = ::select(0, &fdRead, NULL, NULL, NULL);
    31         if(nRet > 0)
    32         {  
    33 
    34         // 3)通过将原来fdSocket 集合与select 处理过的fdRead集合比较,
    35         // 确定都有哪些套接字有未决I/O,并进一步处理这些I/O。
    36         
    37             for(int i=0; i<(int)fdSocket.fd_count; i++)
    38             {  
    39                 if(FD_ISSET(fdSocket.fd_array[i], &fdRead))
    40                 {  
    41                     if(fdSocket.fd_array[i] == sListen)  // (1)监听套接字接收到新连接
    42                     {  
    43                         if(fdSocket.fd_count < FD_SETSIZE)
    44                         {  
    45                             sockaddr_in addrRemote;
    46                             int nAddrLen = sizeof(addrRemote);
    47                             SOCKET sNew =
    48                             ::accept(sListen, (SOCKADDR*)&addrRemote, &nAddrLen);
    49                             FD_SET(sNew, &fdSocket);
    50                             printf("接收到连接(%s)
    ", ::inet_ntoa(addrRemote.sin_addr));
    51                         }
    52                         else
    53                         {  
    54                             printf(" Too much connections! 
    ");
    55                             continue;
    56                         }
    57                     }
    58                     else
    59                     {  
    60                         char szText[256];
    61                         int nRecv = ::recv(fdSocket.fd_array[i], szText, strlen(szText), 0);
    62                         if(nRecv > 0)  // (2)可读
    63                         {  
    64                             szText[nRecv] = '';
    65                             printf("接收到数据:%s 
    ", szText);
    66                         }
    67                         else // (3)连接关闭、重启或者中断
    68                         {  
    69                             ::closesocket(fdSocket.fd_array[i]);
    70                             FD_CLR(fdSocket.fd_array[i], &fdSocket);
    71                         }
    72                     }
    73                 }
    74             }
    75         }
    76         else
    77         {  
    78             printf(" Failed select() 
    ");
    79             break;
    80         }
    81     }
    82     return 0;
    83 }
    Select模型

    使用 select 的好处是程序能够在单个线程内同时处理多个套接字连接,这避免了阻塞模式下的线程膨胀问题。但是,添加到 fd_set 结构的套接字数量是有限制的,默认情况下,最
    大值是 FD_SETSIZE,它在 winsock2.h 文件中定义为 64。为了增加套接字数量,应用程序可以将 FD_SETSIZE 定义为更大的值(这个定义必须在包含 winsock2.h 之前出现)。不过,自
    定义的值也不能超过 Winsock 下层提供者的限制(通常是 1024)。
    另外,FD_SETSIZE 值太大的话,服务器性能就会受到影响。例如有 1000 个套接字,那么在调用 select 之前就不得不设置这 1000 个套接字,select 返回之后,又必须检查这 1000 个
    套接字。

    三、WSAAsyncSelect 模型

      WSAAsyncSelect 模型允许应用程序以 Windows 消息的形式接收网络事件通知。这个模型是为了适应 Windows 的消息驱动环境而设置的,现在许多对性能要求不高的网络应用程序

    都采用 WSAAsyncSelect 模型,MFC(Microsoft Foundation Class,Microsoft 基础类库)中的CSocket 类也使用了它。

      

      1 // 为了使用WSAAyncSelect I/O模型,程序创建了一个隐藏的窗口,窗口函数是WindowProc
      2 LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
      3 int main()
      4 {
      5     char szClassName[] = "MainWClass"; 
      6     WNDCLASSEX wndclass;
      7     // 用描述主窗口的参数填充WNDCLASSEX结构
      8     wndclass.cbSize = sizeof(wndclass);
      9     wndclass.style = CS_HREDRAW|CS_VREDRAW; 
     10     wndclass.lpfnWndProc = WindowProc; 
     11     wndclass.cbClsExtra = 0; 
     12     wndclass.cbWndExtra = 0; 
     13     wndclass.hInstance = NULL;
     14     wndclass.hIcon = ::LoadIcon(NULL, IDI_APPLICATION);
     15     wndclass.hCursor = ::LoadCursor(NULL, IDC_ARROW); 
     16     wndclass.hbrBackground = (HBRUSH)::GetStockObject(WHITE_BRUSH); 
     17     wndclass.lpszMenuName = NULL;
     18     wndclass.lpszClassName = szClassName ;
     19     wndclass.hIconSm = NULL;
     20     ::RegisterClassEx(&wndclass);
     21     // 创建主窗口
     22     HWND hWnd = ::CreateWindowEx(
     23                                 0, 
     24                                 szClassName, 
     25                                 "", 
     26                                 WS_OVERLAPPEDWINDOW, 
     27                                 Windows 网络与通信程序设计
     2838 29                                 CW_USEDEFAULT, 
     30                                 CW_USEDEFAULT, 
     31                                 CW_USEDEFAULT, 
     32                                 CW_USEDEFAULT, 
     33                                 NULL, 
     34                                 NULL, 
     35                                 NULL, 
     36                                 NULL); 
     37     if(hWnd == NULL)
     38     {
     39         ::MessageBox(NULL, "创建窗口出错!", "error", MB_OK);
     40         return -1;
     41     }
     42     USHORT nPort = 4567; // 此服务器监听的端口号
     43     // 创建监听套接字
     44     SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 
     45     sockaddr_in sin;
     46     sin.sin_family = AF_INET;
     47     sin.sin_port = htons(nPort);
     48     sin.sin_addr.S_un.S_addr = INADDR_ANY;
     49     // 绑定套接字到本地机器
     50     if(::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
     51     {
     52         printf(" Failed bind() 
    ");
     53         return -1;
     54     }
     55     // 将套接字设为窗口通知消息类型。
     56     ::WSAAsyncSelect(sListen, hWnd, WM_SOCKET, FD_ACCEPT|FD_CLOSE);
     57     ::listen(sListen, 5); // 进入监听模式
     58     // 从消息队列中取出消息
     59     MSG msg;
     60     while(::GetMessage(&msg, NULL, 0, 0))
     61     {  
     62         ::TranslateMessage(&msg); // 转化键盘消息
     63         ::DispatchMessage(&msg); // 将消息发送到相应的窗口函数
     64     }
     65     return msg.wParam; // 当GetMessage 返回0时程序结束
     66 }
     67 LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
     68 {
     69     switch (uMsg)
     70     { 
     71         case WM_SOCKET:
     72         {      
     73             SOCKET s = wParam; // 取得有事件发生的套接字句柄
     74             // 查看是否出错
     75             if(WSAGETSELECTERROR(lParam))
     76             {  
     77                 ::closesocket(s);
     78                 return 0;
     79             }
     80             // 处理发生的事件
     81             switch(WSAGETSELECTEVENT(lParam))
     82             {
     83                 case FD_ACCEPT:  // 监听中的套接字检测到有连接进入
     84                 {  
     85                     SOCKET client = ::accept(s, NULL, NULL);
     86                     ::WSAAsyncSelect(client,
     87                     hWnd, WM_SOCKET, FD_READ|FD_WRITE|FD_CLOSE);
     88                 }
     89                 break;
     90                 case FD_WRITE:
     91                 {  }
     92                 break;
     93                 case FD_READ:
     94                 {  
     95                     char szText[1024] = { 0 };
     96                     if(::recv(s, szText, 1024, 0) == -1)  ::closesocket(s);
     97                     else printf("接收数据:%s", szText);
     98                 }
     99                 break;
    100                 case FD_CLOSE:
    101                 { 
    102                     ::closesocket(s);  
    103                 }
    104                 break;
    105             }
    106         }
    107     return 0;
    108     case WM_DESTROY:
    109     ::PostQuitMessage(0) ;
    110     return 0 ;
    111     }
    112     // 将我们不处理的消息交给系统做默认处理
    113 
    114     return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
    115 }
    WSAAsyncSelect 

      

    网络事件消息抵达消息处理函数后,应用程序首先检查 lParam 参数的高位,以判断是否在套接字上发生了网络错误。宏 WSAGETSELECTERROR 返回高字节包含的错误信息。若
    应用程序发现套接字上没有产生任何错误便可用宏 WSAGETSELECTEVENT 读取 lParam 参数的低字位确定发生的网络事件。
    WSAAsyncSelect 模型最突出的特点是与 Windows 的消息驱动机制融在了一起,这使得开发带 GUI 界面的网络程序变得很简单。但是如果连接增加,单个 Windows 函数处理上千
    个客户请求时,服务器性能势必会受到影响。

    四、WSAEventSelect 模型

      

    Winsock 提供了另一种有用的异步事件通知 I/O 模型——WSAEventSelect 模型。这个模型与 WSAAsyncSelect 模型类似,允许应用程序在一个或者多个套接字上接收基于事件的网
    络通知。它与 WSAAsyncSelect 模型类似是因为它也接收 FD_XXX 类型的网络事件,不过并不是依靠 Windows 的消息驱动机制,而是经由事件对象句柄通知。

    WSAEventSelect 模型简单易用,也不需要窗口环境。该模型惟一的缺点是有最多等待 64
    个事件对象的限制,当套接字连接数量增加时,就必须创建多个线程来处理 I/O,也就是使
    用所谓的线程池

    五、重叠(Overlapped)I/O 模型  

      与介绍过的其他模型相比,重叠 I/O 模型提供了更好的系统性能。这个模型的基本设计思想是允许应用程序使用重叠数据结构一次投递一个或者多个异步 I/O 请求(即所谓的重叠
    I/O)。提交的 I/O 请求完成之后,与之关联的重叠数据结构中的事件对象受信,应用程序便可使用 WSAGetOverlappedResult 函数获取重叠操作结果。这和使用重叠结构调用 ReadFile
    和 WriteFile 函数操作文件类似。

      

    1.      可以运行在支持Winsock2的所有Windows平台 ,而不像完成端口只是支持NT系统。

    2.      比起阻塞、select、WSAAsyncSelect以及WSAEventSelect等模型,重叠I/O(Overlapped I/O)模型使应用程序能达到更佳的系统性能。

             因为它和这4种模型不同的是,使用重叠模型的应用程序通知缓冲区收发系统直接使用数据,也就是说,如果应用程序投递了一个10KB大小的缓冲区来接收数据,且数据已经到达套接字,则该数据将直接被拷贝到投递的缓冲区。

    而这4种模型种,数据到达并拷贝到单套接字接收缓冲区中,此时应用程序会被告知可以读入的容量。当应用程序调用接收函数之后,数据才从单套接字缓冲区拷贝到应用程序的缓冲区,差别就体现出来了。

    3.      非常好的性能,已经直逼完成端口了

    二. 重叠模型的基本原理

          概括一点说,重叠模型是让应用程序使用重叠数据结构(WSAOVERLAPPED),一次投递一个或多个Winsock I/O请求。针对这些提交的请求,在它们完成之后,应用程序会收到通知,于是就可以通过自己另外的代码来处理这些数据了。

          需要注意的是,有两个方法可以用来管理重叠IO请求的完成情况(就是说接到重叠操作完成的通知):

    1.      事件对象通知(event object notification)

    2.      完成例程(completion routines) ,注意,这里并不是完成端口

    既然要使用重叠结构,我们常用的send, sendto, recv, recvfrom也都要被WSASend, WSASendto, WSARecv, WSARecvFrom替换掉了, 它们的用法我后面会讲到,这里只需要注意一点,它们的参数中都有一个Overlapped参数,我们可以假设是把我们的WSARecv这样的操作操作“绑定”到这个重叠结构上,提交一个请求,其他的事情就交给重叠结构去操心,而其中重叠结构又要与Windows的事件对象“绑定”在一起,这样我们调用完WSARecv以后就可以“坐享其成”,等到重叠操作完成以后,自然会有与之对应的事件来通知我们操作完成,然后我们就可以来根据重叠操作的结果取得我们想要得的数据了。

    三. 关于重叠模型的基础知识

    1.      WSAOVERLAPPED结构

    复制代码
    typedef struct _WSAOVERLAPPED {
      DWORD Internal;
      DWORD InternalHigh;
      DWORD Offset;
      DWORD OffsetHigh;
      WSAEVENT hEvent;      // 唯一需要关注的参数,用来关联WSAEvent对象
     } WSAOVERLAPPED, *LPWSAOVERLAPPED;
    复制代码
    WSAEVENT event;                   // 定义事件
    WSAOVERLAPPED AcceptOverlapped ; // 定义重叠结构
    event = WSACreateEvent();         // 建立一个事件对象句柄
    ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED)); // 初始化重叠结构
    AcceptOverlapped.hEvent = event;    // Done !!
     
    2.      WSARecv系列函数
    复制代码
     int WSARecv(
    SOCKET s,                      // 当然是投递这个操作的套接字
    LPWSABUF lpBuffers,          // 接收缓冲区,与Recv函数不同
                  // 这里需要一个由WSABUF结构构成的数组
    DWORD dwBufferCount,      // 数组中WSABUF结构的数量
    LPDWORD lpNumberOfBytesRecvd,  // 如果接收操作立即完成,这里会返回函数调用
                      // 所接收到的字节数
    LPDWORD lpFlags,             // 这里设置为0 即可
    LPWSAOVERLAPPED lpOverlapped,  // “绑定”的重叠结构
    LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
                                   // 完成例程中将会用到的参数,否则设置为 NULL
      );

    返回值:
    WSA_IO_PENDING : 最常见的返回值,这是说明我们的WSARecv操作成功了,但是
                        I/O操作还没有完成,所以我们就需要绑定一个事件来通知我们操作何时完成
    复制代码
     

    SOCKET s;
    WSABUF DataBuf;           // 定义WSABUF结构的缓冲区
    // 初始化一下DataBuf
    #define DATA_BUFSIZE 5096
    char buffer[DATA_BUFSIZE];
    ZeroMemory(buffer, DATA_BUFSIZE);
    DataBuf.len = DATA_BUFSIZE;
    DataBuf.buf = buffer;
    DWORD dwBufferCount = 1, dwRecvBytes = 0, Flags = 0;
    // 建立需要的重叠结构
    WSAOVERLAPPED AcceptOverlapped ;// 如果要处理多个操作,这里当然需要一个
    // WSAOVERLAPPED数组
    WSAEVENT event;     // 如果要多个事件,这里当然也需要一个WSAEVENT数组
                               // 需要注意的是可能一个SOCKET同时会有一个以上的重叠请求,
    //  也就会对应一个以上的WSAEVENT
    Event = WSACreateEvent();
    ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));
    AcceptOverlapped.hEvent = event;     // 关键的一步,把事件句柄“绑定”到重叠结构上
    // 作了这么多工作,终于可以使用WSARecv来把我们的请求投递到重叠结构上了,呼。。。。
    WSARecv(s, &DataBuf, dwBufferCount, &dwRecvBytes, 
    &Flags, &AcceptOverlapped, NULL);

    3.      WSAWaitForMultipleEvents函数

    复制代码
     DWORD WSAWaitForMultipleEvents(
     DWORD cEvents,                        // 等候事件的总数量
    const WSAEVENT* lphEvents,           // 事件数组的指针
    BOOL fWaitAll,          // 这个要多说两句:
    // 如果设置为 TRUE,则事件数组中所有事件被传信的时候函数才会返回
     // FALSE则任何一个事件被传信函数都要返回
     // 我们这里肯定是要设置为FALSE的
    DWORD dwTimeout,    // 超时时间,如果超时,函数会返回 WSA_WAIT_TIMEOUT
                                   // 如果设置为0,函数会立即返回
                                // 如果设置为 WSA_INFINITE只有在某一个事件被传信后才会返回
                                // 在这里不建议设置为WSA_INFINITE,
    BOOL fAlertable       // 在完成例程中会用到这个参数,这里我们先设置为FALSE
     );

    返回值:
        WSA_WAIT_TIMEOUT :最常见的返回值,我们需要做的就是继续Wait
        WSA_WAIT_FAILED : 出现了错误,请检查cEvents和lphEvents两个参数是否有效
    复制代码
    4.      WSAGetOverlappedResult函数
    既然我们可以通过WSAWaitForMultipleEvents函数来得到重叠操作完成的通知,那么我们自然也需要一个函数来查询一下重叠操作的结果,定义如下
    复制代码
                BOOL WSAGetOverlappedResult(
                              SOCKET s,                   // SOCKET,不用说了
                              LPWSAOVERLAPPED lpOverlapped,  // 这里是我们想要查询结果的那个重叠结构的指针
                              LPDWORD lpcbTransfer,     // 本次重叠操作的实际接收(或发送)的字节数
                              BOOL fWait,                // 设置为TRUE,除非重叠操作完成,否则函数不会返回
                                                                  // 设置FALSE,而且操作仍处于挂起状态,那么函数就会返回FALSE
                                                                  // 错误为WSA_IO_INCOMPLETE
                                                                  // 不过因为我们是等待事件传信来通知我们操作完成,所以我们这里设
                    // 置成什么都没有作用…..-_-b  别仍鸡蛋啊,我也想说得清楚一些…
                              LPDWORD lpdwFlags       // 指向DWORD的指针,负责接收结果标志
                            );
    复制代码
    如果WSAGetOverlappedResult完成以后,第三个参数返回是 0 ,则说明通信对方已经关闭连接,我们这边的SOCKET, Event之类的也就可关闭。

    DWORD dwBytesTransferred;
    WSAGetOverlappedResult( AcceptSocket, AcceptOverlapped ,
    &dwBytesTransferred, FALSE, &Flags);
    // 先检查通信对方是否已经关闭连接
    // 如果==0则表示连接已经,则关闭套接字
    if(dwBytesTransferred == 0)
    {
             closesocket(AcceptSocket);
          WSACloseEvent(EventArray[dwIndex]);    // 关闭事件
             return;
    }

    原文链接:http://blog.csdn.net/PiggyXP/archive/2004/09/23/114883.aspx

    完成例程

    一.         完成例程的优点

    1.    首先需要指明的是,这里的“完成例程”(Completion Routine)并非是大家所常听到的 “完成端口”(Completion Port),而是另外一种管理重叠I/O请求的方式,而至于什么是重叠I/O,简单来讲就是Windows系统内部管理I/O的一种方式,核心就是调用的ReadFile和WriteFile函数,在制定设备上执行I/O操作,不光是可用于网络通信,也可以用于其他需要的地方。

    在Windows系统中,管理重叠I/O可以有三种方式:

    (1)  基于事件通知的重叠I/O模型

     (2)  基于“完成例程”的重叠I/O模型

     (3)  “完成端口”模型

    虽然都是基于重叠I/O,但是因为前两种模型都是需要自己来管理任务的分派 ,所以性能上没有区别,而完成端口是创建完成端口对象使操作系统亲自来管理任务的分派,所以完成端口肯定是能获得最好的性能。

    2.    如果你想要使用重叠I/O机制带来的高性能模型,又懊恼于基于事件通知的重叠模型要收到64个等待事件的限制,还有点畏惧完成端口稍显复杂的初始化过程,那么“完成例程”无疑是你最好的选择!^_^ 因为完成例程摆脱了事件通知的限制,可以连入任意数量客户端而不用另开线程,也就是说只用很简单的一些代码就可以利用Windows内部的I/O机制来获得网络服务器的高性能,是不是心动了呢?那就一起往下看。。。。。。。。。。

    3.    而且个人感觉“完成例程”的方式比重叠I/O更好理解,因为就和我们传统的“回调函数”是一样的,也更容易使用一些,推荐!

    括一点说,上一篇拙作中提到的那个基于事件通知的重叠I/O模型,在你投递了一个请求以后(比如WSARecv),系统在完成以后是用事件来通知你的,而在完成例程中,系统在网络操作完成以后会自动调用你提供的回调函数,区别仅此而已,是不是很简单呢?如果还没有看明白,我们打个通俗易懂的比方,完成例程的处理过程,也就像我们告诉系统,说“我想要在网络上接收网络数据,你去帮我办一下”(投递WSARecv操作),“不过我并不知道网络数据合适到达,总之在接收到网络数据之后,你直接就调用我给你的这个函数(比如_CompletionProess),把他们保存到内存中或是显示到界面中等等,全权交给你处理了”,于是乎,系统在接收到网络数据之后,一方面系统会给我们一个通知,另外同时系统也会自动调用我们事先准备好的回调函数,就不需要我们自己操心了。

    完成例程回调函数原型及传递方式

    Void CALLBACK _CompletionRoutineFunc(   
      DWORD dwError, // 标志咱们投递的重叠操作,比如WSARecv,完成的状态是什么   
      DWORD cbTransferred, // 指明了在重叠操作期间,实际传输的字节量是多大   
      LPWSAOVERLAPPED lpOverlapped, // 参数指明传递到最初的IO调用内的一个重叠  结构   
      DWORD dwFlags  // 返回操作结束时可能用的标志(一般没用));   

    四. 完成例程的实现步骤

    基础知识方面需要知道的就是这么多,下面我们配合代码,来一步步的讲解如何亲手实现一个完成例程模型(前面几步的步骤和基于事件通知的重叠I/O方法是一样的)。

    【第一步】创建一个套接字,开始在指定的端口上监听连接请求

    和其他的SOCKET初始化全无二致,直接照搬即可,在此也不多费唇舌了,需要注意的是为了一目了然,我去掉了错误处理,平常可不要这样啊,尽管这里出错的几率比较小。

    view plaincopy to clipboardprint?
    WSADATA wsaData;   
    WSAStartup(MAKEWORD(2,2),&wsaData);   
      
    ListenSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);  //创建TCP套接字   
      
    SOCKADDR_IN ServerAddr;                           //分配端口及协议族并绑定   
    ServerAddr.sin_family=AF_INET;                                   
    ServerAddr.sin_addr.S_un.S_addr  =htonl(INADDR_ANY);             
    ServerAddr.sin_port=htons(11111);        // 在11111端口监听   
                                        // 端口号可以随意更改,但最好不要少于1024   
      
    bind(ListenSocket,(LPSOCKADDR)&ServerAddr, sizeof(ServerAddr)); // 绑定套接字   
      
    listen(ListenSocket, 5);                                   //开始监听  


    【第二步】接受一个入站的连接请求

      一个accept就完了,都是一样一样一样一样的啊~~~~~~~~~~

     至于AcceptEx的使用,在完成端口中我会讲到,这里就先不一次灌输这么多了,不消化啊^_^

    view plaincopy to clipboardprint?
    AcceptSocket = accept (ListenSocket, NULL,NULL) ;  

    当然,这里是我偷懒,如果想要获得连入客户端的信息(记得论坛上也常有人问到),accept的后两个参数就不要用NULL,而是这样

    view plaincopy to clipboardprint?
    SOCKADDR_IN ClientAddr;                   // 定义一个客户端得地址结构作为参数   
    int addr_length=sizeof(ClientAddr);   
    AcceptSocket = accept(ListenSocket,(SOCKADDR*)&ClientAddr, &addr_length);   
    // 于是乎,我们就可以轻松得知连入客户端的信息了   
    LPCTSTR lpIP =  inet_ntoa(ClientAddr.sin_addr);      // 连入客户端的 IP   
    UINT nPort = ClientAddr.sin_port;                      // 连入客户端的Port  

    【第三步】准备好我们的重叠结构

    有新的套接字连入以后,新建立一个WSAOVERLAPPED重叠结构(当然也可以提前建立好),准备绑定到我们的重叠操作上去。这里也可以看到和上一篇中的明显区别,就是不用再为WSAOVERLAPPED结构绑定一个hEvent了。

    view plaincopy to clipboardprint?
    // 这里只定义一个,实际上是每一个SOCKET的每一个操作都需要绑定一个重叠结构的,所以在实际使用面对多个客户端的时候要定义为数组,详见示例代码;   
    WSAOVERLAPPED AcceptOverlapped;    
    ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));      // 置零  

        

    【第四步】开始在套接字上投递WSARecv请求,需要将第三步准备的WSAOVERLAPPED结构和我们定义的完成例程函数为参数

    各个变量都已经初始化OK以后,我们就可以开始进行具体的Socket通信函数调用了,然后让系统内部的重叠结构来替我们管理I/O请求,我们只用等待网络通信完成后调用咱们的回调函数就OK了。

    这个步骤的重点就是 绑定一个Overlapped变量和一个完成例程函数
    view plaincopy to clipboardprint?
    // 将WSAOVERLAPPED结构指定为一个参数,在套接字上投递一个异步WSARecv()请求   
    // 并提供下面的作为完成例程的_CompletionRoutine回调函数(函数名字)   
    if(WSARecv(   
        AcceptSocket,   
        &DataBuf,   
        1,   
        &dwRecvBytes,   
        &Flags,   
        &AcceptOverlapped,   
        _CompletionRoutine) == SOCKET_ERROR)  // 注意我们传入的回调函数指针   
        {   
            if(WSAGetLastError() != WSA_IO_PENDING)   
            {   
                ReleaseSocket(nSockIndex);   
                continue;   
                }   
            }   
    }  

      

    【第五步】 调用WSAWaitForMultipleEvents函数或者SleepEx函数等待重叠操作返回的结果

      我们在前面提到过,投递完WSARecv操作,并绑定了Overlapped结构和完成例程函数之后,我们基本就是完事大吉了,等了系统自己去完成网络通信,并在接收到数据的时候,会自动调用我们的完成例程函数。

      而我们在主线程中需要做的事情只有:做别的事情,并且等待系统完成了完成例程调用后的返回结果。

    就是说在WSARecv调用发起完毕之后,我们不得不在后面再紧跟上一些等待完成结果的代码。有两种办法可以实现:

    1)    和上一篇重叠I/O中讲到的一样,我们可以使用WSAWaitForMultipleEvent来等待重叠操作的事件通知, 方法如下:

    view plaincopy to clipboardprint?
    // 因为WSAWaitForMultipleEvents() API要求   
    // 在一个或多个事件对象上等待, 但是这个事件数组已经不是和SOCKET相关联的了   
    // 因此不得不创建一个伪事件对象.    
    WSAEVENT EventArray[1];        
    EventArray[0] = WSACreateEvent();                        // 建立一个事件   
            ////////////////////////////////////////////////////////////////////////////////   
    // 然后就等待重叠请求完成就可以了,注意保存返回值,这个很重要   
    DWORD dwIndex = WSAWaitForMultipleEvents(1,EventArray,FALSE,WSA_INFINITE,TRUE);  


    这里参数的含义我就不细说了,MSDN上一看就明白,调用这个函数以后,线程就会置于一个警觉的等待状态,注意 fAlertable 参数一定要设置为 TRUE。

    2)    可以直接使用SleepEx函数来完成等待,效果都是一样的。

    SleepEx函数调用起来就简单得多,它的函数原型定义是这样的


         
    view plaincopy to clipboardprint?
    DWORD SleepEx(   
                 DWORD dwMilliseconds,  // 等待的超时时间,如果设置为INFINITE就会一直等待下去   
                 BOOL   bAlertable   // 是否置于警觉状态,如果为FALSE,则一定要等待超时时间完毕之后才会返回,这里我们是希望重叠操作一完成就能返回,所以同(1)一样,我们一定要设置为TRUE   
     );  

        调用这个函数的时候,同样注意用一个DWORD类型变量来保存它的返回值,后面会派上用场。


    【第六步】通过等待函数的返回值取得重叠操作的完成结果

    这是我们最关心的事情,费了那么大劲投递的这个重叠操作究竟是个什么结果呢?就是通过上一步中我们调用的等待函数的DWORD类型的返回值,正常情况下,在操作完成之后,应该是返回WAIT_IO_COMPLETION,如果返回的是 WAIT_TIMEOUT,则表示等待设置的超时时间到了,但是重叠操作依旧没有完成,应该通过循环再继续等待。如果是其他返回值,那就坏事了,说明网络通信出现了其他异常,程序就可以报错退出了……

    判断返回值的代码大致如下:
    view plaincopy to clipboardprint?
    ///////////////////////////////////////////////////////////////////////////////////   
    // 返回WAIT_IO_COMPLETION表示一个重叠请求完成例程代码的结束。继续为更多的完成例程服务   
    if(dwIndex == WAIT_IO_COMPLETION)   
    {   
    TRACE("重叠操作完成... ");   
    }   
    else if( dwIndex==WAIT_TIMEOUT )   
    {   
         TRACE(“超时了,继续调用等待函数”);   
    }   
    else   
    {   
        TRACE(“废了…”);   
    }  

    操作完成了之后,就说明我们上一个操作已经成功了,成功了之后做什么?当然是继续投递下一个重叠操作了啊…..继续上面的循环。

    【第七步】继续回到第四步,在套接字上继续投递WSARecv请求,重复步骤4-7

    代码

    共同部分-监听socket

    监听线程

    BlockingModel监听线程
    事件选择模型-监听线程
    重叠模型-监听线程
    重叠IO线程
    完成例程-监听模型
    完成例程-I/O线程

    六、IOCP

    爱程序 不爱bug 爱生活 不爱黑眼圈 我和你们一样 我和你们不一样 我不是凡客 我要做geek
  • 相关阅读:
    Jquery实现类似百度的搜索框
    Spring mvc 初始化过程
    Git学习笔记(2)-Eclipse中Git插件使用
    Git学习笔记(1)
    Tomcat7设置环境变量供java代码读取
    webpack+gulp实现自动构建部署
    netty 粘包问题处理
    java 并发工具类CountDownLatch & CyclicBarrier
    add spring-boot modules to maven project
    Spring Boot (#1 quick start)
  • 原文地址:https://www.cnblogs.com/yifi/p/6482757.html
Copyright © 2011-2022 走看看