这篇文章给出Windows网络编程的Socket相关知识。
WinSock的两种I/O模式 与 五种I/O模型。
从最基础的 接受应答 到 选择模型 – 重叠I/O – 完成端口 模型。
两种I/O模式分为 阻塞模式 和 非阻塞模式
阻塞模式:执行I/O操作完成前会一直进行等待,不会将控制权交给程序。套接字默认为阻塞模式的情况下,可以通过多线程来处理。
非阻塞模式:执行I/O操作时,WinSock函数会返回并交出控制权。这种模式比较复杂,因为函数在没有运行完成就进行返回,并会不断地返回 WSAEWOULDBLOCK错误,但是它功能很强大。
为了解决上述非阻塞模式的问题,提出了进行I/O操作的一些I/O模型。
如果在Windows平台上搭建服务器应用,那么I/O模型是必须考虑的。
Windows操作系统提供了 选择(Select)、异步选择(WSAAsynSelect)、事件选择(WSAEventSelect)、重叠I/O(Overlapped I/O) 和 完成端口(Completion Port).
每一种 I/O模型均适用于一种特定的场景。程序员应对自己的应用需求非常明确,而且综合考虑到程序的扩展性 和 可移植性等因素,做出选择。
接下来我们会以一个回射服务器来介绍这五种I/O模型。
首先我们来看看:
零、最普通的回射服务器:
客户端代码如下: 看注释就好了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
#include <Winsock2.h> #include <stdio.h> #pragma comment(lib, "ws2_32.lib") #define SERVER_ADDRESS "127.0.0.1" #define PORT 5150 #define MSGSIZE 1024 char szMessage[MSGSIZE]; int main() { //初始化环境 WSADATA wsaData; WORD wVersionRequested = MAKEWORD(2, 2); int err = WSAStartup(wVersionRequested, &wsaData); if(err != 0) { return 0; } // 创建套接字 SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0); // 绑定地址 SOCKADDR_IN addrSrv; addrSrv.sin_addr.S_un.S_addr = inet_addr(SERVER_ADDRESS); addrSrv.sin_family = AF_INET; addrSrv.sin_port = htons(PORT); //向服务器发送连接请求 connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); while(TRUE) { printf("Send:"); // 从输入获取信息 fgets(szMessage, MSGSIZE, stdin); printf("发送数据...\n"); // 发送数据 send(sockClient, szMessage, strlen(szMessage), 0); printf("发送完毕...\n"); // 接受数据 int ret = recv(sockClient, szMessage, MSGSIZE, 0); szMessage[ret] = '\0'; // 打印信息 printf("Received [%d bytes]: '%s'\n", ret, szMessage); } // Clean up closesocket(sockClient); WSACleanup(); return 0; } |
我们所有的客户端都会使用以上代码。
接下来我们看一下最简单的服务端代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
#include <Winsock2.h> #include <windows.h> #include <stdio.h> #pragma comment(lib, "ws2_32.lib") #define PORT 5150 #define MSGSIZE 1024 char szMessage[MSGSIZE]; int main() { //加载套接字库 WORD wVersionRequested = MAKEWORD(2, 2); WSADATA wsaData; int err = WSAStartup(wVersionRequested, &wsaData); if(err != 0) { return 0; } //创建用于监听的套接字 SOCKET sockSrv = socket(AF_INET, SOCK_STREAM, 0); //绑定套接字 SOCKADDR_IN addrSrv; addrSrv.sin_addr.S_un.S_addr = htonl(INADDR_ANY); addrSrv.sin_family = AF_INET; addrSrv.sin_port = htons(PORT); bind(sockSrv, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR)); //将套接字设置为监听模式,准备接受客户请求。 listen(sockSrv, 1); //等待客户请求到来 SOCKADDR_IN addrClient; int len = sizeof(SOCKADDR); SOCKET sockCoon = accept(sockSrv, (SOCKADDR*)&addrClient, &len); //输出信息 printf("Accepted client:%s:%d\n", inet_ntoa(addrClient.sin_addr), ntohs(addrClient.sin_port)); while(1) { //接收数据 int ret = recv(sockCoon, szMessage, 100, 0); if(ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET)) { // 客户端关闭了 printf("Client socket %d closed.\n", sockCoon); //关闭套接字 closesocket(sockCoon); break; } else { // 收到消息了,再给他发回去。 szMessage[ret] = '\0'; send(sockCoon, szMessage, strlen(szMessage), 0); } } WSACleanup(); return 0; } |
这两段代码都是阻塞模式的…存在的问题就是前面说到的:如果接受不到信息,就会一直阻塞在那里.
而且这里还有一个很大的问题,是Sam跟我说过的,本以为多线程就可以解决,没想到还是存在这种情况:
就是,阻塞的情况下如果接受不到信息,服务端关闭,就会导致客户端崩溃。
操作步骤如下:
- 将服务端的Send代码注释掉
- 打开服务端 – 打开客户端 – 连接上了
- 客户端发送一行数据
- 此时服务端接受到了,但是不会有信息返回
- 客户端就卡在这里一直等待数据
- 此时直接关闭服务端…
- 客户端崩溃了…
大致步骤如上。很危险的。
一、选择模型
Select模型是WinSock中最常见的I/O模型。之所以称其为Select模型,是由于它的“中心思想”便是利用Select函数,实现对I/O的管理。
最初设计该模型时,主要面向的是Unix操作系统的计算机,它们采用的是Berkeley套接字方案。Select模型已经集成到WinSock1.1中,它,采用一种有序的方式,同时对多个套接字的管理。
服务端代码如下:直接看注释就好了…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
#include <Winsock2.h> #include <windows.h> #include <process.h> #include <stdio.h> #pragma comment(lib, "ws2_32.lib") #define PORT 5150 #define MSGSIZE 1024 //总连接数量 int g_iTotalConn = 0; //客户端连接集合 fd_set g_CliSocketArr; //线程函数 unsigned int WINAPI WorkerThread(void* lpParameter); int main() { //初始化环境 WORD wVersionRequested = MAKEWORD(2, 2); WSADATA wsaData; int ret = WSAStartup(wVersionRequested, &wsaData); if(ret != 0) { return 0; } //创建套接字 SOCKET sListen = socket(AF_INET, SOCK_STREAM, 0); //绑定端口 SOCKADDR_IN local; local.sin_addr.S_un.S_addr = htonl(INADDR_ANY); local.sin_family = AF_INET; local.sin_port = htons(PORT); bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN)); //监听 listen(sListen, 20); //创建工作线程 unsigned int dwThreadId; _beginthreadex(NULL, 0, WorkerThread, NULL, 0, &dwThreadId); //接受线程 SOCKADDR_IN client; int iaddrSize = sizeof(SOCKADDR_IN); while(TRUE) { //如果连接数量没有超过最大值 if(g_iTotalConn < FD_SETSIZE) { //接受 SOCKET sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize); //输出信息 printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port)); //把连接放入数组 FD_SET(sClient, &g_CliSocketArr); g_iTotalConn++; } } } unsigned int WINAPI WorkerThread(void* lpParameter) { fd_set fdRead; char szMessage[MSGSIZE]; while(TRUE) { //将套节字集合清空 FD_ZERO(&fdRead); //将所有连接的套接字放入 fdRead = g_CliSocketArr; //检查套节字是否可读 - 会去掉不可读的 int ret = select(0, &fdRead, NULL, NULL, NULL); for(int i = 0; i < g_iTotalConn; i++) { //检查这个套接字是否在可读的集合里面 if(FD_ISSET(g_CliSocketArr.fd_array[i], &fdRead)) { ret = recv(g_CliSocketArr.fd_array[i], szMessage, MSGSIZE, 0); if(ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET)) { // 客户端关闭了 printf("Client socket %d closed.\n", g_CliSocketArr); //关闭套接字 closesocket(g_CliSocketArr.fd_array[i]); //连接数减一 g_iTotalConn--; //从连接套接字中清理出去 FD_CLR(g_CliSocketArr.fd_array[i], &g_CliSocketArr); } else { // 收到消息了,再给他发回去。 szMessage[ret] = '\0'; send(g_CliSocketArr.fd_array[i], szMessage, strlen(szMessage), 0); } } } } return 0; } |
这里可能不好理解的是 fd_set 及其相关的操作。
1 2 3 4 5 6 7 8 9 10 |
fd_set set; FD_ZERO(&set);//将你的套节字集合清空 SOCKET s = ... FD_SET(s, &set);//加入你感兴趣的套节字到集合,这里是一个读数据的套节字s select(0, &set, NULL, NULL, NULL);//检查套节字是否可读, //很多情况下就是 是否有数据(注意,只是说很多情况) //这里select是否出错没有写 if(FD_ISSET(s, &set)) //检查s是否在这个集合里面 { } |
该模型有个最大的缺点就是,它需要一个死循环不停的去遍历所有的客户端套接字集合,询问是否有数据到来,这样,如果连接的客户端很多,势必会影响处理客户端请求的效率,但它的优点就是解决了每一个客户端都去开辟新的线程与其通信的问题。
如果有一个模型,可以不用去轮询客户端套接字集合,而是等待系统通知,当有客户端数据到来时,系统自动的通知我们的程序,这就解决了select模型带来的问题了。
二、异步消息选择
WsaAsyncSelect模型就是这样一个解决了普通select模型问题的异步I/O模型。利用这个模型应用程序可以在一个套接字上接受以Windows消息为基础的网络事件通知。当有客户端数据到来时,系统发送消息给我们的程序,我们的程序只要定义好消息的处理方法就可以了,用到的函数主要是WSAAsyncSelect.
该模型被MFC中的CSocket对象所采纳。
主要实现思路如下:
- 首先我们定义一个消息,告诉系统当有客户端消息到达的时候,发送该消息通知我们。
- 然后在消息处理函数里面添加对消息的处理即可。
实现代码如下: – 老规矩,看代码。VS2013-Win32项目,应该是直接编译运行的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
#include <WinSock2.h> #include <Windows.h> #pragma comment(lib, "ws2_32.lib") #define PORT 5150 #define MSGSIZE 1024 #define WM_SOCKET WM_USER + 0 //定义消息处理函数 LRESULT CALLBACK WndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ); //这个是Main函数入口 - 如果看不懂的话,说明需要补一些Win32知识 INT WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, INT iCmdShow ) { //创建窗口类 WNDCLASS wndcls; wndcls.cbClsExtra = 0; wndcls.cbWndExtra = 0; wndcls.hbrBackground = (HBRUSH)GetStockObject(COLOR_WINDOWFRAME); wndcls.hCursor = LoadCursor(NULL, IDC_ARROW); wndcls.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndcls.hInstance = hInstance; wndcls.lpfnWndProc = WndProc; wndcls.lpszClassName = TEXT("NormalWindow"); wndcls.lpszMenuName = NULL; wndcls.style = CS_HREDRAW | CS_VREDRAW; //注册窗口类 ATOM nAtom = 0; nAtom = RegisterClass(&wndcls); if(nAtom == 0) { //如果注册失败 MessageBox(NULL, TEXT("Register Failed"), TEXT("Error"), MB_OK | MB_ICONERROR); return FALSE; } //创建窗口 HWND hWnd; hWnd = CreateWindow(TEXT("NormalWindow"), TEXT("AsynSelectSrv"), WS_OVERLAPPEDWINDOW, 300, 300, 600, 400, NULL, NULL, hInstance, NULL); if(hWnd == NULL) { //如果创建失败,发送一个退出消息 MessageBox(NULL, TEXT("Create Window Failed"), TEXT("Error"), MB_OK | MB_ICONERROR); PostQuitMessage(0); } //显示窗口 ShowWindow(hWnd, SW_SHOWNORMAL); //刷新窗口 UpdateWindow(hWnd); //消息循环 BOOL bRet; MSG msg; //获取消息 while(bRet = GetMessage(&msg, hWnd, 0, 0)) { if(bRet == -1) { // 处理错误和可能的退出 // handle the error and possibly exit return -1; } else { //将虚拟键消息转换为字符消息 TranslateMessage(&msg); //分发一个消息给窗口程序 DispatchMessage(&msg); } } return msg.wParam; } //消息处理函数的实现 LRESULT CALLBACK WndProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam ) { //这些都是Socket编程的一些变量 WORD wVersionRequested; //Socket版本 WSADATA wsaData; int ret; static SOCKET sListen; //static - 这样我们每次进入这个函数就是同一个监听套接字啦~ SOCKADDR_IN local; //这些是和客户端Sock相关的 SOCKADDR_IN client; int iAddrSize = sizeof(client); SOCKET sClient; char szMessage[MSGSIZE]; //1KB //这两个是显示消息用的 wchar_t MessageOutPut[50]; wchar_t Temp[20]; switch(uMsg) { case WM_CREATE: //窗口创建完后会发出这个消息 //在这里进行套接字的初始化 //初始化环境 wVersionRequested = MAKEWORD(2, 2); ret = WSAStartup(wVersionRequested, &wsaData); if(ret != 0) { return 0; } //创建监听套接字 sListen = socket(AF_INET, SOCK_STREAM, 0); //绑定端口 local.sin_addr.S_un.S_addr = htonl(INADDR_ANY); local.sin_family = AF_INET; local.sin_port = htons(PORT); bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN)); //监听 listen(sListen, 20); //设置为异步模式 WSAAsyncSelect(sListen, hWnd, WM_SOCKET, FD_ACCEPT); break; case WM_SOCKET: //接收到我们指定的消息了 if(WSAGETSELECTERROR(lParam)) { closesocket(wParam); break; } switch(WSAGETSELECTEVENT(lParam)) { case FD_ACCEPT: //处理可以接受连接消息 //接受来自客户端的连接 // Accept a connection from client sClient = accept(sListen, (struct sockaddr *)&client, &iAddrSize); //为了显示有连接,用个 MessageBox 显示下 MultiByteToWideChar(0, 0, inet_ntoa(client.sin_addr), strlen(inet_ntoa(client.sin_addr)) + 1, Temp, 50); wsprintf(MessageOutPut, TEXT("Accepted client - %s : %d"), Temp, ntohs(client.sin_port)); MessageBox(hWnd, MessageOutPut, TEXT("有连接到来"), MB_OK); // 使 客户端套接字与 FD_READ 和 FD_CLOSE 事件 相关联 // Associate client socket with FD_READ and FD_CLOSE event WSAAsyncSelect(sClient, hWnd, WM_SOCKET, FD_READ | FD_CLOSE); break; case FD_READ: //处理可以读取数据消息 //读取数据 ret = recv(wParam, szMessage, MSGSIZE, 0); if(ret == 0 || ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET) { //如果出现了错误,则关闭套接字 closesocket(wParam); } else { //回射信息 szMessage[ret] = '\0'; send(wParam, szMessage, strlen(szMessage), 0); } break; case FD_CLOSE: //处理对端关闭连接消息 closesocket(wParam); //关闭与客户端套接字的连接 break; } break; case WM_CLOSE: //关闭按钮消息 if(IDYES == MessageBox(hWnd, TEXT("是否真的结束?"), TEXT("Message"), MB_YESNO)) { DestroyWindow(hWnd); } break; case WM_DESTROY://当窗口销毁时,会调用WinSunProc closesocket(sListen); //关闭监听套接字 WSACleanup(); //网络环境清理 PostQuitMessage(0); //发送一个退出消息 break; default: //调用默认处理 return DefWindowProc(hWnd, uMsg, wParam, lParam); } return 0; } |
先运行服务端,再运行客户端即可。
这个模型其实是比较简单的,基于Win32窗口程序:
- 在 WM_CREATE 消息处理函数中,初始化 Windows Socket library,创建监听套接字,绑定,监听,并且调用WSAAsyncSelect函数表示我们关心在监听套接字上发生的FD_ACCEPT事件;
- 自定义一个消息WM_SOCKET,一旦在我们所关心的套接字(监听套接字和客户端套接字)上发生了某个事件,系统就会调用 WndProc 并且 message 参数被设置为 WM_SOCKET ;
- 在 WM_SOCKET 的消息处理函数中,分别对 FD_ACCEPT、FD_READ 和FD_CLOSE 事件进行处理;
- 在窗口销毁消息(WM_DESTROY)的处理函数中,我们关闭监听套接字,清除Windows Socket library
这样就完美了。下面表列出了一些网络事件类型。
网络事件 | 含义 |
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 | 接收针对套接字的协议家族,本地地址列表发生变化的通知 |
以上常用的一般就是 READ、WRITE、ACCEPT、CONNECT这四个了。
异步消息选择 解决了普通select模型的问题,但是它最大的缺点就是它只能用在windows程序上,因为它需要一个接收系统消息的窗口句柄,那么有没有一个模型既可以解决select模型的问题,又不限定只能是windows程序才能用呢?
三、WsaEventSelect模型
继续更新哈…
Winsock 提供了另一个有用的异步I/O模型 – WsaEventSelect 模型。和WSAAsyncSelect模型类似的是,它也允许应用程序在一个或多个套接字上,接收以事件为基础的网络事件通知。对于上面的表中总结的-由WSAAsyncSelect模型采用的网络事件来说,这些事件均可原封不动地移植到新模型。在用新模型开发的应用程序中,也能接收和处理所有那些事件。该模型最主要的差别在于网络事件会投递至一个事件对象句柄,而非投递至一个窗口例程。
事件选择模型不用主动去轮询所有客户端套接字是否有数据到来的模型,它也是在客户端有数据到来时,系统发送通知给我们的程序,但是,它不是发送消息,而是通过事件的方式来通知我们的程序,这就解决了WsaAsyncSelect模型只能用在Win32窗口程序的问题。
该模型的实现,我们也可以开辟两个线程来进行处理,一个用来接收客户端的连接请求,一个用来与客户端进行通信,用到的主要函数有WSAEventSelect,WSAWaitForMultipleEvents,WSAEnumNetworkEvents.
还是一样的,看代码吧…注释很清晰的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
#include <Winsock2.h> #include <process.h> #include <stdio.h> #pragma comment(lib,"ws2_32.lib") #define PORT 5150 #define MSGSIZE 1024 int g_iTotalConnect = 0; //客户端连接数组 SOCKET g_ClientSocketArr[MAXIMUM_WAIT_OBJECTS]; //通知事件数组 WSAEVENT g_ClientEventArr[MAXIMUM_WAIT_OBJECTS]; //线程函数 unsigned int WINAPI WorkerThread(void* pParam); //清理函数 void Cleanup(int index); int main() { //初始化环境 WORD wVersionRequested = MAKEWORD(2, 2); WSADATA wsaData; int ret = WSAStartup(wVersionRequested, &wsaData); if(ret != 0) { return 0; } //创建套接字 SOCKET sListen = socket(AF_INET, SOCK_STREAM, 0); //绑定端口 SOCKADDR_IN local; local.sin_addr.S_un.S_addr = htonl(INADDR_ANY); local.sin_family = AF_INET; local.sin_port = htons(PORT); bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN)); //监听 listen(sListen, 20); //创建工作线程 unsigned int dwThreadId; _beginthreadex(NULL, 0, WorkerThread, NULL, 0, &dwThreadId); //接受线程 SOCKET sClient; SOCKADDR_IN client; int iaddrSize = sizeof(SOCKADDR_IN); while(TRUE) { //如果连接数量没有超过最大值 if(g_iTotalConnect < MAXIMUM_WAIT_OBJECTS) { //接受 sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize); //输出信息 printf("Accepted client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port)); //把连接放入数组 //客户端连接 g_ClientSocketArr[g_iTotalConnect] = sClient; //创建事件 g_ClientEventArr[g_iTotalConnect] = WSACreateEvent(); //将连接与事件绑定 - 处理可读与关闭事件 WSAEventSelect(g_ClientSocketArr[g_iTotalConnect], g_ClientEventArr[g_iTotalConnect], FD_READ | FD_CLOSE); //连接数量 + 1 g_iTotalConnect++; } } return 0; } unsigned int WINAPI WorkerThread(void* pParam) { WSANETWORKEVENTS NetworkEvents; char szMessage[MSGSIZE]; while(TRUE) { // 等待任意一个事件对象 int ret = WSAWaitForMultipleEvents(g_iTotalConnect, g_ClientEventArr, FALSE, 1000, FALSE); if(ret == WSA_WAIT_FAILED || ret == WSA_WAIT_TIMEOUT) { continue; } // 根据返回值确定是哪一个事件对象被触发 int index = ret - WSA_WAIT_EVENT_0; //然后判断是哪一个网络事件触发了事件对象 WSAEnumNetworkEvents(g_ClientSocketArr[index], g_ClientEventArr[index], &NetworkEvents); // 根据位运算判断能否接收数据 if(NetworkEvents.lNetworkEvents & FD_READ) { // 从客户端接收信息 // Receive message from client ret = recv(g_ClientSocketArr[index], szMessage, MSGSIZE, 0); if(ret == 0 || (ret == SOCKET_ERROR && WSAGetLastError() == WSAECONNRESET)) { Cleanup(index); } else { // 回射 szMessage[ret] = '\0'; send(g_ClientSocketArr[index], szMessage, strlen(szMessage), 0); } } // 如果对端关闭的话 if(NetworkEvents.lNetworkEvents & FD_CLOSE) { Cleanup(index); } } return 0; } void Cleanup(int index) { // 关闭套接字 closesocket(g_ClientSocketArr[index]); // 清理事件对象 WSACloseEvent(g_ClientEventArr[index]); // 数组内元素删除了,要移动一下 if(index < g_iTotalConnect - 1) { g_ClientSocketArr[index] = g_ClientSocketArr[g_iTotalConnect - 1]; g_ClientEventArr[index] = g_ClientEventArr[g_iTotalConnect - 1]; } // 连接数量减一 g_iTotalConnect--; } |
当然这里要对几个函数进行讲解。
WSAEventSelelct 函数:将Socket套接字与事件对象关联,并制定需要关注的网络事件,一旦在某个套接字上发生了我们关注的事件(FD_READ 和 FD_CLOSE),与之相关联的 WSAEVENT 对象被触发。
1 2 3 4 |
int WSAAPI WSAEventSelect( SOCKET s, WSAEVENT hEventObject, long lNetworkEvents); |
三个参数 第一个参数指定套接字,第二个参数指定事件对象,第三个字段是前面的网络事件表中的类型。
WSAWaitForMultipleEvent函数:等待事件对象被触发。
1 2 3 4 5 6 |
DWORD WSAAPI WSAWaitForMultipleEvents( DWORD cEvents, const WSAEVENT FAR * lphEvents, BOOL fWaitAll, DWORD dwTimeout, BOOL fAlertable); |
五个参数:
- 第一个参数是事件对象的数量
- 第二个参数是事件对象句柄的数组
- 第三个参数指定是等待全部还是任意一个
- 第四个参数是等待时间
- 第五个参数是指定函数返回时是否执行完成例程
WSAResetEvent 函数就不讲了,重置事件。
WSAEnumNetworkEvents – 判断是哪个网络事件触发了对象
1 2 3 4 5 |
int WSAAPI WSAEnumNetworkEvents( SOCKET s, WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents ); |
三个参数:
- 套接字
- 事件对象
- 网络事件对象
大致就是这四个函数。
这里要注意的是,我并没有在各个函数调用后面进行成功与否的判断,这是比较危险的。当然为了代码的简洁性我也尽量去掉了这些东西,大家如果是真正的做服务器还是需要考虑到这一点的。
事件选择模型通过一个死循环里面调用 WSAWaitForMultipleEvents 函数来等待客户端套接字对应的事件的到来,一旦事件通知到达,就通过该套接字去接收数据。虽然WsaEventSelect模型的实现较前两种方法复杂,但它在效率和兼容性方面是最好的。
值得注意的是以上三种选择模型虽然在效率方面有了不少的提升,但它们都存在一个问题,就是都预设了只能接收64个客户端连接,虽然我们在实现时可以不受这个限制(数组,循环判断),但是那样,它们所带来的效率提升又将打折扣,那又有没有什么模型可以解决这个问题呢?
【WinSock】网络编程 – I/O模型(二) – 重叠I/O