套接字的I/O模型(一)

来源:互联网 发布:unity3d中文实例教程 编辑:程序博客网 时间:2024/05/13 21:45

套接字的I/O模型(一)

共有6种类型的套接字I/O模型,它们包括:blocking(阻塞)、select(选择)、WSAAsyncSelect(异步选择)、WSAEventSelect(事件选择)、overlapped(重叠)以及completionport(完成端口).

  1. 阻塞模型

    • 通常采用这个模型的应用程序,在处理I/O时,每个套接字连接通常会使用一个或两个线程。之后每个线程都将发出阻塞操作,如send和recv。
    • 阻塞模型的优点是其简洁性。
    • 缺点是创建线程会消耗系统资源,很难将它扩展到有很多连接的情况。

  2. 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);
    1. 在三个参数readfds,writefds,exceptfds,至少有一个不能为空值(NULL);
    2. timeout,用来决定select等待I/O操作完成时,最多等待多长的时间,若超过timeval设定时间,便会返回0。如果timeout是一个空指针,select会无限期的处于阻塞状态,直到有一个描述符于指定条件相符后才结束
    • readfds集合包含符合下述一个条件的套接字:
      1. 有数据可读
      2. 连接已经被关闭、重启或终止
      3. 假如已调用了listen,而且有一个连接正处于搁置状态,那么accept函数调用会成功
    • writefds集合包括符合下述一个条件的套接字:
      1. 有数据可以发出
      2. 如果正对一个非阻塞连接调用进行处理,则连接成功
    • exceptfds集合包括符合下述一个条件的套接字:
      1. 假如正对一个非阻塞连接调用进行处理,连接尝试就会失败
      2. 有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集合进行处理与检查:
      1. FD_ZERO(* set) : 将set初始化成空集合。集合在使用前都要清空
      2. FD_CLR(s, * set): 从set中删除套接字s
      3. FD_ISSET(s, * set): 检查s是否为set集合的一名成员:若是返回TRUE
      4. FD_SET(s, * set): 将套接字s加入集合set中
    • 假设我们想知道是否可以从一个套接字中安全地读取数据,同时不会陷入阻塞状态,便可使用FD_SET宏,将这个套接字分配给fd_read集合,在调用select。要检测这个套接字是否仍属于fd_read集合的一部分,可使用FD_ISSET宏。
    • 采用下述步骤,便可完成用selectc操作一个或多个套接字句柄的全过程:
      1. 使用FD_ZERO宏,初始化自己感兴趣的每一个fd_set。
      2. 使用FD_SET宏,将套接字句柄分配给自己感兴趣的每个fd_set。
      3. 调用select函数,然后等待直到I/O活动在指定的fd_set集合中设置好了一个或多个套接字句柄。select完成后,会返回在所有fd_set集合中设置的套接字句柄总数,并对每个集合进行相应的更新。
      4. 根据select的返回值,应用程序便可判断出哪些套接字存在着被搁置的I/O操作(具体的方法是使用FD_ISSET宏,对每个fd_set集合进行检查)。
      5. 知道了每个集合中被挂起的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。

  3. 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通知才会发出:

      1. 使用connect或WSAConnect,一个套接字首次建立连接
      2. 使用accept或WSAAccept,套接字被接受以后
      3. 若send、WSASend、sendto或WSASendTo操作失败,返回了WSAEWOULDBLOCK错误,而且缓冲区的空间变得可用时

      因此,作为一个应用程序,自收到首条FD_WRITE消息开始,便应认为必然能在一个套接字上发出数据,直至send、WSASend、sendto或WSASendTo返回套接字错误WSAEWOULDBLOCK。经过了这样的失败以后,要再用另一条FD_WRITE通知应用程序可以再次发送数据。

    • WSAAsyncSelect优点:它可以在系统开销不大的情况下同时处理许多连接,而select模型需要接力fd_set结构。

    • WSAAsyncSelect缺点:应用程序不需要窗口(例如服务或控制台应用程序),它也不得不额外使用一个窗口。同时,用一个单窗口程序来处理成千上万的套接字中的所有事件,很有可能成为性能瓶颈(意味着这个模型的伸缩性不太好)。

  4. 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创建的事件有两种工作状态和两种工作模式。

      1. 工作状态:已传信(signaled)和未传信(non-signaled)
      2. 工作模式:人工重设(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;}
  5. overlapped模型

  6. completionport模型

声明:以上整理于Windows网络编程(第二版)

1 0
原创粉丝点击