剖析windows下的iocp网络模型(附代码+注释)

1.完成端口概述

iocp概述

输入输出完成端口(Input/Output Completion Port,IOCP), 是支持多个同时发生的异步I/O操作的应用程序编程接口,在Windows NT的3.5版本以后,或AIX5版以后或Solaris第十版以后,开始支持。

IOCP特别适合C/S模式网络服务器端模型。因为,让每一个socket有一个线程负责同步(阻塞)数据处理,one-thread-per-client的缺点是:一是如果连入的客户多了,就需要同样多的线程;二是不同的socket的数据处理都要线程切换的代价。

iocp原理

通常的办法是,线程池中的工作线程的数量与CPU内核数量相同,以此来最小化线程切换代价。一个IOCP对象,在操作系统中可关联着多个Socket和(或)文件控制端。 IOCP对象内部有一个先进先出(FIFO)队列,用于存放IOCP所关联的输入输出端的服务请求完成消息。请求输入输出服务的进程不接收IO服务完成通知,而是检查IOCP的消息队列以确定IO请求的状态。 (线程池中的)多个线程负责从IOCP消息队列中取走完成通知并执行数据处理;如果队列中没有消息,那么线程阻塞挂起在该队列。这些线程从而实现了负载均衡。

(上述源自百度百科)

iocp作为一种windows下的网络模型,与reactor类似也要解决以下几个问题:
1. 接收连接(accept),建立连接(connect) 

2. 数据的接收

3. 数据的发送

4. 连接的断开

iocp的处理流程图:

iocp处理流程

1. 接收连接(accept),建立连接(connect) 

1)首先创建socket ----> listenfd

2)讲listenfd 绑定到 iocp 上

3)AcceptEx 投递接收连接的异步请求

.......... (等待端口完成)

4)GetQueuedCompletionStatus 可以拿到完成通知

5)将会得到 socket ----> clientfd

怎么循环往复接收连接?接收完连接后继续投递 AcceptEx 请求即可。

2. 数据的接收

1)将 clientfd 绑定到 

2)WSARecv 投递接收数据的异步请求 WSABuf 

............(wait)

3)GetQueuedCompletionStatus 可以拿到完成通知

4)WSABuf 在内核中以及填充好数据,并且拿到了 TransferBytes

怎么循环往复接收数据?接收完数据后继续投递 WSARecv 请求

3. 数据的发送

1)WSASend 投递发送数据的异步请求 WSABuf

.............(wait)

2)GetQueuedCompletionStatus 可以拿到完成通知

3)得到 WSABuf 当中有多少数据被发送出去

注意:网络编程职责中 数据的发送 只需要把数据拷贝到内核就算完成了

怎么把数据继续发送出去?需要继续投递 WSASend 请求

4. 连接的断开(被动)

1)GetQueuedCompletionStatus

2)TransferBytes = 0

    连接的断开(主动)

1)DisconnectEx

2)CloseSocket

2.什么是重叠IO

同一线程内部向多个目标传输(或从多个目标接收)数据引起的I/O重叠现象称为“重叠I/O”。为了完成这项任务,调用的I/O函数应立即返回,因此前提条件是异步I/O。而且,为了完成异步I/O,调用的I/O函数应以非阻塞模式工作

Windows中重叠I/O的重点并非I/O本身,而是如何确认I/O完成时的状态

重叠io是配合异步编程的重要结构,无需等待上一个io的完成,就可以直接投递下一个io操作请求

一般为了并发的接收更多的连接,服务端启动的时候,一次性可投递多个 AcceptEx 请求

3.同步io和异步io的区别

阻塞式I/O、非阻塞式I/O、I/O复用模型是同步I/O模型,因为在等待数据的过程中,这三种模型中的进程都没有去做别的事情,即便是非阻塞式的轮询,也可以看作是一种同步。

POSIX将同步IO操作定义为“导致请求进程阻塞,直到I/O操作完成”,而书中认为在信号驱动式I/O模型中等待数据的那段时间不算是真正的I/O操作(因为没有调用I/O相关的系统调用),而数据从内核复制到用户空间才是真正的I/O操作(这个时候调用了recvfrom系统调用)。

而异步io首先用户态进程告诉内核态需要什么数据,然后用户态进程就不管了,做别的事情,内核等待用户态需要的数据准备好,然后将数据复制到用户空间,此时才告诉用户态进程,”数据都已经准备好,请查收“,然后用户态进程直接处理用户空间的数据。在复制数据到用户空间这个时间段内,用户态进程也是不阻塞的。

 

int fd = accept(listenfd, &addr, &size);

// accept 是同步io 返回时,意味着 io 操作已经完成

BOOL AcceptEx(
  [in]  SOCKET       sListenSocket,
  [in]  SOCKET       sAcceptSocket,
  [in]  PVOID        lpOutputBuffer,
  [in]  DWORD        dwReceiveDataLength,
  [in]  DWORD        dwLocalAddressLength,
  [in]  DWORD        dwRemoteAddressLength,
  [out] LPDWORD      lpdwBytesReceived,
  [in]  LPOVERLAPPED lpOverlapped
);

// AcceptEx 是异步io 返回时,意味着投递请求的完成,并不代表 io 操作已完成

4.代码 + 注释 

#include <iostream>
#include <winsock2.h>
#include <windows.h>
#include <WinSock2.h>
#include <mswsock.h>
#include <Mswsock.h>
#include <ws2tcpip.h>
#include <string>
#include <cstring>

#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "Mswsock.lib")

#define PORT 8989
#define BUFFER_SIZE 1024
#define START_POST_ACCEPTEX 2       // 并发处理最大连接数

// 注意不要添加 using namespace std; bind 会与 Windows API 冲突

/*
    1. 创建一个 socket
    2. 绑定地址和端口 bind
    3. 监听 listen
    4. iocp
    5. socket 与 iocp 进行关联

    返回:
    1. listensocket
    2. iocp handle
*/

struct ServerParams
{
    SOCKET listenSock;
    HANDLE iocp;
};

enum class IO_OP_TYPE
{
    IO_ACCEPT,
    IO_SEND,
    IO_RECV,
    IO_CONNECT,
    IO_DISCONNECT
};

// 定义一个结构体,用于存储每次IO操作的信息
typedef struct OverlappedPerIO
{
    WSABUF wsaBuf;              // WSABUF 结构体,用于将网络数据存储到用户的缓冲区中(不是实际存储数据的地方)
    char buffer[BUFFER_SIZE];   // 用户缓冲区,用于存储网络数据,实际存储数据的地方
    OVERLAPPED overlapped;      // OVERLAPPED 结构体,用于存储异步操作的状态和结果,我们会将overlapped结构体投递到 iocp 中,然后 iocp 会将overlapped结构体返回给我们,通过类型强制转换为OverlappedPerIO类型,我们就可以获得其他成员的信息,通过这样来定位是哪一次的io请求
    IO_OP_TYPE type;
    SOCKET socket;
} *LPOverlappedPerIO;

void PostAcceptEx(SOCKET listenSocket)
{
    SOCKET sock = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
    if (sock == INVALID_SOCKET)
        return;

    OverlappedPerIO* overlp = new OverlappedPerIO;
    if (overlp == nullptr)
    {
        closesocket(sock);
        return;
    }
    ZeroMemory(overlp, sizeof(OverlappedPerIO)); // 类比 memset
    overlp->type = IO_OP_TYPE::IO_ACCEPT;
    overlp->socket = sock;
    overlp->wsaBuf.buf = overlp->buffer;
    overlp->wsaBuf.len = BUFFER_SIZE;

    DWORD dwByteRecv = 0;
    while (false == AcceptEx(listenSocket, sock, overlp->wsaBuf.buf, 0, sizeof(SOCKADDR_IN) + 16, sizeof(SOCKADDR_IN) + 16, &dwByteRecv, (LPOVERLAPPED)overlp))
    {
        if (WSAGetLastError() == WSA_IO_PENDING)
            break;

        std::cout << WSAGetLastError() << std::endl;
    }
}

DWORD WINAPI workerThread(LPVOID lpParam)
{
    ServerParams* pms = (ServerParams*)lpParam;
    HANDLE completionPort = pms->iocp;
    SOCKET listenSocket = pms->listenSock;

    DWORD bytesTrans;
    ULONG_PTR completionKey;
    LPOverlappedPerIO overlp;
    int ret;

    while (true)
    {
        // GetQueuedCompletionStatus 从 iocp 中获取一个已经完成的异步操作;这里通过循环不断获取已经完成的异步操作
        BOOL result = GetQueuedCompletionStatus(
            completionPort,
            &bytesTrans,
            &completionKey,
            (LPOVERLAPPED*)&overlp,
            INFINITE);

        // 如果获取失败了的流程
        if (!result)
        {
            // 我们则要根据返回的错误来判断一下是不是 wait_timeout 和 error_netname_deleted 错误,这两个错误都是代表我们的连接断开了
            if ((GetLastError() == WAIT_TIMEOUT) || (GetLastError() == ERROR_NETNAME_DELETED))
            {
                // 如果是连接断开了,则接下来对socket进行连接关闭和删除
                std::cout << "socket disconnect: " << overlp->socket << std::endl;
                closesocket(overlp->socket);
                delete overlp;
                continue;
            }
            std::cout << "GetQueuedCompletionStatus false " << std::endl;
            return 0; // 如果不是这两个错误,则说明发生了其他错误,我们直接退出线程
        }

        // 成功后的正常流程
        switch (overlp->type)
        {
            // 如果接受的是连接请求,则马上投递一个接受连接的请求
        case IO_OP_TYPE::IO_ACCEPT:
        {
            PostAcceptEx(listenSocket);
            std::cout << "happened io accept:" << bytesTrans << std::endl;
            // 利用 SO_UPDATE_ACCEPT_CONTEXT 参数,完成客户端socket的关联操作
            setsockopt(overlp->socket, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT, (char*)&listenSocket, sizeof(listenSocket));

            // buffer 的清空 和 overlapped 的清空 与 初始化,overlp 既可以复用 也可以去重新创建,这里是可以多线程编程的点
            ZeroMemory(overlp, BUFFER_SIZE);
            overlp->type = IO_OP_TYPE::IO_RECV;    // 设置类型为接收数据
            overlp->wsaBuf.buf = overlp->buffer;   // buffer 的关联
            overlp->wsaBuf.len = BUFFER_SIZE;      // buffer 的长度设置
            CreateIoCompletionPort((HANDLE)overlp->socket, completionPort, NULL, 0);

            // 接下来通过WSARecv投递一个请求,用于接收客户端发送过来的数据
            DWORD dwRecv = 0, dwFlags = 0;
            ret = WSARecv(overlp->socket, &overlp->wsaBuf, 1, &dwRecv, &dwFlags, &(overlp->overlapped), 0);
            if (ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING)
            {
                std::cout << "WSARecv failed: " << WSAGetLastError() << std::endl;
            }
        }
        break;
        case IO_OP_TYPE::IO_RECV:
        {
            std::cout << "happened io recv: " << bytesTrans << std::endl;
            if (bytesTrans == 0)
            {
                std::cout << "socket disconnect: " << overlp->socket << std::endl;
                closesocket(overlp->socket);
                delete overlp;
                continue;
            }

            std::cout << "recv data: " << overlp->buffer << std::endl;

            ZeroMemory(&overlp->overlapped, sizeof(OVERLAPPED));
            overlp->type = IO_OP_TYPE::IO_SEND;
            overlp->wsaBuf.buf = "response from server\n";
            overlp->wsaBuf.len = strlen(overlp->wsaBuf.buf);

            DWORD dwSend = 0;
            ret = WSASend(overlp->socket, &overlp->wsaBuf, 1, &dwSend, 0, &(overlp->overlapped), 0);
            if (ret == SOCKET_ERROR && WSAGetLastError() != WSA_IO_PENDING)
            {
                std::cout << "WSARecv failded: " << WSAGetLastError() << std::endl;
            }
        }
        break;
        case IO_OP_TYPE::IO_SEND:
        {
            std::cout << "happened io send: " << bytesTrans << std::endl;
            if (bytesTrans == 0)
            {
                std::cout << "socket disconnect: " << overlp->socket << std::endl;
                closesocket(overlp->socket);
                delete overlp;
                continue;
            }
        }
        break;
        }
    }
}

// 采用回滚编程方式初始化
int InitServer(ServerParams& pms)
{
    WSADATA wsaData;
    auto ret = WSAStartup(MAKEWORD(2, 2), &wsaData); // 初始化 winsock

    if (ret == 0)
    {
        // WSA_FLAG_OVERLAPPED 关键参数,设置为可重叠的
        pms.listenSock = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);

        if (pms.listenSock != INVALID_SOCKET)  // socket 创建成功
        {
            // 绑定ip,端口和协议族
            sockaddr_in addr;
            addr.sin_family = AF_INET;
            addr.sin_port = htons(PORT);
            addr.sin_addr.s_addr = INADDR_ANY;

            ret = bind(pms.listenSock, (sockaddr*)&addr, sizeof(addr));
            if (ret == 0)    // 绑定成功
            {
                ret = listen(pms.listenSock, SOMAXCONN);  // 开始监听,SOMAXCONN 是最大连接数
                if (ret == 0)  // 监听成功
                {
                    pms.iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0);  // 创建一个 iocp handle
                    if (pms.iocp != NULL) // 如果不是一个空的handle,说明iocp handle 创建成功辣
                    {
                        /*
                          HANDLE CreateIoCompletionPort(
                            HANDLE FileHandle,               // 要与完成端口关联的文件句柄。对于网络编程,这通常是套接字句柄
                            HANDLE ExistingCompletionPort,   // 如果不为 NULL,则将 FileHandle 添加到 ExistingCompletionPort 中。如果为 NULL,则创建一个新的完成端口。
                            ULONG_PTR CompletionKey,         // 与 FileHandle 关联的用户定义的值。在调用 GetQueuedCompletionStatus 时,这个值会返回,可以用来区分不同的 I/O 操作。
                            DWORD NumberOfConcurrentThreads  // 指定完成端口可以同时运行的线程数。如果为 0,则使用系统默认的线程数。
                          );

                          成功时返回创建的 I/O 完成端口的句柄。

                          失败时返回 INVALID_HANDLE_VALUE,并调用 GetLastError 获取错误代码
                        */
                        if (CreateIoCompletionPort((HANDLE)pms.listenSock, pms.iocp, NULL, 0) != NULL) // 将 listenSocket 与 iocp 进行关联
                        {
                            return 0;
                        }
                        CloseHandle(pms.iocp);  // 关联失败,关闭 iocp handle
                    }
                }
            }
            closesocket(pms.listenSock);  // 绑定失败了 关闭socket
        }
        WSACleanup(); // 失败了则 调用WSACleanup 清理 winsock
    }
    if (ret == 0) ret = -1;  // WSAStartup 失败
    return ret;
}

int main()
{
    int ret;
    ServerParams pms{ 0 };
    ret = InitServer(pms);

    if (ret != 0)
    {
        std::cout << "InitServer failed" << std::endl;
        return -1;
    }

    for (int i = 0; i < 2; ++i)
        CreateThread(NULL, 0, workerThread, &pms, 0, NULL);

    for (int i = 0; i < START_POST_ACCEPTEX; ++i)
        PostAcceptEx(pms.listenSock);

    (getchar());

    closesocket(pms.listenSock);
    CloseHandle(pms.iocp);
    WSACleanup();

    return 0;
}

整体构成没太大问题,拉到vs上可直接运行,但是在接受数据和处理部分并不完整导致可能有些缺陷,个人感觉比 reactor + epoll 要麻烦不少

5.对比与总结

IOCP(Input/Output Completion Port)和Reactor都是用于处理并发I/O操作的事件驱动模型,它们各有优缺点。

IOCP(Input/Output Completion Port)的优点包括:

  1. 高效的并发处理:IOCP可以同时处理大量并发I/O操作,而不会导致系统资源耗尽。

  2. 低延迟:IOCP通过将I/O操作请求提交给系统内核,避免了应用程序在等待I/O操作完成时的阻塞,从而降低了延迟。

  3. 灵活的I/O操作类型:IOCP支持多种I/O操作类型,包括文件I/O、网络I/O等。

  4. 可扩展性:IOCP可以轻松地扩展到处理更多的并发I/O操作,而不会影响应用程序的性能。

IOCP的缺点包括:

  1. 复杂性:IOCP的编程模型相对复杂,需要开发者对操作系统内核有一定的了解。

  2. 适用于Windows平台:IOCP是Windows操作系统中的一个特性,不适用于其他操作系统。

Reactor的优点包括:

  1. 简单易用:Reactor的编程模型相对简单,易于理解和实现。

  2. 适用于多种操作系统:Reactor可以在多种操作系统上使用,包括Windows、Linux等。

  3. 高效的并发处理:Reactor可以同时处理大量并发I/O操作,而不会导致系统资源耗尽。

  4. 低延迟:Reactor通过将I/O操作请求提交给事件循环,避免了应用程序在等待I/O操作完成时的阻塞,从而降低了延迟。

Reactor的缺点包括:

  1. 适用于单线程:Reactor模型通常在单线程中运行,如果需要处理大量并发I/O操作,可能需要使用多线程或线程池。

  2. 适用于I/O密集型任务:Reactor模型在处理I/O密集型任务时性能较好,但如果需要处理大量计算密集型任务,可能需要使用其他模型。

总的来说,IOCP和Reactor都是处理并发I/O操作的有效模型,选择哪个模型取决于具体的应用场景和需求。

  • 15
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值