Windows Socket五种I/O模型——代码全攻略

      如果你想在Windows平台上构建服务器应用,那么I/O模型是你必须考虑的。Windows操作系统提供了选择(Select)、异步选择(WSAAsyncSelect)、事件选择(WSAEventSelect)、重叠I/O(Overlapped I/O)和完成端口(Completion Port)共五种I/O模型。每一种模型均适用于一种特定的应用场景。程序员应该对自己的应用需求非常明确,而且综合考虑到程序的扩展性和可移植性等因素,作出自己的选择。

      我会以一个回应反射式服务器(与《Windows网络编程》第八章一样)来介绍这五种I/O模型。 我们假设客户端的代码如下(为代码直观,省去所有错误检查,以下同):

 1 #include <WINSOCK2.H>
 2 #include <stdio.h>
 3 #define SERVER_ADDRESS "137.117.2.148"
 4 #define PORT       5150
 5 #define MSGSIZE    1024
 6 #pragma comment(lib, "ws2_32.lib")
 7 
 8 int main()
 9 { 
10     //用作WSAStartup()函数的第二个参数,接收Windows Sockets实现的细节。
11     WSADATA wsaData; 
12     //用来与服务器socket进行通信的客户端socket。
13     SOCKET sClient;
14     //用来设置服务器的地址信息。
15     SOCKADDR_IN server;
16     char szMessage[MSGSIZE];
17     int ret; 
18     //第一步:初始化Winsock库
19     WSAStartup(0x0202, &wsaData);
20     //第二步:创建用来与服务器进行通信的客户端
21     sClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
22     //第三步:将服务器端的地址信息保存入SOCKADDR_IN类型的变量sever中
23     memset(&server, 0, sizeof(SOCKADDR_IN));
24     server.sin_family = AF_INET;
25     server.sin_addr.S_un.S_addr = inet_addr(SERVER_ADDRESS);
26     server.sin_port = htons(PORT);
27     //第四步:通过connect函数向服务器发起连接。
28     connect(sClient, (struct sockaddr *)&server, sizeof(SOCKADDR_IN));
29     while (TRUE)
30     { 
31         //连接服务器成功后,客户端控制台窗口将显示Send:
32         printf("Send:");
33         //将用户输入的内容保存到szMessage中
34         gets(szMessage);
35         //发送消息将szMessage中的内容通过sClient发往服务器
36         send(sClient, szMessage, strlen(szMessage), 0);
37         //将接收到的内容放入szMessage中
38         ret = recv(sClient, szMessage, MSGSIZE, 0);
39         szMessage[ret] = '\0';
40         printf("Received [%d bytes]: '%s'\n", ret, szMessage);
41    }
42 
43     closesocket(sClient);
44     WSACleanup();
45     return 0;
46 }

      客户端所做的事情相当简单,创建套接字,连接服务器,然后不停的发送和接收数据。

      比较容易想到的一种服务器模型就是采用一个主线程,负责监听客户端的连接请求,当接收到某个客户端的连接请求后,创建一个专门用于和该客户端通信的套接字和一个辅助线程。以后该客户端和服务器的交互都在这个辅助线程内完成。这种方法比较直观,程序非常简单而且可移植性好,但是不能利用平台相关的特性。例如,如果连接数增多的时候(成千上万的连接),那么线程数成倍增长,操作系统忙于频繁的线程间切换,而且大部分线程在其生命周期内都是处于非活动状态的,这大大浪费了系统的资源。所以,如果你已经知道你的代码只会运行在Windows平台上,建议采用Winsock I/O模型。

      一.选择模型 Select(选择)模型是Winsock中最常见的I/O模型。之所以称其为“Select模型”,是由于它的“中心思想”便是利用select函数,实现对I/O的管理。最初设计该模型时,主要面向的是某些使用UNIX操作系统的计算机,它们采用的是Berkeley套接字方案。Select模型已集成到Winsock 1.1中,它使那些想避免在套接字调用过程中被无辜“锁定”的应用程序,采取一种有序的方式,同时进行对多个套接字的管理。由于Winsock 1.1向后兼容于Berkeley套接字实施方案,所以假如有一个Berkeley套接字应用使用了select函数,那么从理论角度讲,毋需对其进行任何修改,便可正常运行。(节选自《Windows网络编程》第八章) 下面的这段程序就是利用选择模型实现的Echo服务器的代码(已经不能再精简了):

  1 #include <winsock.h>
  2 #include <stdio.h>
  3 #define PORT       5150
  4 #define MSGSIZE    1024
  5 #pragma comment(lib, "ws2_32.lib")
  6  
  7 int g_iTotalConn = 0;
  8 SOCKET g_CliSocketArr[FD_SETSIZE];
  9 DWORD WINAPI WorkerThread(LPVOID lpParameter);
 10 
 11 int main()
 12 {
 13     WSADATA wsaData;
 14      
 15     SOCKET sListen, sClient;
 16     SOCKADDR_IN local, client;
 17     int iaddrSize = sizeof(SOCKADDR_IN);
 18 
 19     DWORD dwThreadId;
 20     // Initialize Windows socket library
 21 
 22     WSAStartup(0x0202, &wsaData);
 23 
 24     // Create listening socket
 25     sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 26 
 27     // Bind
 28     local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
 29     local.sin_family = AF_INET;
 30     local.sin_port = htons(PORT);
 31     bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN));
 32 
 33     // Listen
 34     listen(sListen, 3);
 35 
 36     // Create worker thread
 37     CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);
 38 
 39     while (TRUE)
 40     {
 41         // Accept a connection
 42         sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize);
 43 
 44         printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
 45 
 46         // Add socket to g_CliSocketArr
 47         g_CliSocketArr[g_iTotalConn++] = sClient;
 48     }
 49     return 0;
 50 }
 51 
 52 DWORD WINAPI WorkerThread(LPVOID lpParam)
 53 {
 54     int   i;
 55     fd_set  fdread;
 56     int  ret;
 57     struct timeval   tv = {1, 0};
 58     char   szMessage[MSGSIZE];
 59     while (TRUE)
 60     {
 61         FD_ZERO(&fdread);
 62         for (i = 0; i < g_iTotalConn; i++)
 63         {
 64             FD_SET(g_CliSocketArr[i], &fdread);
 65         }
 66         // We only care read event
 67         ret = select(0, &fdread, NULL, NULL, &tv);
 68         if (ret == 0)
 69         {
 70         // Time expired
 71         continue;
 72         }
 73 
 74         for (i = 0; i < g_iTotalConn; i++)
 75         {
 76             if (FD_ISSET(g_CliSocketArr[i], &fdread))
 77             {
 78                 // A read event happened on g_CliSocketArr[i]
 79                 ret = recv(g_CliSocketArr[i], szMessage, MSGSIZE, 0);
 80                 if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))
 81                 { 
 82                     // Client socket closed 
 83                     printf("Client socket %d closed.\n", g_CliSocketArr[i]);
 84                     closesocket(g_CliSocketArr[i]);
 85                     if (i < g_iTotalConn - 1)
 86                     {
 87                         g_CliSocketArr[i--] = g_CliSocketArr[--g_iTotalConn];
 88                     }
 89                  }
 90                  else 
 91                  { 
 92                     // We received a message from client
 93                     szMessage[ret] = '\0';
 94                     send(g_CliSocketArr[i], szMessage, strlen(szMessage), 0);
 95                  }
 96             }
 97         }
 98     }
 99     return 0;
100 }

      服务器的几个主要动作如下:
      1.创建监听套接字,绑定,监听;
      2.创建工作者线程;
      3.创建一个套接字数组,用来存放当前所有活动的客户端套接字,每accept一个连接就更新一次数组;
      4.接受客户端的连接。这里有一点需要注意的,就是我没有重新定义FD_SETSIZE宏,所以服务器最多支持的并发连接数为64。而且,这里决不能无条件的accept,服务器应该根据当前的连接数来决定是否接受来自某个客户端的连接。一种比较好的实现方案就是采用WSAAccept函数,而且让WSAAccept回调自己实现的Condition Function。如下所示:

int CALLBACK ConditionFunc(LPWSABUF lpCallerId,LPWSABUF lpCallerData, LPQOS lpSQOS,LPQOS lpGQOS,LPWSABUF lpCalleeId, LPWSABUF lpCalleeData,GROUP FAR * g,DWORD dwCallbackData)
{
    if (当前连接数 < FD_SETSIZE)
        return CF_ACCEPT;
    else
        return CF_REJECT;
}

      工作者线程里面是一个死循环,一次循环完成的动作是: 1.将当前所有的客户端套接字加入到读集fdread中; 2.调用select函数; 3.查看某个套接字是否仍然处于读集中,如果是,则接收数据。如果接收的数据长度为0,或者发生WSAECONNRESET错误,则表示客户端套接字主动关闭,这时需要将服务器中对应的套接字所绑定的资源释放掉,然后调整我们的套接字数组(将数组中最后一个套接字挪到当前的位置上)

      除了需要有条件接受客户端的连接外,还需要在连接数为0的情形下做特殊处理,因为如果读集中没有任何套接字,select函数会立刻返回,这将导致工作者线程成为一个毫无停顿的死循环,CPU的占用率马上达到100%。

      二.异步选择 Winsock提供了一个有用的异步I/O模型。利用这个模型,应用程序可在一个套接字上,接收以Windows消息为基础的网络事件通知。具体的做法是在建好一个套接字后,调用WSAAsyncSelect函数。该模型最早出现于Winsock的1.1版本中,用于帮助应用程序开发者面向一些早期的16位Windows平台(如Windows for Workgroups),适应其“落后”的多任务消息环境。应用程序仍可从这种模型中得到好处,特别是它们用一个标准的Windows例程(常称为"WndProc"),对窗口消息进行管理的时候。该模型亦得到了Microsoft Foundation Class(微软基本类,MFC)对象CSocket的采纳。(节选自《Windows网络编程》第八章) 我还是先贴出代码,然后做详细解释:

 

  1 #include <winsock.h>
  2 #include <tchar.h>
  3 #define PORT      5150
  4 #define MSGSIZE   1024
  5 #define WM_SOCKET WM_USER+0
  6 #pragma comment(lib, "ws2_32.lib")
  7 
  8 LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
  9 
 10 int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
 11 {
 12  static TCHAR szAppName[] = _T("AsyncSelect Model");
 13 
 14  HWND  hwnd ;
 15  MSG    msg ;
 16  WNDCLASS  wndclass ;
 17 
 18  wndclass.style= CS_HREDRAW | CS_VREDRAW ;
 19  wndclass.lpfnWndProc= WndProc ;
 20  wndclass.cbClsExtra= 0 ;
 21  wndclass.cbWndExtra= 0 ;
 22  wndclass.hInstance= hInstance ;
 23  wndclass.hIcon= LoadIcon (NULL, IDI_APPLICATION) ;
 24  wndclass.hCursor= LoadCursor (NULL, IDC_ARROW) ;
 25  wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
 26  wndclass.lpszMenuName= NULL ;
 27  wndclass.lpszClassName = szAppName ;
 28 
 29  if (!RegisterClass(&wndclass))
 30  {
 31   MessageBox (NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ;
 32   return 0 ;
 33  }
 34 
 35  hwnd =CreateWindow(
 36   szAppName,// window class name
 37   TEXT ("AsyncSelect Model"),// window caption
 38   WS_OVERLAPPEDWINDOW,// window style
 39   CW_USEDEFAULT,// initial x position
 40   CW_USEDEFAULT,// initial y position
 41   CW_USEDEFAULT,// initial x size
 42   CW_USEDEFAULT,// initial y size
 43   NULL,// parent window handle
 44   NULL,// window menu handle
 45   hInstance,// program instance handle
 46   NULL) ;// creation parameters
 47 
 48  ShowWindow(hwnd, iCmdShow);
 49  UpdateWindow(hwnd);
 50 
 51  while (GetMessage(&msg, NULL, 0, 0))
 52  {
 53   TranslateMessage(&msg) ;
 54   DispatchMessage(&msg) ;
 55  }
 56  return msg.wParam;
 57 }
 58 
 59 LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
 60 {
 61  WSADATA  wsd;
 62  static SOCKET sListen;
 63  SOCKET   sClient;
 64  SOCKADDR_IN   local, client;
 65  int ret, iAddrSize = sizeof(client);
 66  char  szMessage[MSGSIZE];
 67  
 68  switch (message)
 69  {
 70  case WM_CREATE:
 71   // Initialize Windows Socket library
 72   WSAStartup(0x0202, &wsd);
 73   // Create listening socket
 74   sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 75   // Bind
 76   local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
 77   local.sin_family = AF_INET;
 78   local.sin_port = htons(PORT);
 79  
 80   bind(sListen, (struct sockaddr *)&local, sizeof(local));
 81   // Listen
 82   listen(sListen, 3);
 83   // Associate listening socket with FD_ACCEPT event
 84   WSAAsyncSelect(sListen, hwnd, WM_SOCKET, FD_ACCEPT);
 85   return 0;
 86 
 87  case WM_DESTROY:
 88   closesocket(sListen);
 89   WSACleanup();
 90   PostQuitMessage(0);
 91   return 0;
 92  
 93  case WM_SOCKET:
 94   if (WSAGETSELECTERROR(lParam))
 95   {
 96    closesocket(wParam);
 97    break;
 98   }
 99   switch (WSAGETSELECTEVENT(lParam))
100   {
101   case FD_ACCEPT:
102    // Accept a connection from client
103    sClient = accept(wParam, (struct sockaddr *)&client, &iAddrSize);
104    // Associate client socket with FD_READ and FD_CLOSE event
105    WSAAsyncSelect(sClient, hwnd, WM_SOCKET, FD_READ | FD_CLOSE);
106    break;
107   case FD_READ:
108    ret = recv(wParam, szMessage, MSGSIZE, 0);
109    if (ret == 0 || ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET)
110    {
111     closesocket(wParam);
112    }
113    else
114    {
115     szMessage[ret] = '\0';
116     send(wParam, szMessage, strlen(szMessage), 0);
117    }
118    break;
119  
120   case FD_CLOSE:
121    closesocket(wParam);
122    break;
123   }
124   return 0;
125  }
126  return DefWindowProc(hwnd, message, wParam, lParam);
127 }

      在我看来,WSAAsyncSelect是最简单的一种Winsock I/O模型(之所以说它简单是因为一个主线程就搞定了)。使用Raw Windows API写过窗口类应用程序的人应该都能看得懂。这里,我们需要做的仅仅是: 1.在WM_CREATE消息处理函数中,初始化Windows Socket library,创建监听套接字,绑定,监听,并且调用WSAAsyncSelect函数表示我们关心在监听套接字上发生的FD_ACCEPT事件; 2.自定义一个消息WM_SOCKET,一旦在我们所关心的套接字(监听套接字和客户端套接字)上发生了某个事件,系统就会调用WndProc并且message参数被设置为WM_SOCKET; 3.在WM_SOCKET的消息处理函数中,分别对FD_ACCEPT、FD_READ和FD_CLOSE事件进行处理; 4.在窗口销毁消息(WM_DESTROY)的处理函数中,我们关闭监听套接字,清除Windows Socket library

      下面这张用于WSAAsyncSelect函数的网络事件类型表可以让你对各个网络事件有更清楚的认识: 表1

FD_READ

应用程序想要接收有关是否可读的通知,以便读入数据

FD_WRITE

应用程序想要接收有关是否可写的通知,以便写入数据

FD_OOB

应用程序想接收是否有带外(OOB)数据抵达的通知

FD_ACCEPT

应用程序想接收与进入连接有关的通知

FD_CONNECT

应用程序想接收与一次连接或者多点join操作完成的通知

FD_CLOSE

应用程序想接收与套接字关闭有关的通知

FD_QOS

应用程序想接收套接字“服务质量”(QoS)发生更改的通知

FD_GROUP_QOS 

应用程序想接收套接字组“服务质量”发生更改的通知(现在没什么用处,为未来套接字组的使用保留)

FD_ROUTING_INTERFACE_CHANGE

应用程序想接收在指定的方向上,与路由接口发生变化的通知

FD_ADDRESS_LIST_CHANGE 

应用程序想接收针对套接字的协议家族,本地地址列表发生变化的通知

      三.事件选择 Winsock提供了另一个有用的异步I/O模型。和WSAAsyncSelect模型类似的是,它也允许应用程序在一个或多个套接字上,接收以事件为基础的网络事件通知。对于表1总结的、由WSAAsyncSelect模型采用的网络事件来说,它们均可原封不动地移植到新模型。在用新模型开发的应用程序中,也能接收和处理所有那些事件。该模型最主要的差别在于网络事件会投递至一个事件对象句柄,而非投递至一个窗口例程。(节选自《Windows网络编程》第八章) 还是让我们先看代码然后进行分析:

  1 #include <winsock2.h>
  2 #include <stdio.h>
  3 #define PORT    5150
  4 #define MSGSIZE 1024
  5 #pragma comment(lib, "ws2_32.lib")
  6 
  7 int   g_iTotalConn = 0;
  8 SOCKET   g_CliSocketArr[MAXIMUM_WAIT_OBJECTS];
  9 WSAEVENT g_CliEventArr[MAXIMUM_WAIT_OBJECTS];
 10 DWORD WINAPI WorkerThread(LPVOID);
 11 void Cleanup(int index);
 12 
 13 int main()
 14 {
 15  WSADATA   wsaData;
 16  SOCKET    sListen, sClient;
 17  SOCKADDR_IN local, client;
 18  DWORD   dwThreadId;
 19  int    iaddrSize = sizeof(SOCKADDR_IN);
 20  
 21  // Initialize Windows Socket library
 22  WSAStartup(0x0202, &wsaData);
 23 
 24  // Create listening socket
 25  sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 26 
 27  // Bind
 28  local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
 29  local.sin_family = AF_INET;
 30  local.sin_port = htons(PORT);
 31  bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN));
 32 
 33  // Listen
 34  listen(sListen, 3);
 35 
 36  // Create worker thread
 37  CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);
 38  while (TRUE)
 39  {
 40   // Accept a connection
 41   sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize);
 42   printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
 43 
 44   // Associate socket with network event
 45   g_CliSocketArr[g_iTotalConn] = sClient;
 46   g_CliEventArr[g_iTotalConn] = WSACreateEvent();
 47   WSAEventSelect(
 48    g_CliSocketArr[g_iTotalConn],
 49    g_CliEventArr[g_iTotalConn],
 50    FD_READ | FD_CLOSE);
 51   g_iTotalConn++;
 52  }
 53 }
 54 
 55 DWORD WINAPI WorkerThread(LPVOID lpParam)
 56 {
 57  int   ret, index;
 58  WSANETWORKEVENTS NetworkEvents;
 59  char    szMessage[MSGSIZE];
 60  while (TRUE)
 61  {
 62   ret = WSAWaitForMultipleEvents(g_iTotalConn, g_CliEventArr, FALSE, 1000, FALSE);
 63   if (ret == WSA_WAIT_FAILED || ret == WSA_WAIT_TIMEOUT)
 64   {
 65    continue;
 66   }
 67   index = ret - WSA_WAIT_EVENT_0;
 68   WSAEnumNetworkEvents(g_CliSocketArr[index], g_CliEventArr[index], &NetworkEvents);
 69   if (NetworkEvents.lNetworkEvents & FD_READ)
 70   {
 71    // Receive message from client
 72    ret = recv(g_CliSocketArr[index], szMessage, MSGSIZE, 0);
 73    if (ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET))
 74    {
 75     Cleanup(index);
 76    }
 77    else
 78    {
 79     szMessage[ret] = '\0';
 80     send(g_CliSocketArr[index], szMessage, strlen(szMessage), 0);
 81    }
 82   }
 83   if (NetworkEvents.lNetworkEvents & FD_CLOSE)
 84   {
 85    Cleanup(index);
 86   }
 87  }
 88  return 0;
 89 }
 90 
 91 void Cleanup(int index)
 92 {
 93  closesocket(g_CliSocketArr[index]);
 94  WSACloseEvent(g_CliEventArr[index]);
 95  if (index < g_iTotalConn - 1)
 96  {
 97   g_CliSocketArr[index] = g_CliSocketArr[g_iTotalConn - 1];
 98   g_CliEventArr[index] = g_CliEventArr[g_iTotalConn - 1];
 99  }
100  g_iTotalConn--;
101 }

      事件选择模型也比较简单,实现起来也不是太复杂,它的基本思想是将每个套接字都和一个WSAEVENT对象对应起来,并且在关联的时候指定需要关注的哪些网络事件。一旦在某个套接字上发生了我们关注的事件(FD_READ和FD_CLOSE),与之相关联的WSAEVENT对象被Signaled。程序定义了两个全局数组,一个套接字数组,一个WSAEVENT对象数组,其大小都是MAXIMUM_WAIT_OBJECTS(64),两个数组中的元素一一对应。 同样的,这里的程序没有考虑两个问题,一是不能无条件的调用accept,因为我们支持的并发连接数有限。解决方法是将套接字按MAXIMUM_WAIT_OBJECTS分组,每MAXIMUM_WAIT_OBJECTS个套接字一组,每一组分配一个工作者线程;或者采用WSAAccept代替accept,并回调自己定义的Condition Function。第二个问题是没有对连接数为0的情形做特殊处理,程序在连接数为0的时候CPU占用率为100%。

      四.重叠I/O模型 Winsock2的发布使得Socket I/O有了和文件I/O统一的接口。我们可以通过使用Win32文件操纵函数ReadFile和WriteFile来进行Socket I/O。伴随而来的,用于普通文件I/O的重叠I/O模型和完成端口模型对Socket I/O也适用了。这些模型的优点是可以达到更佳的系统性能,但是实现较为复杂,里面涉及较多的C语言技巧。例如我们在完成端口模型中会经常用到所谓的“尾随数据”。

     1.用事件通知方式实现的重叠I/O模型

  1 #include <winsock2.h>
  2 #include <stdio.h>
  3 #define PORT    5150
  4 #define MSGSIZE 1024
  5 #pragma comment(lib, "ws2_32.lib")
  6 
  7 typedef struct
  8 {
  9  WSAOVERLAPPED overlap;
 10  WSABUF      Buffer;
 11  char        szMessage[MSGSIZE];
 12  DWORD       NumberOfBytesRecvd;
 13  DWORD       Flags;
 14 }PER_IO_OPERATION_DATA, *LPPER_IO_OPERATION_DATA;
 15 
 16 int  g_iTotalConn = 0;
 17 SOCKET   g_CliSocketArr[MAXIMUM_WAIT_OBJECTS];
 18 WSAEVENT g_CliEventArr[MAXIMUM_WAIT_OBJECTS];
 19 LPPER_IO_OPERATION_DATA g_pPerIODataArr[MAXIMUM_WAIT_OBJECTS];
 20 DWORD WINAPI WorkerThread(LPVOID);
 21 
 22 void Cleanup(int);
 23 
 24 int main()
 25 {
 26  WSADATA  wsaData;
 27  SOCKET   sListen, sClient;
 28  SOCKADDR_IN local, client;
 29  DWORD   dwThreadId;
 30  int   iaddrSize = sizeof(SOCKADDR_IN);
 31 
 32  // Initialize Windows Socket library
 33  WSAStartup(0x0202, &wsaData);
 34 
 35  // Create listening socket
 36  sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 37 
 38  // Bind
 39  local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
 40  local.sin_family = AF_INET;
 41  local.sin_port = htons(PORT);
 42  bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN));
 43 
 44  // Listen
 45  listen(sListen, 3);
 46 
 47  // Create worker thread
 48  CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);
 49 
 50  while (TRUE)
 51  {
 52   // Accept a connection
 53   sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize);
 54   printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
 55   g_CliSocketArr[g_iTotalConn] = sClient;
 56 
 57   // Allocate a PER_IO_OPERATION_DATA structure
 58   g_pPerIODataArr[g_iTotalConn] = (LPPER_IO_OPERATION_DATA)HeapAlloc(
 59    GetProcessHeap(),
 60    HEAP_ZERO_MEMORY,
 61    sizeof(PER_IO_OPERATION_DATA));
 62   g_pPerIODataArr[g_iTotalConn]->Buffer.len = MSGSIZE;
 63   g_pPerIODataArr[g_iTotalConn]->Buffer.buf = g_pPerIODataArr[g_iTotalConn]->szMessage;
 64   g_CliEventArr[g_iTotalConn] = g_pPerIODataArr[g_iTotalConn]->overlap.hEvent = WSACreateEvent();
 65 
 66   // Launch an asynchronous operation
 67   WSARecv(
 68    g_CliSocketArr[g_iTotalConn],
 69    &g_pPerIODataArr[g_iTotalConn]->Buffer,
 70    1,
 71    &g_pPerIODataArr[g_iTotalConn]->NumberOfBytesRecvd,
 72    &g_pPerIODataArr[g_iTotalConn]->Flags,
 73    &g_pPerIODataArr[g_iTotalConn]->overlap,
 74    NULL);
 75   g_iTotalConn++;
 76  }
 77 
 78  closesocket(sListen);
 79  WSACleanup();
 80  
 81  return 0;
 82 }
 83 
 84 DWORD WINAPI WorkerThread(LPVOID lpParam)
 85 {
 86  int   ret, index;
 87  DWORD cbTransferred;
 88  while (TRUE)
 89  {
 90   ret = WSAWaitForMultipleEvents(g_iTotalConn, g_CliEventArr, FALSE, 1000, FALSE);
 91   if (ret == WSA_WAIT_FAILED || ret == WSA_WAIT_TIMEOUT)
 92   {
 93    continue;
 94   }
 95 
 96   index = ret - WSA_WAIT_EVENT_0;
 97   WSAResetEvent(g_CliEventArr[index]);
 98   WSAGetOverlappedResult(
 99    g_CliSocketArr[index],
100    &g_pPerIODataArr[index]->overlap,
101    &cbTransferred,
102    TRUE,
103    &g_pPerIODataArr[g_iTotalConn]->Flags);
104 
105   if (cbTransferred == 0)
106   {
107    // The connection was closed by client
108    Cleanup(index);
109   }
110   else{
111    // g_pPerIODataArr[index]->szMessage contains the received data
112    g_pPerIODataArr[index]->szMessage[cbTransferred] = '\0';
113    send(g_CliSocketArr[index], g_pPerIODataArr[index]->szMessage,\
114     cbTransferred, 0);
115    // Launch another asynchronous operation
116    WSARecv(
117     g_CliSocketArr[index],
118     &g_pPerIODataArr[index]->Buffer,
119     1,
120     &g_pPerIODataArr[index]->NumberOfBytesRecvd,
121     &g_pPerIODataArr[index]->Flags,
122     &g_pPerIODataArr[index]->overlap,
123     NULL);
124   }
125  }
126  return 0;
127 }
128 
129 void Cleanup(int index)
130 {
131  closesocket(g_CliSocketArr[index]);
132  WSACloseEvent(g_CliEventArr[index]);
133  HeapFree(GetProcessHeap(), 0, g_pPerIODataArr[index]);
134  if (index < g_iTotalConn - 1)
135  {
136   g_CliSocketArr[index] = g_CliSocketArr[g_iTotalConn - 1];
137   g_CliEventArr[index] = g_CliEventArr[g_iTotalConn - 1];
138   g_pPerIODataArr[index] = g_pPerIODataArr[g_iTotalConn - 1];
139  }
140  g_pPerIODataArr[--g_iTotalConn] = NULL;
141 }

      这个模型与上述其他模型不同的是它使用Winsock2提供的异步I/O函数WSARecv。在调用WSARecv时,指定一个WSAOVERLAPPED结构,这个调用不是阻塞的,也就是说,它会立刻返回。一旦有数据到达的时候,被指定的WSAOVERLAPPED结构中的hEvent被Signaled。由于下面这个语句 g_CliEventArr[g_iTotalConn] = g_pPerIODataArr[g_iTotalConn]->overlap.hEvent; 使得与该套接字相关联的WSAEVENT对象也被Signaled,所以WSAWaitForMultipleEvents的调用操作成功返回。我们现在应该做的就是用与调用WSARecv相同的WSAOVERLAPPED结构为参数调用WSAGetOverlappedResult,从而得到本次I/O传送的字节数等相关信息。在取得接收的数据后,把数据原封不动的发送到客户端,然后重新激活一个WSARecv异步操作。

      2.用完成例程方式实现的重叠I/O模型

  1 #include <WINSOCK2.H>
  2 #include <stdio.h>
  3 #define PORT 5150
  4 #define MSGSIZE 1024
  5 #pragma comment(lib, "ws2_32.lib")
  6 
  7 typedef struct
  8 {
  9  WSAOVERLAPPED overlap;
 10  WSABUF Buffer;
 11  char szMessage[MSGSIZE];
 12  DWORD NumberOfBytesRecvd;
 13  DWORD Flags;
 14  SOCKET sClient;
 15 }PER_IO_OPERATION_DATA, *LPPER_IO_OPERATION_DATA;
 16 
 17 DWORD WINAPI WorkerThread(LPVOID);
 18 void CALLBACK CompletionROUTINE(DWORD, DWORD, LPWSAOVERLAPPED, DWORD);
 19 SOCKET g_sNewClientConnection;
 20 BOOL g_bNewConnectionArrived = FALSE;
 21 
 22 int main()
 23 {
 24  WSADATA wsaData;
 25  SOCKET sListen;
 26  SOCKADDR_IN local, client;
 27  DWORD dwThreadId;
 28  int iaddrSize = sizeof(SOCKADDR_IN);
 29  // Initialize Windows Socket library
 30  WSAStartup(0x0202, &wsaData);
 31  // Create listening socket
 32  sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 33 
 34  // Bind
 35  local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
 36  local.sin_family = AF_INET;
 37  local.sin_port = htons(PORT);
 38  bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN));
 39 
 40  // Listen
 41  listen(sListen, 3);
 42 
 43  // Create worker thread
 44  CreateThread(NULL, 0, WorkerThread, NULL, 0, &dwThreadId);
 45 
 46  while (TRUE)
 47  {
 48   // Accept a connection
 49   g_sNewClientConnection = accept(sListen, (struct sockaddr *)&client, &iaddrSize);
 50   g_bNewConnectionArrived = TRUE;
 51   printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
 52  }
 53 }
 54 
 55 DWORD WINAPI WorkerThread(LPVOID lpParam)
 56 {
 57  LPPER_IO_OPERATION_DATA lpPerIOData = NULL;
 58  while (TRUE)
 59  {
 60   if (g_bNewConnectionArrived)
 61   {
 62    // Launch an asynchronous operation for new arrived connection
 63    lpPerIOData = (LPPER_IO_OPERATION_DATA)HeapAlloc(
 64     GetProcessHeap(),
 65     HEAP_ZERO_MEMORY,
 66     sizeof(PER_IO_OPERATION_DATA));
 67    lpPerIOData->Buffer.len = MSGSIZE;
 68    lpPerIOData->Buffer.buf = lpPerIOData->szMessage;
 69    lpPerIOData->sClient = g_sNewClientConnection;
 70    WSARecv(lpPerIOData->sClient,
 71     &lpPerIOData->Buffer,
 72     1,
 73     &lpPerIOData->NumberOfBytesRecvd,
 74     &lpPerIOData->Flags,
 75     &lpPerIOData->overlap,
 76     CompletionROUTINE);
 77    g_bNewConnectionArrived = FALSE;
 78   }
 79   SleepEx(1000, TRUE);
 80  }
 81  return 0;
 82 }
 83 
 84 void CALLBACK CompletionROUTINE(DWORD dwError,DWORD cbTransferred,LPWSAOVERLAPPED lpOverlapped,DWORD dwFlags)
 85 {
 86  LPPER_IO_OPERATION_DATA lpPerIOData = (LPPER_IO_OPERATION_DATA)lpOverlapped;
 87  if (dwError != 0 || cbTransferred == 0)
 88  {
 89   // Connection was closed by client
 90   closesocket(lpPerIOData->sClient);
 91   HeapFree(GetProcessHeap(), 0, lpPerIOData);
 92  }
 93  else
 94  {
 95   lpPerIOData->szMessage[cbTransferred] = '\0';
 96   send(lpPerIOData->sClient, lpPerIOData->szMessage, cbTransferred, 0);
 97   // Launch another asynchronous operation
 98   memset(&lpPerIOData->overlap, 0, sizeof(WSAOVERLAPPED));
 99   lpPerIOData->Buffer.len = MSGSIZE;
100   lpPerIOData->Buffer.buf = lpPerIOData->szMessage;
101   WSARecv(lpPerIOData->sClient,
102    &lpPerIOData->Buffer,
103    1,
104    &lpPerIOData->NumberOfBytesRecvd,
105    &lpPerIOData->Flags,
106    &lpPerIOData->overlap,
107    CompletionROUTINE);
108  }
109 }

      用完成例程来实现重叠I/O比用事件通知简单得多。在这个模型中,主线程只用不停的接受连接即可;辅助线程判断有没有新的客户端连接被建立,如果有,就为那个客户端套接字激活一个异步的WSARecv操作,然后调用SleepEx使线程处于一种可警告的等待状态,以使得I/O完成后CompletionROUTINE可以被内核调用。如果辅助线程不调用SleepEx,则内核在完成一次I/O操作后,无法调用完成例程(因为完成例程的运行应该和当初激活WSARecv异步操作的代码在同一个线程之内)。 完成例程内的实现代码比较简单,它取出接收到的数据,然后将数据原封不动的发送给客户端,最后重新激活另一个WSARecv异步操作。注意,在这里用到了“尾随数据”。我们在调用WSARecv的时候,参数lpOverlapped实际上指向一个比它大得多的结构PER_IO_OPERATION_DATA,这个结构除了WSAOVERLAPPED以外,还被我们附加了缓冲区的结构信息,另外还包括客户端套接字等重要的信息。这样,在完成例程中通过参数lpOverlapped拿到的不仅仅是WSAOVERLAPPED结构,还有后边尾随的包含客户端套接字和接收数据缓冲区等重要信息。这样的C语言技巧在我后面介绍完成端口的时候还会使用到。

      五.完成端口模型 “完成端口”模型是迄今为止最为复杂的一种I/O模型。然而,假若一个应用程序同时需要管理为数众多的套接字,那么采用这种模型,往往可以达到最佳的系统性能!但不幸的是,该模型只适用于Windows NT和Windows 2000操作系统。因其设计的复杂性,只有在你的应用程序需要同时管理数百乃至上千个套接字的时候,而且希望随着系统内安装的CPU数量的增多,应用程序的性能也可以线性提升,才应考虑采用“完成端口”模型。要记住的一个基本准则是,假如要为Windows NT或Windows 2000开发高性能的服务器应用,同时希望为大量套接字I/O请求提供服务(Web服务器便是这方面的典型例子),那么I/O完成端口模型便是最佳选择!(节选自《Windows网络编程》第八章) 完成端口模型是我最喜爱的一种模型。虽然其实现比较复杂(其实我觉得它的实现比用事件通知实现的重叠I/O简单多了),但其效率是惊人的。我在T公司的时候曾经帮同事写过一个邮件服务器的性能测试程序,用的就是完成端口模型。结果表明,完成端口模型在多连接(成千上万)的情况下,仅仅依靠一两个辅助线程,就可以达到非常高的吞吐量。下面我还是从代码说起:

  1 #include <WINSOCK2.H>
  2 #include <stdio.h>
  3 #define PORT 5150
  4 #define MSGSIZE 1024
  5 #pragma comment(lib, "ws2_32.lib")
  6 
  7 typedef enum
  8 {
  9  RECV_POSTED
 10 }OPERATION_TYPE;
 11 
 12 typedef struct
 13 {
 14  WSAOVERLAPPED overlap;
 15  WSABUF  Buffer;
 16  char  szMessage[MSGSIZE];
 17  DWORD  NumberOfBytesRecvd;
 18  DWORD  Flags;
 19  OPERATION_TYPE OperationType;
 20 }PER_IO_OPERATION_DATA, *LPPER_IO_OPERATION_DATA;
 21 
 22 DWORD WINAPI WorkerThread(LPVOID);
 23  
 24 int main()
 25 {
 26  WSADATA wsaData;
 27  SOCKET sListen, sClient;
 28  SOCKADDR_IN local, client;
 29  DWORD i, dwThreadId;
 30  int iaddrSize = sizeof(SOCKADDR_IN);
 31  HANDLE CompletionPort = INVALID_HANDLE_VALUE;
 32  SYSTEM_INFO systeminfo;
 33  LPPER_IO_OPERATION_DATA lpPerIOData = NULL;
 34 
 35  // Initialize Windows Socket library
 36  WSAStartup(0x0202, &wsaData);
 37 
 38  // Create completion port
 39  CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
 40 
 41  // Create worker thread
 42  GetSystemInfo(&systeminfo);
 43 
 44  for (i = 0; i < systeminfo.dwNumberOfProcessors; i++)
 45  {
 46   CreateThread(NULL, 0, WorkerThread, CompletionPort, 0, &dwThreadId);
 47  }
 48  // Create listening socket
 49  sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 50 
 51  // Bind
 52  local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
 53  local.sin_family = AF_INET;
 54  local.sin_port = htons(PORT);
 55  bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN));
 56 
 57  // Listen
 58  listen(sListen, 3);
 59 
 60  while (TRUE)
 61  {
 62   // Accept a connection
 63   sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize);
 64   printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
 65   // Associate the newly arrived client socket with completion port
 66   CreateIoCompletionPort((HANDLE)sClient, CompletionPort, (DWORD)sClient, 0);
 67   // Launch an asynchronous operation for new arrived connection
 68   lpPerIOData = (LPPER_IO_OPERATION_DATA)HeapAlloc(
 69    GetProcessHeap(),
 70    HEAP_ZERO_MEMORY,
 71    sizeof(PER_IO_OPERATION_DATA));
 72   lpPerIOData->Buffer.len = MSGSIZE;
 73   lpPerIOData->Buffer.buf = lpPerIOData->szMessage;
 74   lpPerIOData->OperationType = RECV_POSTED;
 75   WSARecv(sClient,
 76    &lpPerIOData->Buffer,
 77    1,
 78    &lpPerIOData->NumberOfBytesRecvd,
 79    &lpPerIOData->Flags,
 80    &lpPerIOData->overlap,
 81    NULL);
 82  }
 83 
 84  PostQueuedCompletionStatus(CompletionPort, 0xFFFFFFFF, 0, NULL);
 85  CloseHandle(CompletionPort);
 86  closesocket(sListen);
 87  WSACleanup();
 88  
 89  return 0;
 90 }
 91 
 92 DWORD WINAPI WorkerThread(LPVOID CompletionPortID)
 93 {
 94  HANDLE CompletionPort=(HANDLE)CompletionPortID;
 95  DWORD dwBytesTransferred;
 96  SOCKET sClient;
 97  LPPER_IO_OPERATION_DATA lpPerIOData = NULL;
 98  while (TRUE)
 99  {
100   GetQueuedCompletionStatus(CompletionPort,
101    &dwBytesTransferred,
102    (PULONG_PTR)&sClient,
103    (LPOVERLAPPED *)&lpPerIOData,
104    INFINITE);
105 
106   if (dwBytesTransferred == 0xFFFFFFFF)
107   {
108    return 0;
109   }
110   if (lpPerIOData->OperationType == RECV_POSTED)
111   {
112    if (dwBytesTransferred == 0)
113    {
114     // Connection was closed by client
115     closesocket(sClient);
116     HeapFree(GetProcessHeap(), 0, lpPerIOData);
117    }
118    else
119    {
120     lpPerIOData->szMessage[dwBytesTransferred] = '\0';
121     send(sClient, lpPerIOData->szMessage, dwBytesTransferred, 0);
122     // Launch another asynchronous operation for sClient
123     memset(lpPerIOData, 0, sizeof(PER_IO_OPERATION_DATA));
124     lpPerIOData->Buffer.len = MSGSIZE;
125     lpPerIOData->Buffer.buf = lpPerIOData->szMessage;
126     lpPerIOData->OperationType = RECV_POSTED;
127     WSARecv(sClient,
128      &lpPerIOData->Buffer,
129      1,
130      &lpPerIOData->NumberOfBytesRecvd,
131      &lpPerIOData->Flags,
132      &lpPerIOData->overlap,
133      NULL);
134    }
135   }
136  }
137  return 0;
138 }

      首先,说说主线程:

      1.创建完成端口对象

      2.创建工作者线程(这里工作者线程的数量是按照CPU的个数来决定的,这样可以达到最佳性能)

      3.创建监听套接字,绑定,监听,然后程序进入循环

      4.在循环中,我做了以下几件事情:

       (1).接受一个客户端连接  

       (2).将该客户端套接字与完成端口绑定到一起(还是调用CreateIoCompletionPort,但这次的作用不同),注意,按道理来讲,此时传递给CreateIoCompletionPort的第三个参数应该是一个完成键,一般来讲,程序都是传递一个单句柄数据结构的地址,该单句柄数据包含了和该客户端连接有关的信息,由于我们只关心套接字句柄,所以直接将套接字句柄作为完成键传递;  

       (3).触发一个WSARecv异步调用,这次又用到了“尾随数据”,使接收数据所用的缓冲区紧跟在WSAOVERLAPPED对象之后,此外,还有操作类型等重要信息。

 

      在工作者线程的循环中,我们

      1.调用GetQueuedCompletionStatus取得本次I/O的相关信息(例如套接字句柄、传送的字节数、单I/O数据结构的地址等等)

      2.通过单I/O数据结构找到接收数据缓冲区,然后将数据原封不动的发送到客户端

      3.再次触发一个WSARecv异步操作。

 

      六.五种I/O模型的比较

      我会从以下几个方面来进行比较

      *有无每线程64连接数限制 如果在选择模型中没有重新定义FD_SETSIZE宏,则每个fd_set默认可以装下64个SOCKET。同样的,受MAXIMUM_WAIT_OBJECTS宏的影响,事件选择、用事件通知实现的重叠I/O都有每线程最大64连接数限制。如果连接数成千上万,则必须对客户端套接字进行分组,这样,势必增加程序的复杂度。 相反,异步选择、用完成例程实现的重叠I/O和完成端口不受此限制。

      *线程数 除了异步选择以外,其他模型至少需要2个线程。一个主线程和一个辅助线程。同样的,如果连接数大于64,则选择模型、事件选择和用事件通知实现的重叠I/O的线程数还要增加。

      *实现的复杂度 我的个人看法是,在实现难度上,异步选择<选择<用完成例程实现的重叠I/O<事件选择<完成端口<用事件通知实现的重叠I/O

      *性能 由于选择模型中每次都要重设读集,在select函数返回后还要针对所有套接字进行逐一测试,我的感觉是效率比较差;完成端口和用完成例程实现的重叠I/O基本上不涉及全局数据,效率应该是最高的,而且在多处理器情形下完成端口还要高一些;事件选择和用事件通知实现的重叠I/O在实现机制上都是采用WSAWaitForMultipleEvents,感觉效率差不多;至于异步选择,不好比较。所以我的结论是:选择<用事件通知实现的重叠I/O<事件选择<用完成例程实现的重叠I/O<完成端口

你可能感兴趣的:(Windows Socket五种I/O模型——代码全攻略)