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

  1. completionport模型

    • 假若一个应用程序同时需要管理为数众多的套接字,那么采用这种模型,可以达到最佳的系统性能。
    • 从本质上说,完成端口模型要求创建一个Windows完成端口对象,该对象通过指定数量的线程,对重叠I/O请求进行管理,以便为已经完成的重叠I/O请求提供服务。
    • 完成端口实际是Windows采用的一种I/O构造机制,除套接字之外,还可接受其他东西。
    • 使用这种模型之前,首先要创建一个I/O完成端口对象,用它面向任何数量的套接字句柄,管理多个I/O请求。则需要调用CreateIoCompletionPort函数:
    HANDLE WINAPI CreateIoCompletionPort(
     _In_     HANDLE    FileHandle,
     _In_opt_ HANDLE    ExistingCompletionPort,
     _In_     ULONG_PTR CompletionKey,
     _In_     DWORD     NumberOfConcurrentThreads
    )

    函数目的:1. 用于创建一个完成端口对象 2. 将一个句柄同完成端口关联到一起

    • 目的一:最开始创建一个完成端口时,我们唯一感兴趣的参数便是NumberOfConcurrentThreads:前三个参数都不太重要。NumberOfConcurrentThreads参数的特殊之处在于,它定义了在一个完成端口上,同时允许执行的线程数量。在理想情况下,我们希望每个处理器各自负责一个线程的运行,为完成端口提供服务,避免过于频繁的线程任务转换。若将该参数设为0则告诉系统,安装了多少个处理器,则允许同时同时运行多少个线程。下面用于创建一个I/O完成端口:

      CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0)

      该语句的作用是返回一个句柄,在为完成端口分配一个套接字句柄后,用来对那个端口进行辨识。

    • 目的二:一旦在完成端口上拥有足够多的工作器线程来为I/O请求提供服务,便可着手将套接字句柄同完成端口关联到一起。这需要在一个现有的完成端口上,调用CreateIoCompletionPort函数。

      1. FileHandle参数指定一个要同完成端口关联在一起的套接字句柄。

      2. ExistingCompletionPort参数标识的是一个现有的完成端口套接字句柄已经与它关联在一起。

      3. CompletionKey(完成键) 参数则标识的是要与某个特定套接字句柄关联在一起的单句柄数据

      CompletionKey 这个参数中,应用程序可保存于一个套接字对应的任意类型的信息。之所以把它叫做单句柄数据,是由于它代表了与套接字句柄关联在一起的数据。

    • 下面构建了一个基本的应用程序框架。阐述了如何使用完成端口模型,来开发一个回应服务器的应用程序

      1. 创建一个完成端口。第四个参数保持为0,它指定在完成端口上每个处理器一次只运行执行一个工作器线程。
      2. 判断系统内有多少个处理器。
      3. 创建工作器线程,根据步骤2得到的处理器信息,在完成端口上为已完成的I/O请求提供服务。在这个简单的例子中,我们为每个处理器都只创建一个工作线程。这是由于事先已预计到,到时不会有任何线程进入暂停状态,造成由于线程数量不足而使处理器空闲的局面(没有足够的线程可供执行)。调用CreateThread函数时,必须同时提供一个工作器例程,由线程创建好后执行。
      4. 准备好一个监听套接字,在端口5150上监听传入的连接请求。
      5. 使用accept函数,接受入站的连接请求。
      6. 创建一个数据结构,用于容纳单句柄数据,同时在结构中存入接受的套接字句柄。
      7. 调用CreateIoCompletionPort,将自accept返回的新套接字句柄同完成端口关联到一起。通过CompletionKey参数,将单句柄数据结构传递给CreateIoCompletionPort。
      8. 开始在已接受的连接上进行I/O操作。在此,我们希望通过重叠I/O机制,在新建的套接字上投递一个或多个异步WSARecv或WSASend请求。这些I/O请求完成后,工作器线程会为I/O请求提供服务,同时继续处理以后的I/O请求。在步骤3指定的工作器例程中,我们将看到这一点。
      9. 重复步骤5~8,直至服务器终止。
      typedef struct _PER_HANDLE_DATA
      {
      SOCKET Socket;
      SOCKADDR_STORAGE ClientAddr;
      //将和这个句柄关联的其他有用信息
      }PER_HANDLE_DATA,*LPPER_HANDLE_DATA;
      
      int main()
      {
      HANDLE CompletionPort;
      WSADATA wsd;
      SYSTEM_INFO SystemInfo;
      SOCKADDR_IN InternetAddr;
      SOCKET Listen;
      int i;
      
      //加载Winsock
      WSAStartup(MAKEWORD(2, 2), &wsd);
      
      //第一步 创建一个I/O完成端口
      CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
      
      //第二步 确定系统中有多少个处理器
      GetSystemInfo(&SystemInfo);
      
      //第三步 基于系统中可用的处理器数量创建工作器线程
      //对这个简单例子而言,我们为每个处理器都创建一个工作线程
      
      for (i = 0; i < SystemInfo.dwNumberOfProcessors; ++i)
      {
          HANDLE ThreadHandle;
      
          //创建一个服务器的工作线程,并将完成端口传递到该线程
          //注意:ServerWorkerThread进程并不是在这个列表中定义的
          ThreadHandle = CreateThread(NULL, 0, ServerWorkerTharead, CompletionPort, 0, NULL);
      
              //关闭线程句柄
              CloseHandle(ThreadHandle);
      }
      
      //第四步 创建一个监听的套接字
      Listen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
      InternetAddr.sin_family = AF_INET;
      InternetAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
      InternetAddr.sin_port = htons(5150);
      bind(Listen, (PSOCKADDR)&InternetAddr, sizeof(InternetAddr));
      
      //让套接字为监听做好准备
      listen(Listen, 5);
      while (TRUE)
      {
          PER_HANDLE_DATA *PerHandleData = NULL;
          SOCKADDR_IN saRemote;
          SOCKET Accept;
          int RemoteLen;
      
          //第五步 接受连接,并分配到完成端口
          RemoteLen = sizeof(saRemote);
          Accept = WSAAccept(Listen, (SOCKADDR *)&saRemote, &RemoteLen,......);
      
          //第六步 创建用来和套接字关联的单句柄数据信息结构
          PerHandleData = (LPPER_HANDLE_DATA)GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA));
          printf("Socket number %d connected \n", Accept);
          PerHandleData->Socket = Accept;
          memcpy(&PerHandleData->ClientAddr, &saRemote, RemoteLen);
      
          //第七步 将接受套接字和完成端口关联起来
          CreateIoCompletionPort((HANDLE)Accept, CompletionPort, (DWORD)PerHandleData, 0);
      
          //第八步 开始在接受套接字上处理I/O
          //使用重叠I/O,在套接字上投递一个或多个WSASend(),或WSARecv调用
          WSARecv(.....);
      }
      }
      
      DWORD WINAPI ServerWorkerThread(LPVOID lpParam)
      {
      //工作器线程的必备条件和稍后讨论
      return 0;
      }

—–>完成端口和重叠I/O<—–

  • 将套接字句柄与一个完成端口关联到一起后,便能以套接字句柄为基础,投递重叠发送与接收请求,开始对I/O请求进行处理。之后,可开始依赖完成端口,接收有关I/O操作完成情况的通知。从本质上说,完成端口模型利用了Windows重叠I/O机制。在这种机制中,类似WSASend和WSARecv这样的Winsock API调用会立即返回。此时,需要由应用程序负责在以后的某个时间,通过OVERLAPPED结构来检索调用的结果。在完成端口模型中,要想做到这一点,需要使用GetQueuedCompletionStatus函数,让一个或多个工作器线程在完成端口上等待。函数定义如下:

    BOOL WINAPI GetQueuedCompletionStatus(
    _In_  HANDLE       CompletionPort,  //线程所在的完成端口
    _Out_ LPDWORD      lpNumberOfBytes, //I/O操作(WSASend WSARecv等)实际传输的字节数
    _Out_ PULONG_PTR   lpCompletionKey, //原先传递到CreateCompletionPort套接字返回的单句柄数据
    _Out_ LPOVERLAPPED *lpOverlapped,       //接收已完成的I/O操作的WSAOVERLAPPED结构
    _In_  DWORD        dwMilliseconds       //等待一个完成数据包在完成端口上出现的时间(毫秒)
    );
  • 单句柄数据和单I/O操作数据

    ​ 当一个工作器线程从GetQueuedCompletionStatus这个API调用中接收到I/O完成通知后,在lpCompletionKey和lpOverlapped参数中,会包含一些必要的套接字信息。利用这些信息,可通过完成端口,继续在一个套接字上进行I/O处理。通过这些参数,可获得两种重要的套接字数据类型:单句柄数据以及单I/O操作数据。

    ​ 因为在一个套接字首次与完成端口关联到一起的时候,单句柄数据便于一个特定的套接字句柄对应起来了,所以lpCompletionKey参数也就包含了单句柄数据。这些数据正是在进行CreateIoCompletionPort API调用的时候,通过CompletionKey参数传递的。如早先所述,应用程序可通过该参数传递任意类型的数据。通常情况下,应用程序会将I/O请求有关的套接字句柄保存在这里。

    ​ lpOverlapped参数则包含了一个OVERLAPPED结构,在它后面跟随单I/O操作数据。工作器线程处理一个完成数据包时(向应用程序、接受连接以及投递另一个线程等),这些信息是它必须要知道的。单I/O操作数据是包含在一个结构内的、任意数量的字节,这个结构本身也包含了一个结构传递进去,以满足它的要求。想要做到这一点,一个简单的方法是定义一个结构,然后将OVERLAPPED结构作为新结构的第一个元素使用。例如,可定义下述数据结构,实现对单I/O操作数据的管理:

    typedef struct
    {
    OVERLAPPED Overlapped;
    char Buffer[DATA_BUFSIZE];
    int BufferLen;
    int OperationType;
    }PER_IO_DATA;

    ​ 该结构演示了通常要与I/O操作关联在一起的某些重要数据元素,例如刚才完成的那个I/O操作(发送或接收请求)的类型。在这个结构中,我们认为提供给已完成的I/O操作的数据缓冲区是非常有用的。要想调用一个Winsock API函数,同时为其分配一个OVERLAPPED结构,只要简单地撤销对结构中的OVERLAPPED元素的引用即可。

    PER_IO_OPERATION_DATA PerIoData;
    WSABUF wbuf;
    DWORD Bytes,Flags;
    
    //初始化 wbuf.....
    
    WSARecv(socket,&wbuf,&Bytes,&Flags,&(PerIoData.Overlapped),NULL);

    ​ 在工作器线程的后面部分,GetQueuedCompletionStatus函数返回了一个重叠结构和完成键。获取单I/O数据应使用宏CONTAINING_RECORD。例如:

    PER_IO_DATA *PerIoData = NULL;
    OVERLAPPED *lpOverlapped = NULL;
    
    ret = GetQueuedCompletionStatus(ComportHandle, &Transferred, (PULONG_PTR)&CompletionKey, &lpOverlapped, INFINITE);
    //检查成功的返回
    PerIoData = CONTAINING_RECORD(lpOverlapped, PER_IO_DATA, Overlapped);

    ​ 应该使用这个宏,否则,结构PER_IO_DATA的成员OVERLAPPED就始终不得不首先出现,这会成为一个危险的假设。可以使用单I/O结构的一个字段来指示被投递的操作类型,从而可以确定到底是哪个操作投递到了句柄之上。

    • 和Windows 完成端口有关的另一个重要方面:所有重叠操作可确保按照应用程序安排好的顺序执行。然而,不能确保从完成端口返回到完成通知也按照上述顺序执行。也就是说,对于两个重叠WSARecv操作,如果一个应用程序提供前者10KB的缓冲,而后者是12KB的缓冲,则10KB缓冲先被填充。应用程序的工作器线程可能会在10KB操作完成事件之前从GetQueuedCompletionStatus接收到12KB WSARecv的通知。当然,在一个套接字上投递多重操作时,这个问题不会造成多大影响。

    为了完成前面所述的简单回应服务器示例,需要提供一个ServerWorkerThread函数。

//一个简单的IOCP的例子 网络上看到的,稍微修改了点
#include <WinSock2.h>
#include <windows.h>
#include <iostream>

#pragma comment(lib,"WS2_32")

using namespace std;

#define PORT 5050
#define DATA_BUFSIZE  8192

#define OutErr(a) cout<<(a)<<endl<<"file:"<<__FILE__<<endl<<"line:"<<__LINE__<<endl

#define OutMsg(a) cout<<(a)<<endl;

void InitWinsock()
{
    WSADATA wsd;
    if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
    {
        OutErr("WSAstartup()");
    }
}
#if 0
////////////////////////////////////client
#define MAXCNT 30000
#include <WS2tcpip.h>
int main()
{
    InitWinsock();

    static int nCnt = 0;
    char SendBuf[2000];
    while (nCnt < MAXCNT)//循环测试套接字创建关闭的情况 看服务端消息处理
    {
        SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
        SOCKADDR_IN addrSrv;
        //addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
        inet_pton(AF_INET, "127.0.0.1", (void*)&addrSrv.sin_addr.S_un.S_addr);

        addrSrv.sin_family = AF_INET;
        addrSrv.sin_port = htons(5050);//和服务器端的端口号保持一致  
        connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));//连接服务器端(套接字,地址转换,长度)  

        sprintf_s(SendBuf, "This is TestNo : %d\n", ++nCnt);
        send(sockClient, SendBuf, strlen(SendBuf), 0);
        printf("send:%s", SendBuf);

        //           memset(recvBuf,0,100);  
        //           recv(sockClient,recvBuf,100,0);//接收数据  
        //           printf("%s\n",recvBuf);//打印  

        closesocket(sockClient);//关闭套接字  
        Sleep(1000);
    }
    WSACleanup();//终止对这个套接字库的使用  
    return 1;
}

#endif
//////////////////////////////////////////

#if 1

//************************************
// 函数名称:  BindServerOverlapped
// 函数说明:   绑定端口,并返回一个Overlapped的ListenSocket
// 返 回 值:  SOCKET
// 参数说明:  int nPort
// 作    者:  xcyk
// 日    期:  2017/04/21
//************************************
SOCKET BindServerOverlapped(int nPort)
{
    //创建socket
    SOCKET sServer = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
    //绑定端口
    struct sockaddr_in servAddr;
    servAddr.sin_family = AF_INET;
    servAddr.sin_port = htons(PORT);
    servAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);

    if (bind(sServer, (struct sockaddr*)&servAddr, sizeof(servAddr)) < 0)
    {
        OutErr("bind Failed!");
        return NULL;
    }

    //设置监听队列为200
    if (listen(sServer, 200) != 0)
    {
        OutErr("listen Filed!");
        return NULL;
    }
    return sServer;
}

//结构体定义
typedef struct
{
    OVERLAPPED Overlapped;
    WSABUF DataBuf;
    CHAR Buffer[DATA_BUFSIZE];
}PER_IO_OPERATION_DATA, *LPPER_IO_OPERATION_DATA;

typedef struct
{
    SOCKET Socket;
}PER_HANDLE_DATA, *LPPER_HANDLE_DATA;

DWORD WINAPI ProcessIO(LPVOID lpParam)
{
    HANDLE CompletionPort = (HANDLE)lpParam;
    DWORD BytesTransferred;
    LPPER_HANDLE_DATA PerHandleData;
    LPPER_IO_OPERATION_DATA PerIoData;


    while (true)
    {
        if (0 == GetQueuedCompletionStatus(CompletionPort, &BytesTransferred, (LPDWORD)&PerHandleData, (LPOVERLAPPED*)&PerIoData, INFINITE))
        {
            if ((GetLastError()==WAIT_TIMEOUT)||(GetLastError()==ERROR_NETNAME_DELETED))
            {
                cout << "closeingsocket : " << PerHandleData->Socket << endl;
                closesocket(PerHandleData->Socket);
                delete PerIoData;
                delete PerHandleData;
                continue;
            }
            else
            {
                OutErr("GetQueuedCompletionStatus failed!");
            }
            return 0;
        }

        //判断客户端已经是否退出
        if (BytesTransferred == 0)
        {
            cout << "closing socket " << PerHandleData->Socket << endl;
            closesocket(PerHandleData->Socket);
            delete PerIoData;
            delete PerHandleData;
            continue;
        }

        //取得数据并处理
        cout << PerHandleData->Socket << " Send message is :" << PerIoData->Buffer << endl;

        //继续向Socket投递WSARecv操作
        DWORD Flags = 0;
        DWORD dwRecv = 0;
        memset(PerIoData, 0, sizeof(PER_IO_OPERATION_DATA));
        PerIoData->DataBuf.buf = PerIoData->Buffer;
        PerIoData->DataBuf.len = DATA_BUFSIZE;
        WSARecv(PerHandleData->Socket, &PerIoData->DataBuf, 1, &dwRecv, &Flags, &PerIoData->Overlapped, NULL);
    }
    return 0;
}

int main()
{
    InitWinsock();
    HANDLE CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

    //根据系统的CPU来创建工作者线程
    SYSTEM_INFO SystemInfo;
    GetSystemInfo(&SystemInfo);

    //线程数目=系统进程数据的两倍加二
    for (int i = 0; i < SystemInfo.dwNumberOfProcessors * 2 + 2; ++i)
    {
        HANDLE hProcessIO = CreateThread(NULL, 0, ProcessIO, CompletionPort, 0, NULL);
        if (hProcessIO)
        {
            CloseHandle(hProcessIO);
        }
    }
    //创建侦听SOCKET
    SOCKET sListen = BindServerOverlapped(PORT);

    SOCKET sClient;
    LPPER_HANDLE_DATA PerHandleData;
    LPPER_IO_OPERATION_DATA PerIoData;
    while (TRUE)
    {
        //等待客户端接入
        sClient = WSAAccept(sListen, NULL, NULL, NULL, 0);
        //sClient = accept(sListen, 0, 0);
        cout << "Socket " << sClient << "连接进来" << endl;

        PerHandleData = new PER_HANDLE_DATA();
        PerHandleData->Socket = sClient;

        // 将接入的客户端和完成端口联系起来  
        CreateIoCompletionPort((HANDLE)sClient, CompletionPort, (DWORD)PerHandleData, 0);

        // 建立一个Overlapped,并使用这个Overlapped结构对socket投递操作  
        PerIoData = new PER_IO_OPERATION_DATA();

        memset(PerIoData, 0,sizeof(PER_IO_OPERATION_DATA));
        PerIoData->DataBuf.buf = PerIoData->Buffer;
        PerIoData->DataBuf.len = DATA_BUFSIZE;

        // 投递一个WSARecv操作  
        DWORD Flags = 0;
        DWORD dwRecv = 0;
        WSARecv(sClient, &PerIoData->DataBuf, 1, &dwRecv, &Flags, &PerIoData->Overlapped, NULL);
    }

    DWORD dwByteTrans;
    //将一个已经完成的IO通知添加到IO完成端口的队列中.  
    //提供了与线程池中的所有线程通信的方式.  
    PostQueuedCompletionStatus(CompletionPort, dwByteTrans, 0, 0);  //IO操作完成时接收的字节数.  
    closesocket(sListen);

    return 0;
}

#endif

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值