原书名《Network.Programming.for.Windows》第八章
一、套接字模式:
windows套接字在两种模式下执行I/O操作,锁定和非锁定。
1.1锁定模式
该模式下,在I/O操作完成之前,执行的winsock函数(如recv和send)会一直等候下去,不会立即将控制权交还给应用程序。
锁定套接字上调用的winsock函数都会耗费或长或短的“等待”时间,大多数winsock应用都是遵照“生产者-消费者”模型来编制的,在这种模型中,应用程序需要读取或写入指定数量的字节,然后以它为基础执行一些计算。比如经常会看到这样的代码段:
SOCKET sock; char buf[2048] = {0}; bool bDone = false; ... while (!bDone) { iCount = recv(sock, buf, 128); if (iCount == SOCKET_ERROR) { // do sth error return ; } //do job }
最难受的地方在于事前并不知道还能读取多少数据,recv也有可能一直无返回以至于达不到跳出循环的条件、
当然,还有可能会在recv中使用MSG_ PEEK标志,或者调用ioctlsocket(设置FIONREAD选项)去“偷看”缓冲区是否存在足够的字节数。《windows网络编程》中相关介绍是不推荐这种方式,一个是会增加无谓的开销,一个是非良好编程习惯。通常避免由于recv等不到数据产生的锁定状态,我们会将该接收数据过程置于一个独立线程中,在另一个线程中等待数据到达的通知之后再做相应处理。
1.2非锁定模式
该模式下,winsock函数无论何时都能立即返回。
其实就是在锁定套接字的基础上使用ioctlsocket函数将一个锁定套接字置为非锁定模式。基本用法:
unsigned long ul = 1; int iRet = ioctlsocket(sock, FIONBIO, &ul);
这里主要用到函数ioctlsocket,使用它来控制套接字的模式。其原型如下:
1 int ioctlsocket( 2 _In_ SOCKET s, 3 _In_ long cmd, 4 _Inout_ u_long *argp 5 );
- s,套接字句柄
- cmd,需要设置的命令
- argp,cmd命令的参数列表
- return,成功返回0, 否则返回SOCKET_ERROR,通过WSAGetLastError获取错误信息
其中,cmd的命令项及参数如下:
FIONBIO - 0,关闭套接字非锁定
- 非0值,启用套接字非锁定
注:WSAAsyncSelect和WSAEventSelect会自动将套接字置为非锁定模式,此时再通过ioctlsocket调用想将套接字设置回锁定模式会失败,返回WSAEINVAL;想要这个调用成功,需要先调用WSAAsyncSelect传入lEvent参数为0来关闭WSAAsyncSelect模型,或者调用WSAEventSelect传入lNetworkEvents参数为0来关闭WSAEventSelect模型。
FIONREAD - 作为出参存储缓存中未读取数据字节数
SIOCATMARK - ,作为OOD(带外数据)情况下使用
这样将一个套接字置为非锁定模式后,winsock的API调用会立即返回。大多数情况下,这些调用都会“失败”,并返回一个WSAEWOULDBLOCK的错误。含义是请求的操作在调用期间没有时间完成。这种情形下,通常需要重复调用一个函数直至获得一个成功返回代码,但是并不意味着轮询直至成功,这样的话开销与前面说的“偷看”缓冲区相比,没有任何优势可言。
锁定套接字和非锁定套接字模式都存在各自的优点和缺点。从概念的角度来说,锁定套接字更容易使用。但是当应用场景为多链接,数据收发不均匀、时间不定时,难以管理。另一方面,如果需要对winsock的WSAEWOULDBLOCK错误加以应对,非锁定套接字会让人抓狂。由此,便产生了“套接字I/O模型”,有助于程序通过异步方式,同时对一个或者多个套接字上的通信进行管理。
二、套接字I/O模型
一共存在五种类型的套接字I/O模型,包括select(选择)、WSAAsynSelect(异步选择)、WSAEventSelect(事件选择)、overlapped(重叠)以及completion port(完成端口)。
2.1 select模型
函数原型如下:
1 int select( 2 _In_ int nfds, 3 _Inout_ fd_set *readfds, 4 _Inout_ fd_set *writefds, 5 _Inout_ fd_set *exceptfds, 6 _In_ const struct timeval *timeout 7 );
- nfds,忽略,仅为保持与早期Berkeley套接字兼容
- readfds,检查可读性套接字集合;
- 有数据可以读入(包括带外数据,如果SO_OOBINLINE启用)
- 连接已经关闭、重设或终止
- 如果已经调用listen,而且一个连接正在建立,那么accpet调用会成功
- writefds,检查可写性套接字集合
- 有数据可以发出
- 如果已经完成对一个非锁定连接调用的处理,连接就会成功
- exceptfds,用于例外数据套接字集合
- 假如已完成对一个连接调用的处理,连接尝试就会失败
- 有带外(Out of band,OOD)数据可供读取
- timeout,指针,指向一个timeval结构,用于决定select最多等待I/O操作完成多久时间
- nullptr,select调用无限等待
- (0,0),即传入,select调用会立即返回
- 其他情况,select调用等待指定时长,要么有套接字有响应,要么超时返回
- 返回值,
- 0,等待超时
- SOCKET_ERROR,有错误发生
- 其他值,返回集合中准备好待决套接字的数量
PS:
- readfds,writefds,exceptfds三个参数中,至少要有一个非空,不然select的等待就没有意义,当然,三个值都为nullptr的时候,函数直接返回-1;
- select返回后,会修改传入的三个集合,删除那些不存在待决I/O操作的套接字句柄;如果要重复监听,则在下一次调用select之前还需要重新加入集合;
相关数据结构:
fd_set,本身是一个socket套接字数组,结构如下:
1 typedef struct fd_set { 2 u_int fd_count; /* how many are SET? */ 3 SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */ 4 } fd_set;
其中FD_SETSIZE宏定义为64。这个就是前面提到的所谓套接字集合的参数。有相关的四个操作宏:
1 void FD_ZERO(fd_set *fdset); //清空集合 2 void FD_SET(int fd, fd_set *fdset); //将一个给定的文件描述符加入集合之中 3 void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除 4 int FD_ISSET(int fd, fd_set *fdset); //检查集合中指定的文件描述符是否可以读写
timeval,等待时间结构体,结构如下:
struct timeval { long tv_sec; /* seconds */ long tv_usec; /* and microseconds 微秒*/};
服务端实例(引用自《windows网络编程》随书源码)如下:
1 #include <winsock2.h> 2 #include <windows.h> 3 #include <stdio.h> 4 5 #define PORT 5150 6 #define DATA_BUFSIZE 8192 7 8 typedef struct _SOCKET_INFORMATION { 9 CHAR Buffer[DATA_BUFSIZE]; 10 WSABUF DataBuf; 11 SOCKET Socket; 12 OVERLAPPED Overlapped; 13 DWORD BytesSEND; 14 DWORD BytesRECV; 15 } SOCKET_INFORMATION, * LPSOCKET_INFORMATION; 16 17 BOOL CreateSocketInformation(SOCKET s); 18 void FreeSocketInformation(DWORD Index); 19 20 DWORD TotalSockets = 0; 21 LPSOCKET_INFORMATION SocketArray[FD_SETSIZE]; 22 23 void main(void) 24 { 25 SOCKET ListenSocket; 26 SOCKET AcceptSocket; 27 SOCKADDR_IN InternetAddr; 28 WSADATA wsaData; 29 INT Ret; 30 FD_SET WriteSet; 31 FD_SET ReadSet; 32 DWORD i; 33 DWORD Total; 34 ULONG NonBlock; 35 DWORD Flags; 36 DWORD SendBytes; 37 DWORD RecvBytes; 38 39 40 if ((Ret = WSAStartup(0x0202,&wsaData)) != 0) 41 { 42 printf("WSAStartup() failed with error %d\n", Ret); 43 WSACleanup(); 44 return; 45 } 46 47 // Prepare a socket to listen for connections. 48 49 if ((ListenSocket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, 50 WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET) 51 { 52 printf("WSASocket() failed with error %d\n", WSAGetLastError()); 53 return; 54 } 55 56 InternetAddr.sin_family = AF_INET; 57 InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY); 58 InternetAddr.sin_port = htons(PORT); 59 60 if (bind(ListenSocket, (PSOCKADDR) &InternetAddr, sizeof(InternetAddr)) 61 == SOCKET_ERROR) 62 { 63 printf("bind() failed with error %d\n", WSAGetLastError()); 64 return; 65 } 66 67 if (listen(ListenSocket, 5)) 68 { 69 printf("listen() failed with error %d\n", WSAGetLastError()); 70 return; 71 } 72 73 // Change the socket mode on the listening socket from blocking to 74 // non-block so the application will not block waiting for requests. 75 76 NonBlock = 1; 77 if (ioctlsocket(ListenSocket, FIONBIO, &NonBlock) == SOCKET_ERROR) 78 { 79 printf("ioctlsocket() failed with error %d\n", WSAGetLastError()); 80 return; 81 } 82 83 while(TRUE) 84 { 85 // Prepare the Read and Write socket sets for network I/O notification. 86 FD_ZERO(&ReadSet); 87 FD_ZERO(&WriteSet); 88 89 // Always look for connection attempts. 90 91 FD_SET(ListenSocket, &ReadSet); 92 93 // Set Read and Write notification for each socket based on the 94 // current state the buffer. If there is data remaining in the 95 // buffer then set the Write set otherwise the Read set. 96 97 for (i = 0; i < TotalSockets; i++) 98 if (SocketArray[i]->BytesRECV > SocketArray[i]->BytesSEND) 99 FD_SET(SocketArray[i]->Socket, &WriteSet); 100 else 101 FD_SET(SocketArray[i]->Socket, &ReadSet); 102 103 if ((Total = select(0, &ReadSet, &WriteSet, NULL, NULL)) == SOCKET_ERROR) 104 { 105 printf("select() returned with error %d\n", WSAGetLastError()); 106 return; 107 } 108 109 // Check for arriving connections on the listening socket. 110 if (FD_ISSET(ListenSocket, &ReadSet)) 111 { 112 Total--; 113 if ((AcceptSocket = accept(ListenSocket, NULL, NULL)) != INVALID_SOCKET) 114 { 115 116 // Set the accepted socket to non-blocking mode so the server will 117 // not get caught in a blocked condition on WSASends 118 119 NonBlock = 1; 120 if (ioctlsocket(AcceptSocket, FIONBIO, &NonBlock) == SOCKET_ERROR) 121 { 122 printf("ioctlsocket() failed with error %d\n", WSAGetLastError()); 123 return; 124 } 125 126 if (CreateSocketInformation(AcceptSocket) == FALSE) 127 return; 128 129 } 130 else 131 { 132 if (WSAGetLastError() != WSAEWOULDBLOCK) 133 { 134 printf("accept() failed with error %d\n", WSAGetLastError()); 135 return; 136 } 137 } 138 } 139 140 // Check each socket for Read and Write notification until the number 141 // of sockets in Total is satisfied. 142 143 for (i = 0; Total > 0 && i < TotalSockets; i++) 144 { 145 LPSOCKET_INFORMATION SocketInfo = SocketArray[i]; 146 147 // If the ReadSet is marked for this socket then this means data 148 // is available to be read on the socket. 149 150 if (FD_ISSET(SocketInfo->Socket, &ReadSet)) 151 { 152 Total--; 153 154 SocketInfo->DataBuf.buf = SocketInfo->Buffer; 155 SocketInfo->DataBuf.len = DATA_BUFSIZE; 156 157 Flags = 0; 158 if (WSARecv(SocketInfo->Socket, &(SocketInfo->DataBuf), 1, &RecvBytes, 159 &Flags, NULL, NULL) == SOCKET_ERROR) 160 { 161 if (WSAGetLastError() != WSAEWOULDBLOCK) 162 { 163 printf("WSARecv() failed with error %d\n", WSAGetLastError()); 164 165 FreeSocketInformation(i); 166 } 167 168 continue; 169 } 170 else 171 { 172 SocketInfo->BytesRECV = RecvBytes; 173 174 // If zero bytes are received, this indicates the peer closed the 175 // connection. 176 if (RecvBytes == 0) 177 { 178 FreeSocketInformation(i); 179 continue; 180 } 181 } 182 } 183 184 185 // If the WriteSet is marked on this socket then this means the internal 186 // data buffers are available for more data. 187 188 if (FD_ISSET(SocketInfo->Socket, &WriteSet)) 189 { 190 Total--; 191 192 SocketInfo->DataBuf.buf = SocketInfo->Buffer + SocketInfo->BytesSEND; 193 SocketInfo->DataBuf.len = SocketInfo->BytesRECV - SocketInfo->BytesSEND; 194 195 if (WSASend(SocketInfo->Socket, &(SocketInfo->DataBuf), 1, &SendBytes, 0, 196 NULL, NULL) == SOCKET_ERROR) 197 { 198 if (WSAGetLastError() != WSAEWOULDBLOCK) 199 { 200 printf("WSASend() failed with error %d\n", WSAGetLastError()); 201 202 FreeSocketInformation(i); 203 } 204 205 continue; 206 } 207 else 208 { 209 SocketInfo->BytesSEND += SendBytes; 210 211 if (SocketInfo->BytesSEND == SocketInfo->BytesRECV) 212 { 213 SocketInfo->BytesSEND = 0; 214 SocketInfo->BytesRECV = 0; 215 } 216 } 217 } 218 } 219 } 220 } 221 222 BOOL CreateSocketInformation(SOCKET s) 223 { 224 LPSOCKET_INFORMATION SI; 225 226 printf("Accepted socket number %d\n", s); 227 228 if ((SI = (LPSOCKET_INFORMATION) GlobalAlloc(GPTR, 229 sizeof(SOCKET_INFORMATION))) == NULL) 230 { 231 printf("GlobalAlloc() failed with error %d\n", GetLastError()); 232 return FALSE; 233 } 234 235 // Prepare SocketInfo structure for use. 236 237 SI->Socket = s; 238 SI->BytesSEND = 0; 239 SI->BytesRECV = 0; 240 241 SocketArray[TotalSockets] = SI; 242 243 TotalSockets++; 244 245 return(TRUE); 246 } 247 248 void FreeSocketInformation(DWORD Index) 249 { 250 LPSOCKET_INFORMATION SI = SocketArray[Index]; 251 DWORD i; 252 253 closesocket(SI->Socket); 254 255 printf("Closing socket number %d\n", SI->Socket); 256 257 GlobalFree(SI); 258 259 // Squash the socket array 260 261 for (i = Index; i < TotalSockets; i++) 262 { 263 SocketArray[i] = SocketArray[i + 1]; 264 } 265 266 TotalSockets--; 267 }
实例中有一些细节:
- 事实上第一次select等待的仅有readfds这个集合,writefds集合是空的,第一次select调用成功的返回必然是有客户端连接进来了;
- 无论是listen用的监听套接字还是后续<监听套接字其实视情况而定它的是否锁定,上述例程中select函数本身就直接给等待时间NULL无限等待,其次后续connect触发这个套接字活动时,必然会有一次accept操作成功,所以我的意见是这里不必设置非锁定状态>accept接受到的客户通信套接字,都通过ioctlsocket函数将套接字置为非锁定模式,防止在后续select过程中阻塞;
- 需要自己对每次返回的所有套接字数量、客户机通信套接字数组进行维护;
- 每次调用select之前重置套接字集合及重新置入要select的套接字。
整体来讲,select操作步骤挺简单的,就下面这五步:
- 使用FD_ZERO宏,将自己要使用的套接字集合初始化;
- 使用FD_SET宏,将要操作的套接字置入对应的集合中;
- 调用select函数,等待在指定的套接字集合中的I/O活动设置好一个或多个套接字句柄,select完成后会返回所有集合中套接字句柄的总数,并对每个集合进行相应的更新;
- 根据返回值,可以通过FD_ISSET宏判断哪些集合中的哪些套接字上有待决的I/O操作;
- 根据FD_ISSET判断的结果对待决I/O操作进行相应的处理完毕后,跳转至步骤1,继续进行select处理。
整个select模型作为I/O模型的基础版本,一些简单的服务端例程在使用的时候还是比较好控制,而且不必产生额外的监听线程及维护等开销,轻量级还不错。
2.2 WSAAsynSelect 异步选择
这个模型可以使应用程序在一个套接字上接收以windows消息为基础的网络事件通知。
当使用这个模型时,需要有一个windows标准的窗口程序,比如我们使用CreateWindow函数创建一个win32窗体,再为该窗体提供一个窗口例程支持函数(WinProc)。
WSAAsynSelect函数原型如下:
1 int WSAAsyncSelect( 2 _In_ SOCKET s, 3 _In_ HWND hWnd, 4 _In_ unsigned int wMsg, 5 _In_ long lEvent);
- s,请求网络事件消息的套接字
- hWnd,接收网络事件消息的windows窗口句柄
- wMsg,窗口收到的网络事件消息
- 通常会设置为WM_USER+X的形式,定义用户消息
- lEvent,感兴趣的网络事件消息位掩码,所有需要的事件需要一次注册完毕。后续除非closesocket或者应用程序重新为该套接字调用WSAAsyncSelect更改注册事件,否则事件通知永远有效。
- FD_READ-套接字s已经可读
- FD_WRITE-套接字s已经可写
- FD_ACCEPT-套接字s准备好接受一个新的连接
- FD_CONNECT-套接字s上的连接或多点连接操作完成
- FD_CLOSE-套接字s对应的连接已经关闭
- FD_OOB-准备读取套接字上的带外数据
- FD_QOS-套接字的网络质量服务改变
- FD_GROUP_QOS-套接字组的网络质量服务改变
- FD_ROUTING_INTERFACE_CHANGE-本地接口发送目标改变
- FD_ADDRESS_LIST_CHANGE-套接字绑定的本地地址列表发生改变
- 返回值,
- 0,成功
- SOCKET_ERROR,通过WSAGetLastError取错误信息
使用步骤:
- 创建一个windows窗口,调用WSAAsyncSelect设置一个套接字感兴趣的网络事件消息;
- 窗口例程中对自定义消息进行捕获,WSAGETSELECTERROR宏判断是否出错,出错则WSAGETSELECTERROR宏取得错误码;
- WSAGETSELECTEVENT宏取得事件,进行相应的处理;
- 处理完毕后继续步骤2。
最后一个问题是应用程序对FD_WRITE事件通知进行的处理。只有三种情况下才会发出FD_WRITE消息:
- 使用connect或者WSAConnect,一个套接字首次建立连接
- 使用accept或者WSAAccept,套接字被接受之后
- 若send、WSASend、sendto或者WSASendTo操作失败,返回了WSAEWOULDBLOCK错误,而且缓冲区空间变为可用
因此,作为一个应用程序,自收到首条FD_WRITE消息开始,便应认为自己必然能在一个套接字上发出数据,直至一个send、WSASend、sendto或WSASendTo返回套接字错误WSAEWOULDBLOCK。经过了这样的失败以后,要再用另一条FD_WRITE通知应用程序再次发送数据。
太久没有用MFC写界面,这方面的东西暂时没法整。
3.3 WSAEventSelect 事件选择
与WSAAsyncSelect类似的是,WSAEventSelect也允许应用程序在一个或多个套接字上,接收以事件为基础的网络事件通知。主要差异在于事件选择模型是将网络事件投递至一个事件对象句柄,而非投递至一个窗口例程。
使用步骤:
- 1、要对每一个使用的套接字创建一个对应的事件对象
WSAEVENT WSACreateEvent(void);
直接使用该函数创建一个事件对象,返回对象句柄,失败返回WSA_INVALID_EVENT。默认为人工置位且初始状态为无信号,当触发事件后状态自动变更为有信号,这时候需要调用WSAResetEvent函数手动将状态再置为无信号。
WSAEVENT WSAResetEvent(WSAEVENT hEvent);
当这个事件不再使用时,需要调用WSACloseEvent函数将其关闭,
WSAEVENT WSACloseEvent(WSAEVENT hEvent);
- 2、接下来需要将套接字与我们创建好的事件对象绑定在一起,同时注册感兴趣的网络事件类型
具体类型跟异步选择模型一致。此时需要用到函数WSAEventSelect
int WSAEventSelect( _In_ SOCKET s, _In_ WSAEVENT hEventObject, _In_ long lNetworkEvents);
- s,套接字描述符
- hEvnetObject,需要设置感兴趣网络事件类型的事件对象句柄
- lNetworkEvents,感兴趣的网络事件位掩码,使用方法同异步选择模型
- 3、WSAWaitForMultipleEvents来等待事件触发
DWORD WSAWaitForMultipleEvents( _In_ DWORD cEvents, _In_ const WSAEVENT *lphEvents, _In_ BOOL fWaitAll, _In_ DWORD dwTimeout, _In_ BOOL fAlertable);
- cEvents,事件对象个数,单次函数调用最大不超过WSA_MAXIMUM_WAIT_EVENTS(64),这个跟事件内核对象的等待函数一致。
- lphEvents,事件数组首地址
- fWaitAll,TRUE-所有事件触发后返回;FALSE-有事件触发就返回,此时返回值指出是哪一个事件触发
- dwTimeout,等待超时时间,单位ms,到了等待时间不管fWaitAll如何设置都会返回;尽量避免设置为0
- fAlertable,通常忽略,设置为FALSE,一般用于重叠I/O
注意:
- 想要等待超过64个套接字就必须创建额外的等待线程来管理更多的套接字;
- 当等待到一个事件触发信号时,需要使用返回值减去WSA_WAIT_EVENT_0,得到的对应的有信号事件索引
- 4.获取发生的事件类型
需要调用WSAEnumNetworkEvents函数从已取得的套接字和事件句柄上取得发生的网络事件类型,其函数原型如下:
int WSAEnumNetworkEvents( _In_ SOCKET s, _In_ WSAEVENT hEventObject, _Out_ LPWSANETWORKEVENTS lpNetworkEvents );
- s,产生网络事件的套接字
- hEventObject,可选,指定一个事件对象句柄,该事件会被重置
- lpNetworkEvents,指针,指向WSANETWORKEVENTS结构,用于接收套接字上发生的网络事件类型以及可能出现的错误代码。
WSANETWORKEVENTS结构如下:
typedef struct _WSANETWORKEVENTS { long lNetworkEvents; int iErrorCode[FD_MAX_EVENTS]; } WSANETWORKEVENTS, FAR * LPWSANETWORKEVENTS;
lNetworkEvents,指定了一个值,对应于套接字上发生的所有网络事件类型;可能同时会发生多个网络事件
iErrorCode,指定的是一个错误代码数组,同lNetworkEvents中的网络事件关联在一起。针对每个网络事件类型,都存在一个特殊的事件索引,名字与事件类型名字类似,在类型名后添加一个"_BIT"字符串即可。具体使用方法如下:
1 if (NetworkEvents.lNetworkEvents & FD_ACCEPT) 2 { 3 if (NetworkEvents.iErrorCode[FD_ACCEPT_BIT] != 0) 4 { 5 printf("FD_ACCEPT failed with error %d\n", NetworkEvents.iErrorCode[FD_ACCEPT_BIT]); 6 break; 7 } 8 }
- 5.处理完步骤4中的事件后,应用程序应该在所有可用套接字上继续等待更多的网络事件
2.4 overlapped 重叠模型
相对前面三种模型,重叠I/O模型使应用程序达到更佳的系统性能。重叠模型的基本设计原理是让应用程序使用一个重叠的数据结构,一次投递一个或者多个winsock I/O请求。针对提交的请求,在它们完成之后,应用程序可为它们提供服务。适用于除Windows CE外的各种Windows平台,总体设计以Win32重叠I/O机制为基础。该机制可通过ReadFile和WriteFile对设备执行I/O操作,但后续winsock 2版本改为使用WSARecv和WSASend函数。
要在一个套接字上使用重叠I/O模型,就必须先使用WSA_FLAG_OVERLAPPED这个标志来创建一个套接字:
if ((sock = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0,WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET) { printf("Failed to get a socket %d\n", WSAGetLastError()); return; }
如果创建套接字函数使用的是socke,那么会默认设置为WSA_FLAG_OVERLAPPED标志。成功创建一个套接字,同时将其与一个本地端口绑定后,可以开始进行重叠I/O操作,方法是下列winsock函数,同时制定一个WSAOVERLAPPED结构(可选):
- WSASend
- WSASendTo
- WSARecv
- WSARecvFrom
- WSAIoctl
- AcceptEx
- TransmitFile
其中每个函数都与一个套接字上的数据发送、接受及连接的接受有关。因此,这些活动可能会花极少的时间完成,这正是每个函数都可接受一个WSAOVERLAPPED结构作为参数的原因。若随一个WSAOVERLAPPED结构一起调用这些函数,函数会立即完成并返回,无论套接字是否锁定。它们依赖于WSAOVERLAPPED结构来返回一个I/O请求的返回。
主要有两种方法可以用来管理一个重叠I/O请求的完成:
- 可以等待“事件对象通知”;
- 通过“完成例程”;
前面的函数列表(AcceptEx除外)有另一个常用的参数:lpCompletionRoutine,可选,指向一个完成例程函数,在重叠请求完成后调用。
2.4.1 事件通知
重叠I/O的事件通知方法要求将Win32事件对象与WSAOVERLAPPED结构关联在一起,上述的一些函数调用会立即返回SOCKET_ERROR,获取错误码会得到一个与错误状态有关的报告,意味着I/O操作正在进行。之后应用程序需要等待与WSAOVERLAPPED结构对应的事件对象,来获取重叠I/O请求何时完成。
typedef struct _OVERLAPPED { ULONG_PTR Internal; ULONG_PTR InternalHigh; union { struct { DWORD Offset; DWORD OffsetHigh; } DUMMYSTRUCTNAME; PVOID Pointer; } DUMMYUNIONNAME; HANDLE hEvent; } OVERLAPPED, *LPOVERLAPPED;
WSAOVERLAPPED结构只是上述OVERLAPPED结构的一个别名,我们关心的只是其中的hEvent字段,其他字段均由系统内部使用。hEvent字段将一个事件对象句柄与一个套接字关联起来,具体使用类似事件选择模型,调用WSACreateEvent函数创建一个事件对象,将句柄值赋给重叠I/O结构的hEvent字段,再使用该重叠结构来调用一个winsock2函数即可。
前面的步骤一致,都是创建套接字,绑定,监听,创建事件对象,绑定事件对象与套接字,调用WSAWaitForMultipleEvents函数来等待投递的重叠I/O请求的事件响应,到这一步为止都是与事件选择模型步调一致,当获取到一次重叠请求完成之后,需要调用函数WSAGetOverlappedResult来判断重叠调用结果,函数原型如下:
BOOL WSAAPI WSAGetOverlappedResult(
_In_ SOCKET s,
_In_ LPWSAOVERLAPPED lpOverlapped,
_Out_ LPDWORD lpcbTransfer,
_In_ BOOL fWait,
_Out_ LPDWORD lpdwFlags);
- s,指定在重叠操作开始时,与之对应的套接字
- lpOverlapped,指针,对应于重叠操作开始时指定的重叠结构
- lpcbTransfer,指针,对应一个DWORD变量,负责接收一次重叠发送或接受实际传输的字节数
- fWait,TRUE-除非操作完成,否则函数不返回;FALSE-操作处于“待决”状态,函数返回FALSE,附加错误WSA_IO_INCOMPLETE(IO操作未完成),对于事件通知类型而言,该参数没有意义
- lpdwFlags,指针,指向一个DWORD,负责接收结果标志,不能为nullptr
若函数调用成功,则返回TRUE,意味着重叠请求已经完成,而且由lpcbTransfer指向的值进行了更新。若返回值为FALSE,可能由下列某种原因造成:
- 重叠操作仍处于“待决“状态
- 重叠操作未完成,但是含有错误
- 重叠操作完成状态不可判定,因为在提供给函数的一个或多个参数中,存在错误
失败后,lpcbTransfer指向内容不会更新,应用程序应当调用WSAGetLastError函数查询错误原因。实际应用步骤如下:
- 创建套接字,在指定端口监听连接请求
- 接受一个进入的连接请求
- 为接受的套接字新建一个WSAOVERLAPPED结构,为该结构分配一个事件对象,并将事件对象句柄置入等待数组,供后续WSAWaitForMultipleEvents函数使用
- 在套接字上投递一个异步WSARecv请求,指定参数为WSAOVERLAPPED结构<通常立即返回SOCKET_ERROR, WSA_IO_PENDING IO操作尚未完成>
- 使用步骤3中的事件数组,调用WSAWaitForMultipleEvents函数等待重叠请求绑定的事件触发
- WSAWaitForMultipleEvents针对触发事件,调用WSAResetEvent重设状态
- 调用WSAGetOverlappedResult函数,判断重叠调用返回的状态及相应处理
- 在套接字上投递另一个重叠WSARecv请求
- 重复步骤5-8
需要提供对多个套接字的支持时,将重叠I/O处理部分移动到一个独立线程中,让主线程为后续连接请求提供服务。这种设计主要是无法避免accept的阻塞,除了使用ioctlsocket函数修改监听套接字非阻塞然后进行一大堆的额外处理这种吃力不讨好的方式外,winsock提供的另一个API:AcceptEx函数在这种情况下比较适用,不需要进行额外的线程开销,只需要投递相应的AcceptEx的监听重叠请求并进行相应的处理即可<处理完一个accept重叠请求之后再投递下一个,建议保持一个活动的请求即可>。
2.4.2 完成例程
“完成例程”是应用程序对重叠I/O请求进行管理的另一种方法,完成例程实际上就是对重叠I/O请求进行处理的函数。在投递重叠I/O请求时,将完成例程作为参数传递给相应的请求函数,在该重叠I/O请求完成时由操作系统调用传递的完成例程函数。
这个基本设计宗旨是通过调用者的线程,为一个已完成的I/O请求提供服务,除此以外,应用程序通过完成例程,继续进程重叠I/O处理。
使用完成例程时,在我们的应用程序中,必须为一个I/O请求调用的winsoc函数,指定一个完成例程函数,同时指定一个WSAOVERLAPPED结构,一个完成例程必须拥有以下函数原型:
void CALLBACK CompletionROUTINE( IN DWORD dwError, IN DWORD cbTransferred, IN LPWSAOVERLAPPED lpOverlapped, IN DWORD dwFlags);
- dwError,表明一个重叠I/O请求的完成状态
- cbTransferred,指定了在重叠操作期间,实际传输的字节数
- lpOverlapped,指针,指向传递给最初I/O请求的重叠结构
- dwFlag,当接收操作立刻完成时存放相关的信息
完成例程提交的重叠I/O请求与用事件对象提交的重叠请求之间,存在一处非常重要的区别。在完成例程中,WSAOVERLAPPED结构的事件句柄字段hEvent没有使用,也就是说,在完成例程中,重叠结构不关联事件对象。
在完成例程发出一个重叠I/O调用之后,作为调用线程,一旦完成,它最必须为完成例程提供服务。这要求我们的调用线程处于一种“可警告的等待状态”,并在I/O操作完成之后,对完成例程加以处理。