套接字的I/O模型(一)
共有6种类型的套接字I/O模型,它们包括:blocking(阻塞)、select(选择)、WSAAsyncSelect(异步选择)、WSAEventSelect(事件选择)、overlapped(重叠)以及completionport(完成端口).
阻塞模型
- 通常采用这个模型的应用程序,在处理I/O时,每个套接字连接通常会使用一个或两个线程。之后每个线程都将发出阻塞操作,如send和recv。
- 阻塞模型的优点是其简洁性。
- 缺点是创建线程会消耗系统资源,很难将它扩展到有很多连接的情况。
select模型
select模型的工作原理是利用select函数,实现对I/O的管理,所以称其为“select模型”。
select函数可用于判断套接字上是否存在数据,或者能否向一个套接字写入数据。
设计select函数的目的是为了防止应用程序在套接字处于阻塞模式时,在I/O绑定调用(如send或recv)过程中进入阻塞状态;同时也防止在套接字处于非阻塞模式中,产生WSAEWOULDBLOCK错误。除非满足事先用参数规定的条件,否则select函数在进行I/O操作时会阻塞。
WSAEWOULDBLOCK错误意味着请求的操作在调用期间没有时间完成。
int select( _In_ int nfds, //忽略,仅为和早起套接字程序兼容 置为0 _Inout_ fd_set *readfds, // checked for readability. _Inout_ fd_set *writefds, // checked for writability. _Inout_ fd_set *exceptfds, //checked for errors _In_ const struct timeval *timeout );
- 在三个参数readfds,writefds,exceptfds,至少有一个不能为空值(NULL);
- timeout,用来决定select等待I/O操作完成时,最多等待多长的时间,若超过timeval设定时间,便会返回0。如果timeout是一个空指针,select会无限期的处于阻塞状态,直到有一个描述符于指定条件相符后才结束
- readfds集合包含符合下述一个条件的套接字:
- 有数据可读
- 连接已经被关闭、重启或终止
- 假如已调用了listen,而且有一个连接正处于搁置状态,那么accept函数调用会成功
- writefds集合包括符合下述一个条件的套接字:
- 有数据可以发出
- 如果正对一个非阻塞连接调用进行处理,则连接成功
- exceptfds集合包括符合下述一个条件的套接字:
- 假如正对一个非阻塞连接调用进行处理,连接尝试就会失败
- 有OOB(Out-of-band,带外)数据可供读取
typedef struct timeval { long tv_sec; long tv_usec; } timeval; //tv_sec为 timeval时的秒数,tv_usec为微秒数,即秒后面的零头。
- 用select对套接字进行监听之前,应用程序必须将套接字句柄分配给一个集合,设置好一个或所有的读、写以及例外的fd_set。 讲一个套接字分配给任何一个集合后,再来调用select,便可知道某个套接字上是否在发生I/O活动。可以使用下列宏对fd_set集合进行处理与检查:
- FD_ZERO(* set) : 将set初始化成空集合。集合在使用前都要清空
- FD_CLR(s, * set): 从set中删除套接字s
- FD_ISSET(s, * set): 检查s是否为set集合的一名成员:若是返回TRUE
- FD_SET(s, * set): 将套接字s加入集合set中
- 假设我们想知道是否可以从一个套接字中安全地读取数据,同时不会陷入阻塞状态,便可使用FD_SET宏,将这个套接字分配给fd_read集合,在调用select。要检测这个套接字是否仍属于fd_read集合的一部分,可使用FD_ISSET宏。
- 采用下述步骤,便可完成用selectc操作一个或多个套接字句柄的全过程:
- 使用FD_ZERO宏,初始化自己感兴趣的每一个fd_set。
- 使用FD_SET宏,将套接字句柄分配给自己感兴趣的每个fd_set。
- 调用select函数,然后等待直到I/O活动在指定的fd_set集合中设置好了一个或多个套接字句柄。select完成后,会返回在所有fd_set集合中设置的套接字句柄总数,并对每个集合进行相应的更新。
- 根据select的返回值,应用程序便可判断出哪些套接字存在着被搁置的I/O操作(具体的方法是使用FD_ISSET宏,对每个fd_set集合进行检查)。
- 知道了每个集合中被挂起的I/O操作之后,对I/O进行处理,然后返回步骤1,继续处理select。
- select返回后,它会修改每个fd_set结构,删除那些不存在被挂起的I/O操作的套接字句柄。这正是上述步骤4中,为何用使用FD_ISSET宏来判断某个特定的套接字是否仍在集合中的原因。
- 示例代码:(仅流程)
SOCKET s; fd_set fdread; int ret; //创建套接字,接受连接 //在套接字上管理I/O while (TRUE) { //在调用select()之前始终清除读出集 FD_ZERO(&fdread); if ((ret == select(0, &fdread, NULL, NULL, NULL)) == SOCKET_ERROR) { //条件有错 } if (ret > 0) { //在这个简单的例子中,select() 返回值为1.处理多个套接字的应用程序可返回比1大的值。 //在这里,应用程序应该检查一下,看看该套接字是否属于集合的一部分。 if (FD_ISSET(s, &fdread)) { //套接字上发生了一个事件 } } }
- 使用select的优势是,能够从单个线程的套接字上进行多重连接及I/O。这就避免了伴随阻塞套接字和多重连接的线程剧增。但可以加到fd_set结构中的最大套接字数量是一个不好的地方,默认状态下,最大数据由FD_SETSIZE定义(默认为64)。用fd_set设置最大值为1024。
WSAAsyncSelect模型
WSAAsyncSelect是一个异步I/O模型,应用程序可在一个套接字上,接收以windows消息为基础的网络事件通知。 具体做法是在建好一个套接字后,调用WSAAsyncSelect函数。
WSAAsyncSelect和WSAEventSelect模型提供了读写数据能力的异步通知,但是它们不提供异步数据传送,而重叠及完成端口模型却提供异步数据传送。
【消息通知】想要使用WSAAsyncSelect模型,在应用程序中,首先必须使用createWindow函数创建一个窗口,再为该窗口提供一个窗口过程支持函数(Winproc)。亦可使用一个对话框,为其提供一个对话过程来代替窗口过程,这是因为对话框本质也是窗口。
int WSAAsyncSelect( _In_ SOCKET s, //我们感兴趣的套接字 _In_ HWND hWnd, //指定窗口句柄,网络事件发生后,想要接收到通知消息的窗口或对话框 _In_ unsigned int wMsg, //指定网络事件发生时,打算接收消息。投递到hWnd所标示的窗口或对话框 _In_ long lEvent //应用程序感兴趣的一系列事件。 );
WSAAsyncSelect(s, hWnd, wMsg, FD_READ|FD_CONNECT|FD_CLOSE);//可参考MSDN
多个事件务必在套接字上一次完成注册。另外还要注意的是,一旦在某个套接字上启用了事件通知,那么以后除非明确调用closesocket命令,或者由应用程序针对这个套接字调用了WSAAsyncSelect,从而更改注册的网络事件类型,否则事件通知总是有效的。若将lEvent设置为0,则相当于停止在套接字上进行所有的网络事件通知。
应用程序在一个套接字上成功调用WSAAsyncSelect之后,它会在hWnd窗口句柄参数相关联的窗口过程中,以Windows消息的形式,接收网络事件通知。
LRESULT CALLBACK WindowProc( _In_ HWND hwnd, //窗口句柄 _In_ UINT uMsg, //指定需要对哪些消息进行处理 _In_ WPARAM wParam, //指定的是一个套接字,该套接字上发生了一个网络事件 _In_ LPARAM lParam //低位节指定了已经发生的网络事件,高位字包含了可能出现的错误代码 );
网络事件抵达窗口过程后,应用程序首先检查lParam高字位判断是否发生了网络错误(WSAGETSELECTERROR)。若无错误,则调查是哪个网络事件类型造成了这条Windows消息的触发(lParam低字位内容)。WSAGETSELECTEVENT返回lParam低字部分。
示例代码:(仅流程)
#define WM_SOCKET WM_USER+11 #include <WinSock2.h> #include <windows.h> #include<Ws2tcpip.h> int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WSADATA wsd; SOCKET Listen; SOCKADDR_IN InternetAddr; HWND Window; //创建窗口,并将下面的ServerWinProc分配给它 Window = CreateWindow(); //启动并创建套接字 WSAStartup(MAKEWORD(2, 2),&wsd); Listen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //将套接字绑定到5150端口上并开始监听连接 InternetAddr.sin_family = AF_INET; inet_pton(AF_INET, INADDR_ANY, (void*)InternetAddr.sin_addr.S_un.S_addr); InternetAddr.sin_port = htons(5150); bind(Listen, (PSOCKADDR)&InternetAddr,sizeof(InternetAddr)); //使用上面定义的WM_SOCKET在新套接字上设置窗口消息通知 WSAAsyncSelect(Listen, Window, WM_SOCKET, FD_ACCEPT | FD_CLOSE); listen(Listen,5); //转换并分派窗口消息,知道应用程序终止 while (TRUE) { //............. } } BOOL CALLBACK ServerWinProc(HWND hDlg, UINT wMsg, WPARAM wParam, LPARAM lParam) { SOCKET Accept; switch (wMsg) { case WM_PAINT: //处理窗口画图消息 break; case WM_SOCKET: //使用WSAGETSELECTERROR宏来判断套接字上是否发生了错误 if (WSAGETSELECTERROR(lParam)) { //显示错误,关闭套接字 closesocket((SOCKET)wParam); break; } //确定在套接字上发生了什么事件 switch (WSAGETSELECTEVENT(lParam)) { case FD_ACCEPT: //接受一个传入的连接 Accept = accept(wParam, NULL, NULL); //让结束套接字为读、写及关闭通知做好准备 WSAAsyncSelect(Accept, hDlg, WM_SOCKET); break; case FD_READ: //从wParam中的套接字中检索数据 break; case FD_WRITE: 从wParam中的套接字已准备好发送数据 break; case FD_CLOSE: closesocket((SOCKET)wParam); break; } break; } return TRUE; }
应用程序如何对FD_WRITE事件通知进行处理。在下列三种条件下,FD_WRITE通知才会发出:
- 使用connect或WSAConnect,一个套接字首次建立连接
- 使用accept或WSAAccept,套接字被接受以后
- 若send、WSASend、sendto或WSASendTo操作失败,返回了WSAEWOULDBLOCK错误,而且缓冲区的空间变得可用时
因此,作为一个应用程序,自收到首条FD_WRITE消息开始,便应认为必然能在一个套接字上发出数据,直至send、WSASend、sendto或WSASendTo返回套接字错误WSAEWOULDBLOCK。经过了这样的失败以后,要再用另一条FD_WRITE通知应用程序可以再次发送数据。
WSAAsyncSelect优点:它可以在系统开销不大的情况下同时处理许多连接,而select模型需要接力fd_set结构。
WSAAsyncSelect缺点:应用程序不需要窗口(例如服务或控制台应用程序),它也不得不额外使用一个窗口。同时,用一个单窗口程序来处理成千上万的套接字中的所有事件,很有可能成为性能瓶颈(意味着这个模型的伸缩性不太好)。
WSAEventSelect模型
类似于WSAAsyncSelect,它也允许应用程序在一个或多个套接字上,接收以事件为基础的网络事件通知。于它的主要差别在于网络事件通知是由对象句柄完成的,而不是通过窗口例程完成。
事件通知模型要求应用程序针对打算使用的每一个套接字,首先创建一个事件对象。
WSAEVENT WSACreateEvent(void);//if error return WSA_INVALID_EVENT
WSACreateEvent返回值是一个人工重设的对象句柄,一旦得到了事件对象句柄之后,必须将它与某个套接字关联在一起,同时注册感兴趣的网络事件类型。使用WSAEventSelect。
int WSAEventSelect( _In_ SOCKET s, //感兴趣的套接字 _In_ WSAEVENT hEventObject, //要与套接字关联在一起的事件对象,用WSACreateEvent获得。 _In_ long lNetworkEvents //网络事件 FD_XXX );
为WSACreateEvent创建的事件有两种工作状态和两种工作模式。
- 工作状态:已传信(signaled)和未传信(non-signaled)
- 工作模式:人工重设(manualreset)和自动重设(autoreset)。
WSACreateEvent最开始在一种未传信的工作状态中,并用一种人工重设模式,来创建事件句柄。若网络事件触发了一个与套接字关联在一起的事件对象,工作状态便会从未传信转变为已传信。由于事件对象是在一种人工重设模式中创建的,所以在完成了一个I/O请求的处理之后,应用程序需要负责将工作状态从已传信更改为未传信。需要调用WSAResetEvent。
BOOL WSAResetEvent(_In_ WSAEVENT hEvent //事件句柄 );//if succeeds return TRUE
当应用程序完成对某个事件对象的处理后,便应调用WSACloseEvent释放由事件句柄使用的系统资源。
BOOL WSACloseEvent( _In_ WSAEVENT hEvent);////if succeeds return TRUE
套接字同一个事件对象句柄关联在一起后,应用程序便可开始开始I/O处理。这就需要应用程序等待网络事件触发事件对象的工作状态。WSAWaitForMultipleEvents函数便是一个用来等待一个或多个事件对象句柄,并在事先指定的一个或所有句柄进入已传信状态后,或在超过一个规定的时间周期后,立即返回。
DWORD WSAWaitForMultipleEvents( _In_ DWORD cEvents, //事件对象的数量 最大值:WSA_MAXIMUM_WAIT_EVENTS (64) _In_ const WSAEVENT *lphEvents, //引用该事件对象的数组 _In_ BOOL fWaitAll, //TRUE:lphEvents所有对象进入已传信状态才会返回 _In_ DWORD dwTimeout, //等待最长时间(毫秒),WSA_INFINITE等到一个事件为止 _In_ BOOL fAlertable //在WSACreateEvent里可被忽略且应设置为FALSE );
如果一次只服务一个已传信事件(fWaitAll设为FALSE),就可能让套接字一直处于“挨饿”,且可能持续到事件数组末尾。例如下面代码
WSAEVENT HandleArray[WSA_MAXIMUM_WAIT_EVENTS]; int Waitcount = 0, ret, index; //将事件句柄分配到HandleArray while (TRUE) { ret = WSAWaitForMultipleEvents(Waitcount, HandleArray, FALSE, WSA_INFINITE, FALSE); if ((ret != WSA_WAIT_FAILED) && (ret != WSA_WAIT_TIMEOUT)) { index = ret - WSA_WAIT_EVENT_0; //服务事件在HandleArray[index]上被传信 WSAResetEvent(HandleArray[index]); } }
如果和事件数组中索引0相关的套接字连续地接收数据, 以至于事件被重置之后,又有额外数据到达,并导致事件再次被传信,则数组中其余的时间就会被闲置。这当然不是理想的状态。只要回路中有一个事件被传信并得到处理,就应该检查数组中所有的事件,看看它们是否也被传信。在有事件被传信之后,对每个事件的句柄使用WSAWaitForMultipleEvents,并将dwTimeOut指定为0,既可达到上述目的。
若WSAWaitForMultipleEvents收到一个事件对象的网络事件通知,便会返回一个值,指出造成函数返回的事件对象。这样,应用程序便可引用事件数组中已传信的事件,并检索与那个事件对应的套接字,判断到底是哪个套接字上,发生了什么样的网络事件类型。对事件数组中的事件进行引用时应该用WSAWaitForMultipleEvents的返回值减去预定于值WSA_WAIT_EVENT_0,从而得到具体索引值。
index = WSAWaitForMultipleEvents(......); MyEven t= EvenArray[index - WSA_WAIT_EVENT_0];
知道了造成网络事件的套接字之后,接下来可调用WSAEnumNetworkEvents函数,查看网络事件。
int WSAEnumNetworkEvents( _In_ SOCKET s, //造成网络事件的套接字 _In_ WSAEVENT hEventObject, //事件句柄 _Out_ LPWSANETWORKEVENTS lpNetworkEvents //检索套接字上发生的网络事件类型等.... ); //hEventObject参数是可选的,指定了一个事件句柄,对应打算重设的那个事件对象。由于我们对象处于一种已传信状态,所以可将它传入,令其自动成为未传信状态。如果不想用hEventObject参数来重设事件,那么可使用WSAResetEvent函数对事件进行人工重设。
示例代码:(仅流程)
#include<winsock2.h> #include<Ws2tcpip.h> #include<stdio.h> #pragma comment(lib,"WS2_32") #define BUFFER_SIZE 1024 void CompressArrays(WSAEVENT events[], SOCKET sockets[], DWORD *total, int index) { for (size_t i = index + 1; i < *total; i++) { events[i - 1] = events[i]; } *total--; } int main(int argc, char **argv) { WSADATA wsaData; char buffer[BUFFER_SIZE]; sockaddr_in InternetAddr; SOCKET SocketArray[WSA_MAXIMUM_WAIT_EVENTS]; WSANETWORKEVENTS NetworkEvents; WSAEVENT NewEvent; WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS]; SOCKET Accept, Listen; DWORD EventTotal = 0; DWORD Index; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("WSAStartup()/n"); return 0; } // 创建一个流式套接口 Listen = socket(AF_INET, SOCK_STREAM, 0); InternetAddr.sin_family = AF_INET; InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY); InternetAddr.sin_port = htons(5050); if (bind(Listen, (PSOCKADDR)&InternetAddr, sizeof(InternetAddr)) == SOCKET_ERROR) { printf("bind()/n"); return 0; } // 创建一个事件对象 NewEvent = WSACreateEvent(); // 在Listen套接口上注册套接口连接和关闭的网络事件 WSAEventSelect(Listen, NewEvent, FD_ACCEPT | FD_CLOSE); if (listen(Listen, 5) == SOCKET_ERROR) { printf("listen()/n"); return 0; } SocketArray[EventTotal] = Listen; EventArray[EventTotal] = NewEvent; EventTotal++; while (true) { // 在所有套接口上等待网络事件的发生 Index = WSAWaitForMultipleEvents(EventTotal, EventArray, FALSE, WSA_INFINITE, FALSE); if (WSAEnumNetworkEvents(SocketArray[Index - WSA_WAIT_EVENT_0], EventArray[Index - WSA_WAIT_EVENT_0], &NetworkEvents) == SOCKET_ERROR) { printf("%d/n", WSAGetLastError()); printf("WSAEnumNetworkEvents()/n"); return 0; } // 检查FD_ACCEPT if (NetworkEvents.lNetworkEvents & FD_ACCEPT) { if (NetworkEvents.iErrorCode[FD_ACCEPT_BIT] != 0) { WSACloseEvent(EventArray[Index - WSA_WAIT_EVENT_0]); printf("FD_ACCEPT failed with error %d/n", NetworkEvents.iErrorCode[FD_ACCEPT_BIT]); break; } // 接收新的连接,并将其存入套接口数组 Accept = accept(SocketArray[Index - WSA_WAIT_EVENT_0], NULL, NULL); // 当套接口的数量超界时,关闭该套接口 if (EventTotal > WSA_MAXIMUM_WAIT_EVENTS) { printf("Too many connections"); closesocket(Accept); break; } NewEvent = WSACreateEvent(); if (NewEvent == WSA_INVALID_EVENT) { printf("WSACreateEvent()/n"); break; } WSAEventSelect(Accept, NewEvent, FD_READ | FD_WRITE | FD_CLOSE); EventArray[EventTotal] = NewEvent; SocketArray[EventTotal] = Accept; EventTotal++; printf("Socket %d connected/n", Accept); } // 一下处理FD_READ通知 if (NetworkEvents.lNetworkEvents & FD_READ) { if (NetworkEvents.iErrorCode[FD_READ_BIT] != 0) { WSACloseEvent(EventArray[Index - WSA_WAIT_EVENT_0]); printf("FD_READ failed with error %d/n", NetworkEvents.iErrorCode[FD_READ_BIT]); break; } // 从套接口读入数据 int iRecv = recv(SocketArray[Index - WSA_WAIT_EVENT_0], buffer, sizeof(buffer), 0); if (iRecv == 0) { break; } else if (iRecv == SOCKET_ERROR) { printf("recv()/n"); break; } else { printf("recv data: %s", buffer); } } // 以下处理FD_WRITE通知 if (NetworkEvents.lNetworkEvents & FD_WRITE) { if (NetworkEvents.iErrorCode[FD_WRITE_BIT] != 0) { WSACloseEvent(EventArray[Index - WSA_WAIT_EVENT_0]); printf("FD_WRITE failed with error %d/n", NetworkEvents.iErrorCode[FD_WRITE_BIT]); break; } send(SocketArray[Index - WSA_WAIT_EVENT_0], buffer, sizeof(buffer), 0); } // 以下处理FD_CLOSE通知 if (NetworkEvents.lNetworkEvents & FD_CLOSE) { if (NetworkEvents.iErrorCode[FD_WRITE_BIT] != 0) { WSACloseEvent(EventArray[Index - WSA_WAIT_EVENT_0]); printf("FD_WRITE faield with error %d/n", NetworkEvents.iErrorCode[FD_WRITE_BIT]); break; } // 关闭套接口 closesocket(SocketArray[Index - WSA_WAIT_EVENT_0]); // 从套接口事件和事件数组中删除关闭的套接口的有关信息 CompressArrays(EventArray, SocketArray, &EventTotal, Index - WSA_WAIT_EVENT_0); } } WSACleanup(); return 0; }
//client #include<winsock2.h> #include<Ws2tcpip.h> #include<stdio.h> #pragma comment(lib,"WS2_32") #define BUFFER_SIZE 1024 int main(int argc, char **argv) { WSADATA wsaData; sockaddr_in ser; SOCKET sClient; char send_buf[] = "hello, I am a client"; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("WSAStartup()/n"); return 0; } ser.sin_family = AF_INET; ser.sin_port = htons(5050); InetPtonA(AF_INET, "127.0.0.1", (void*)&ser.sin_addr); sClient = socket(AF_INET, SOCK_STREAM, 0); if (sClient == INVALID_SOCKET) { printf("socket()/n"); return 0; } if (connect(sClient, (sockaddr*)&ser, sizeof(ser)) == INVALID_SOCKET) { printf("socket()/n"); return 0; } else { for (int i = 0; i < 10; i++) { int iLen = send(sClient, send_buf, sizeof(send_buf), 0); if (iLen == 0) { return 0; } else if (iLen == SOCKET_ERROR) { printf("send()/n"); return 0; } } } closesocket(sClient); WSACleanup(); return 0; }
overlapped模型
completionport模型
声明:以上整理于Windows网络编程(第二版)