• 网络编程——完成端口


    一、创建 I/O 完成端口对象

    使用这种模型之前,首先要创建一个 I/O 完成端口对象,需要调用 CreateCompletionPort 函数HANDLE WINAPI CreateIoCompletionPort(

      __in          HANDLE FileHandle,

      __in          HANDLE ExistingCompletionPort,

      __in          ULONG_PTR CompletionKey,

      __in          DWORD NumberOfConcurrentThreads

    );

    要注意该函数有两个功能:

    ● 用于创建一个完成端口对象;HANDLECompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

    如果仅仅为了创建一个完成端口对象,需要特别注意的参数便是 NumberOfConcurrentThreads(并发线程的数量),它定义了在一个完成端口上,同时允许执行的线程数量。 理想情况下,我们希望每个处理器各自负责一个线程的运行,为完成端口提供服务,避免过于频繁的线程上下文切换。 若将该参数设为 0,表明系统内安装了多少个处理器,便允许同时运行多少个工作者线程!

    ● 将一个句柄同完成端口对象关联到一起。

    CreateIoCompletionPort((HANDLE)sClient,CompletionPort, (DWORD)PerHandleData, 0);

    参数解析:

    其中第一个参数是句柄,可以是文件句柄、SOCKET句柄;

    第二个就是我们上面创建出来的完成端口;

    第三个参数很关键,叫做PerHandleData,对应于每个句柄的数据块。我们可以使用这个参数在后面取到与这个SOCKET对应的数据。

    最后一个参数给0,意思就是根据CPU的个数,允许尽可能多的线程并发执行。

    ★1、工作者线程与完成端口

    Q:成功创建一个完成端口后,便可开始将套接字句柄与其关联到一起。但在关联套接字之前,首先必须创建一个或多个“工作者线程”,以便在 I/O 请求投递给完成端口后,为完成端口提供服务。在这个时候,大家或许会觉得奇怪,到底应创建多少个线程,以便为完成端口提供服务呢?

    需要说明一点,我们调用 CreateIoComletionPort 时指定的并发线程数量,与打算创建的工作者线程数量,它们不是同一回事。

    CreateIoCompletionPort 函数的 NumberOfConcurrentThreads 参数明确指示操作系统在一个完成端口上,一次只允许 n 个工作者线程运行。假如在完成端口上创建的工作者线程数量超出 n 个,那么在同一时刻,也只允许n个线程运行。

    Q:那么,为何实际创建的工作者线程数量有时要比 CreateIoCompletionPort 函数设定的多一些呢?这样做有必要吗?

    假定我们的某个工作者线程调用了一个函数,比如 Sleep 或 WaitForSingleObject,线程此时就进入了暂停(锁定或挂起)状态,那么允许另一个线程代替它的位置。用个生活中的例子来比喻,足球比赛的人数是11人(正式队员),但是去比赛现场的还包括替补队员,这里的“正式队员”好比CreateIoComletionPort 时指定的并发线程数量N1,而创建工作线程N2往往比比N1多,多出来的线程就好比替补队员,但正式队员在场上受伤、累了、或者需要临时休息时,这时候替补队员就顶正式队员的位置,发挥作用。尽可能高效的利用CPU的时间片。

    ==========================================

    一旦在完成端口上拥有足够多的工作者线程来为 I/O 请求提供服务,便可着手将套接字句柄同完成端口关联到一起。

    这要求我们在一个现有的完成端口上,调用 CreateIoCompletionPort 函数,

    同时为前三个参数— FileHandle,ExistingCompletionPort 和 CompletionKey—提供套接字的信息。

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

    ● ExistingCompletionPort 参数指定的是一个现有的完成端口;

    ● CompletionKey(完成键)参数指定与某个套接字句柄关联在一起的“单句柄数据”,可将其作为指向一个数据结构的指针,

    在此数据结构中,同时包含了套接字的句柄,以及与套接字有关的其他信息,如 IP 地址等。为完成端口提供服务的线程函数可通过这个参数,取得与套接字句柄有关的信息。

    Q:使用完成端口模型来开发一个服务器程序,大概步骤是什么呢?

    我们按照以下步骤进行:

    1) 创建一个完成端口,最后参数指定CPU数量

    2) 判断系统处理器数量;

    3) 根据处理器数量创建工作者线程,处理完成I/O通知

    4) 创建监听套接字,绑定,侦听,然后将监听socket和完成端口绑定,帮助我们进行监听工作。

    5) 使用 accept 函数,接受客户端连接请求;

    6) 创建一个数据结构,保存新连接的socket句柄和网络地址信息

    7) 调用CreateIoCompletionPort函数,将从accept返回的新套接字句柄同完成端口关联到一起,

    通过完成键(CompletionKey)参数,将单句柄数据结构传递给 CreateIoCompletionPort 函数;

    8) 开始在已接受的连接上进行 I/O 操作,在此,我们希望通过重叠 I/O 机制,在新建的套接字上投递一个或多个异步 WSARecv 或 WSASend 请求。这些 I/O 请求完成后,一个工作者线程会为 I/O 请求提供服务,同时继续处理未来的其他 I/O 请求,稍后便会在步骤 3) 指定的工作者例程中,体验到这一点;

    9)重复步骤 5) ~ 8),直至服务器中止。

    代码如下:

    HANDLE CompletionPort;

    WSADATA wsd;

    SYSTEM_INFO SystemInfo;

    SOCKADDR_IN InternetAddr;

    SOCKET Listen;

    int i;

    typedef struct _PER_HANDLE_DATA

    {

    SOCKET Socket;

    SOCKADDR_STORAGE  ClientAddr;

    // Other information useful to be associated with the handle

    } PER_HANDLE_DATA, * LPPER_HANDLE_DATA;

    // Load Winsock

    StartWinsock(MAKEWORD(2,2), &wsd);

    // Step 1:

    //  创建一个完成端口

    CompletionPort = CreateIoCompletionPort(

        INVALID_HANDLE_VALUE, NULL, 0, 0);

    // Step 2:

    // 判断系统内到底安装了多少个处理器

    GetSystemInfo(&SystemInfo);

    // Step 3:

    // 根据处理器的数量创建工作者线程

    for(i = 0; i < SystemInfo.dwNumberOfProcessors; i++)

    {

        HANDLE ThreadHandle;

        // Create a server worker thread, and pass the

        // completion port to the thread. NOTE: the

        // ServerWorkerThread procedure is not defined

        // in this listing.

        ThreadHandle = CreateThread(NULL, 0,

            ServerWorkerThread, CompletionPort,

            0, NULL;

        // Close the thread handle

        CloseHandle(ThreadHandle);

    }

    // Step 4:

    // 准备好一个监听套接字

    Listen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0,

        WSA_FLAG_OVERLAPPED);

    InternetAddr.sin_family = AF_INET;

    InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);

    InternetAddr.sin_port = htons(9527);

    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);

        // Step 5:

        // 接受客户端的连接

        Accept = WSAAccept(Listen, (SOCKADDR *)&saRemote,

        &RemoteLen);

        // Step 6:

        // 创建一个数据结构,用于容纳“单句柄数据”

        PerHandleData = (LPPER_HANDLE_DATA)GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA));

        printf("Socket number %d connected ", Accept);

        PerHandleData->Socket = Accept;

        memcpy(&PerHandleData->ClientAddr, &saRemote, RemoteLen);

        // Step 7:

        // 调用 CreateIoCompletionPort 函数,将从 accept 返回的新套接字句柄同完成端口关联到一起

        CreateIoCompletionPort((HANDLE) Accept,

            CompletionPort, (DWORD) PerHandleData, 0);

        // Step 8:

        //  开始在已接受的连接上进行 I/O 操作

        WSARecv(...);

    }

    DWORD WINAPI ServerWorkerThread(LPVOID lpParam)

    {

        // The requirements for the worker thread will be

        // discussed later.

        return 0;

    }

    ★2、完成端口和重叠 I/O(工作者线程要做的事情)

    将套接字句柄与一个完成端口关联在一起后,便可投递发送与接收请求,开始对 I/O 请求的处理。

    接下来,可开始依赖完成端口,来接收有关 I/O 操作完成情况的通知。

    从本质上说,完成端口模型利用了 Win32 重叠 I/O 机制。在这种机制中,象 WSASend 和 WSARecv 这样的 WinsockAPI 调用会立即返回。

    此时,需要由我们的应用程序负责在以后的某个时间,通过一个 OVERLAPPED 结构,来接收之前调用请求的结果。

    在完成端口模型中,要想做到这一点,需要使用 GetQueuedCompletionStatus(获取排队完成状态)函数,

    让一个或多个工作者线程在完成端口上等待 I/O 请求完成的通知。该函数的定义如下:

    BOOL WINAPI GetQueuedCompletionStatus(

      __in          HANDLE CompletionPort,

      __out         LPDWORD lpNumberOfBytes,

      __out         PULONG_PTR lpCompletionKey,

      __out         LPOVERLAPPED* lpOverlapped,

      __in          DWORD dwMilliseconds

    );

    ● CompletionPort 参数对应于要在上面等待的完成端口;

    ● lpNumberOfBytes 参数负责在完成了一次 I/O 操作后(如:WSASend 或 WSARecv),接收实际传输的字节数。

    ● lpCompletionKey 参数为原先传递给CreateIoCompletionPort 函数第三个参数“单句柄数据”,如我们早先所述,大家最好将套接字句柄保存在这个“键”(Key)中。

    ● lpOverlapped 参数用于接收完成 I/O 操作的重叠结果。这实际是一个相当重要的参数,因为可用它获取每个 I/O 操作的数据。

    ● dwMilliseconds 参数用于指定希望等待一个完成数据包在完成端口上出现的时间,即,超时时间。假如将其设为 INFINITE,会一直等待下去。

    Q:“单句柄数据”和 “单 I/O 操作数据”到底是什么?怎么来理解他们?

    工作者线程调用GetQueuedCompletionStatus 函数获取到 I/O 完成通知后,lpCompletionKey 和 lpOverlapped 参数中保存着返回的信息。

    lpCompletionKey参数包含了“单句柄数据”,因为在一个套接字首次与完成端口关联到一起的时候,

    那些数据便与一个特定的套接字句柄对应起来了。这些数据正是我们在调用 CreateIoCompletionPort 函数时候,通过 CompletionKey 参数传递的。

    lpOverlapped 包含“单IO操作数据”。例如每次的WSARecv/WSASend等等。例如我们自己定义一个结构体:

    将 OVERLAPPED 结构作为新结构的第一个元素使用,自己如果还有需要的话,则在OVERLAPPED 结构的后面添加即可。

    Q:调用函数时传递的是指向OVERLAPPED 结构的指针,返回其在内存中的地址,而这个地址也就是我们定义的这个结构体的地址,然后我们就可以使用这个结构体了。

    typedef struct

    {

    OVERLAPPED Overlapped;

    WSABUF DataBuf;

    char szBuffer[DATA_BUF_SIZE];

    int OperationType;

    } PER_IO_OPERATION_DATA;

    该结构演示了通常与 I/O 操作关联的一些重要的数据元素,比如刚才完成的那个 I/O 操作的类型(发送或接收请求),用 OperationType 字段表示,已完成 I/O 操作数据的缓冲区 szBuffer 也是非常有用的。如果想调用一个 Winsock API 函数(如:WSASend、WSARecv),要为其分配一个 OVERLAPPED 结构,

    这时,就可以将我们的结构强制转换成一个 OVERLAPPED 指针,或者从结构中将 OVERLAPPED 元素的地址取出来。如下例所示:

    PER_IO_OPERATION_DATA PerIoData;

    //可以这样调用:

    WSARecv(socket, ..., (OVERLAPPED *)&PerIoData);

    //也可以这样调用:

    WSARecv(socket, ..., &(PerIoData.Overlapped));

    在工作线程的后面部分,等 GetQueuedCompletionStatus 函数返回了一个重叠结构(和完成键)后,便可通过 OperationType 成员,看出到底是哪个操作投递到了这个句柄之上。

    DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID)

    {

        HANDLE CompletionPort = (HANDLE) CompletionPortID;

        DWORD BytesTransferred;

        LPOVERLAPPED Overlapped;

        LPPER_HANDLE_DATA PerHandleData;

        LPPER_IO_DATA PerIoData;

        DWORD SendBytes, RecvBytes;

        DWORD Flags;

        while(TRUE)

        {

            // Wait for I/O to complete on any socket

            // associated with the completion port

            ret = GetQueuedCompletionStatus(CompletionPort,

                &BytesTransferred,(LPDWORD)&PerHandleData,

                (LPOVERLAPPED *) &PerIoData, INFINITE);

            // First check to see if an error has occurred

            // on the socket; if so, close the

            // socket and clean up the per-handle data

            // and per-I/O operation data associated with

            // the socket

            if (BytesTransferred == 0 &&

                (PerIoData->OperationType == RECV_POSTED ││

                 PerIoData->OperationType == SEND_POSTED))

            {

                // A zero BytesTransferred indicates that the

                // socket has been closed by the peer, so

                // you should close the socket. Note:

                // Per-handle data was used to reference the

                // socket associated with the I/O operation.

                closesocket(PerHandleData->Socket);

                GlobalFree(PerHandleData);

                GlobalFree(PerIoData);

                continue;

            }

            // Service the completed I/O request. You can

            // determine which I/O request has just

            // completed by looking at the OperationType

            // field contained in the per-I/O operation data.

             if (PerIoData->OperationType == RECV_POSTED)

            {

                // Do something with the received data

                // in PerIoData->Buffer

            }

            // Post another WSASend or WSARecv operation.

            // As an example, we will post another WSARecv()

            // I/O operation.

            Flags = 0;

            // Set up the per-I/O operation data for the next

            // overlapped call

            ZeroMemory(&(PerIoData->Overlapped),

                sizeof(OVERLAPPED));

            PerIoData->DataBuf.len = DATA_BUFSIZE;

            PerIoData->DataBuf.buf = PerIoData->Buffer;

            PerIoData->OperationType = RECV_POSTED;

            WSARecv(PerHandleData->Socket,

                &(PerIoData->DataBuf), 1, &RecvBytes,

                &Flags, &(PerIoData->Overlapped), NULL);

        }

    }

    ★4、正确地关闭 I/O 完成端口

    如何正确地关闭 I/O 完成端口,特别是同时运行了一个或多个线程,在几个不同的套接字上执行 I/O 操作的时候。

    要避免的一个重要问题是在进行重叠 I/O 操作的同时,强行释放一个 OVERLAPPED 结构。

    要想避免出现这种情况,最好的办法是针对每个套接字句柄,调用 closesocket 函数,任何尚未进行的重叠 I/O 操作都会完成。一旦所有套接字句柄都已关闭,

    便需在完成端口上,终止所有工作者线程的运行。要想做到这一点,需要使用 PostQueuedCompletionStatus 函数,向每个工作者线程都发送一个特殊的完成数据包。

    该函数会指示每个线程都“立即结束并退出”。下面是 PostQueuedCompletionStatus 函数的定义:

    BOOL WINAPI PostQueuedCompletionStatus(

      __in          HANDLE CompletionPort,

      __in          DWORD dwNumberOfBytesTransferred,

      __in          ULONG_PTR dwCompletionKey,

      __in          LPOVERLAPPED lpOverlapped

    );

    ● CompletionPort 参数指定想向其发送一个完成数据包的完成端口对象;

    ● 而就 dwNumberOfBytesTransferred、dwCompletionKey 和 lpOverlapped 三个参数来说,每一个都允许我们指定一个值,

    直接传递给 GetQueuedCompletionStatus 函数中对应的参数。这样一来,一个工作者线程收到传递过来的三个 GetQueuedCompletionStatus 函数参数后,

    便可根据由这三个参数的某一个设置的特殊值,决定何时或者应该怎样退出。

    例如,可用 dwCompletionPort 参数传递 0 值,而一个工作者线程会将其解释成中止指令。

    一旦所有工作者线程都已关闭,便可使用 CloseHandle 函数,关闭完成端口,最终安全退出程序。

    现在,我们来个总结:

    Q:微软为什么要提出完成端口模型,以及完成端口模型是为了解决什么问题的呢?

    第一点,写高性能的服务器程序,要求通信一定要是异步的。

    异步通信就 是在咱们与外部的I/O设备进行打交道的时候,我们没有必要等待着I/O操作完成再执行后续的代码,而是将这个请求交给设备的驱动程序自己去处理,我们的线程可以继续做其他更重要的事情,大体的流程如 下图所示:

    在Windows中实现异步的机制同样有好几种,关键在于“通知应用程序处理网络数据”这一步上。

    第二点,使用“同步通信(阻塞通信)+多线程”的方式,服务器端在每一个客户端连入之后,都要启动一个新的Thread和客户端进行通信,有多少个客户端,就需要启动多少个线程,对吧?但是由于这些线程都是处于运行状态,所以系统不得不在所有可运行的线程之间进行上下文的切换,我们自己是没啥感觉,但是CPU却痛苦不堪,因为线程切换是相当浪费CPU时间的,如果客户端的连入线程过多,这就会弄得CPU都忙着去切换线程了,根本没有多少时间去执行线程体了,所以效率是非常低下的,承认坑爹了不?

    第三点,微软提出完成端口模型的初衷,就是为了解决这种"one-thread-per-client"的缺点的,它充分利用内核对象的调度,只使用少量的几个线程来处理和客户端的所有通信,消除了无谓的线程上下文切换,最大限度的提高了网络通信的性能。

    第四点,这里我想要解释的是,重叠结构Overlapped是异步通信机制实现的一个核心数据结构,因为你看到后面的代码你会发现,几乎所有的网络操作例如发送/接收之类的,都会 用WSASend()和WSARecv()代替,参数里面都会附带一个重叠结构,这是为什么呢?因为重叠结构我们就可以理解成为是一个网络操作的ID号, 也就是说我们要利用重叠I/O提供的异步机制的话,每一个网络操作都要有一个唯一的ID号,因为进了系统内核,里面黑灯瞎火的,也不了解上面出了什么状况,一看到有重叠I/O的调用进来了,就会使用其异步机制,并且操作系统就只能靠这个重叠结构带有的ID号来区分是哪一个网络操作了,然后内核里面处理完毕之后,根据这个ID号,把对应的数据传上去。

    PS:完成端口的基本原理和一些基本的操作我是懂了,但是还需要多多的写代码来实践,不断的提高写的服务器的效率,加油,少年!

    附上的程序是csdn上比较不错的源代码,我自己写的代码就先不发了,担心误人子弟哈,哈哈

  • 相关阅读:
    crt key转p12, jks p12互转,windows生成jks,
    使用c语言实现在linux下的openssl客户端和服务器端编程
    AES CFB/OFB/ECB/CBC/CTR优缺点
    SSL握手通信详解及linux下c/c++ SSL Socket代码举例
    SSL握手通信详解及linux下c/c++ SSL Socket代码举例(另附SSL双向认证客户端代码)
    对称加密和分组加密中的四种模式(ECB、CBC、CFB、OFB)
    Mosquitto服务器的搭建以及SSL/TLS安全通信配置
    openssl详解
    使用 openssl 生成证书
    字符编码的故事:ASCII,GB2312,Unicode,UTF-8,UTF-16
  • 原文地址:https://www.cnblogs.com/codergeek/p/3433178.html
Copyright © 2020-2023  润新知