套接字的I/O模型(一)
来源:互联网 发布:unity3d中文实例教程 编辑:程序博客网 时间:2024/05/13 21:45
套接字的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/Owhile (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;//将事件句柄分配到HandleArraywhile (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网络编程(第二版)
- 套接字的I/O模型(一)
- Winsock的套接字I/O模型
- 套接字的I/O模型(二)
- 套接字的I/O模型(三)
- 套接字i/o模型
- 套接字I/O模型
- 套接字I/O模型
- 套接字I/O模型
- 套接字I/O模型之WSAEventSelect
- 套接字之重叠I/O模型
- 套接字I/O模型之WSAEventSelect
- 套接字I/O模型之WSAEventSelect
- 套接字I/O模型之WSAEventSelect
- 套接字Select I/O模型
- Windows套接字I/O模型
- 套接字Select I/O模型
- Windows套接字I/O 模型
- 套接字Select I/O模型
- MySQL的增删改查
- JDBC数据库连接 1)通过Driver连接数据库
- tiny-dnn import caffe's model
- Java虚拟机类加载
- Linux学习——基础命令总结(1)
- 套接字的I/O模型(一)
- geoserver 源码编译问题
- 开源中国源码学习UI篇(一)之FragmentTabHost的使用分析
- Android程序中回调的讲解和使用
- web.xml中<web-app>报红解决方案
- Android使用Parcelable序列化复杂数据结构
- 使用QFileInfo类获取文件信息
- 三种model 在lfw 上的精度
- 最大m子段和问题算法进化历程