完成端口IOCP

本文深入解析Windows的IO完成端口(IOCP)模型,探讨其非阻塞IO工作方式和线程池管理。通过创建、绑定完成端口与套接字,以及使用GetQueuedCompletionStatus获取已完成的IO信息。示例代码展示了一个简单的回声服务器,说明IOCP如何在多线程环境中高效处理客户端IO请求。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、概述

IOCP就是单独实现一个线程用来处理所有与客户端的IO的模型

不仅仅是只创建一个线程处理IO,而是至少创建一个线程来负责IO前后的全部处理。

理解IOCP重点不要集中于线程,而是观察:1.IO是否以非阻塞模式工作;2.如果确认非阻塞模式的IO是否完成。

IOCP的完成端口不是指TCP/IP端口号,而是类似一个消息队列,由操作系统把已经完成的重叠I/O请求的通知放入其中。当某项I/O完成之后,其对应的工作线程就会收到一个通知,然后进行其他的操作。IOCP是异步I/O,其依赖于一个工作者线程池。使用工作者线程池限制线程的数量以避免创建太多thread而导致在切换线程时浪费大量的时间。

二、主要函数

1.创建完成端口对象

2.建立完成端口对象与套接字直接的联系

IOCP中已完成的IO信息将被注册到完成端口对象。这个过程并非单纯的注册,首先经过请求过程:“该套接字的IO完成时,把状态信息注册到指定CP对象”。

该过程称为“”套接字和CP对象直接的连接请求“。

#include<windows.h>
// 成功返回CP对象句柄,失败返回NULL
HANDLE CreateIoCompletionPort(HANDLE FileHandle, HANDLE ExistingCompletionPort,
     ULONG_PTR CompletionKey,
    DWORD NumberOfConcurrentThreads);
// 创建CP对象时,
// 第一个参数为INVALID_HANDLE_VALUE
// 第二个参数为NULL
// 第三个参数为0
// 第四个参数为指定线程并发数,为0表明为系统支持的最优线程数
// 建立CP对象与套接字联系时
// 第一个参数为连接至CP对象的套接字句柄
// 第二个参数为创建的CP对象
// 第三个参数为传递已完成IO相关信息,在GetQueuedCompletionStatus函数中讨论
// 第四个参数无论传递何值,只要第二个参数为非NULL就会忽略。

绑定之后,只有套接字的IO完成,相关信息就会注册到CP对象中。

3.确认完成端口已完成的IO和线程的IO处理

#include<windows.h>
// 成功返回true, 失败返回false
BOOL GetQueuedCompletionStatus(HANDLE CompletionPort, LPDWORD lpNumberOfBytes,
    PULONG_PTR lpCompletionKey, LPOVERLAPPED *lpOverlapped, DWORD dwMilliseconds);
//CompletionPort 创建并绑定后的完成端口
//lpNumberOfBytes 保存IO过程中传输的数据大小的变量地址值
//lpCompletionKey 用于保存CreateIoCompletionPort绑定的第三个参数的地址值 
//lpOverlapped 用于保存WSARecv和WSASend时传递的Overlapped参数地址的变量地址值
//dwMilliseconds 用于保存超时信息,超过返回false并跳出函数,
    传入INFINITE,程序阻塞至已完成IO信息完成写入CP对象

注意:

CreateIoCompletionPort的第三个参数是结构体,lpCompletionKey保存的时候该指针的地址

lpOverlapped 也是同样的保存指针的地址

GetQueuedCompletionStatus由处理IOCP中已完成的IO的线程调用

如前所书,IOCP创建全职IO线程,由该线程针对所有客户端进行IO。而且reateIoCompletionPort函数中也有参数用于指定分配给CP对象的最大线程数:是否自动创建线程并处理IO?当然不是!

由程序员自行创建调用WSASend和WSARecv等IO线程,只是该线程为了确认IO的完成会调用GetQueuedCompletionStatus函数。

虽然任何线程都能调用GetQueuedCompletionStatus函数,但是实际得到IO完成信息的线程数不会超过CreateIoCompletionPort指定的最大线程数

三、例子

1.回声Server例子

#include <stdio.h>
#include <stdlib.h>
#include <winsock2.h>
#include <process.h>
#include <windows.h>

#define BUF_SIZE 100
#define READ 3
#define WRITE 5

typedef struct
{
    SOCKET hClntSock;
    SOCKADDR clntAdr;
}PER_HANDLE_DATA, *LPPER_HANDLE_DATA;

typedef struct
{
    OVERLAPPED overlapped;
    WSABUF wsaBuf;
    char buffer[BUF_SIZE];
    int rwMode;
}PER_IO_DATA, *LPER_IO_DATA;

DWORD WINAPI EchoThreadMain(LPVOID CompletionPortIO);
void ErrorHandling(char *message);

int main(int argc, char* argv[])
{
    WSADATA wsaData;
    HANDLE hComPort;
    SYSTEM_INFO sysInfo;
    LPER_IO_DATA ioInfo;
    LPER_HANDLE_DATA handleInfo;

    SOCKET hServSock;
    SOCKADDR_IN servAdr;
    int recvBytes,i,flags=0,mode = 1;

    if(argc!=2)
    {
        printf("Usage: %s <prot> \n",argv[0]);
        exit(1);
    }

    if(WSAStartup(MAKEWORD(2,2), &wsaData))
        ErrorHandling("WSAStartup() error");
    
    hComPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0 , 0);
    GetSystemInfo(&sysInfo);
    for(i=0;i<sysInfo.dwNumberOfProcessors;i++)
    {
        _beginthreadex(NULL,0,EchoThreadMain,(LPVOID)hComport,0,NULL);
    }

    hServSock = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
    ioclsocket(hServSock, FINOBIO, &mode);

    memset(&servAdr,0,sizeof(servAdr));
    lisnAdr.sin_family = AF_INET;
    lisnAdr.sin_port = htons(atoi(argv[1]));
    lisnAdr.sin_addr.s_addr = htonl(INADDR_ANY);

    if(SOCKET_ERROR == bind(hServSock,(SOCKADDR*)&servAdr,sizeof(servAdr)))
        ErrorHandling("bind error");
    
    if(SOCKET_ERROR == listen(hServSock,5))
        ErrorHandling("listen error");
    
    while(1)
    {
        SOCKET hClntSock;
        SOCKADDR_IN clntAdr;

        hClntSock = accept(hServSock, (SOCKADDR*)&clntAdr, sizeof(clntAdr));
        handleInfo = (LPER_HANDLE_DATA)malloc(sizeof(PER_HANDLE_DATA));
        handleInfo->hClntSock = hClntSock;
        memcpy(&(handleInfo->clntAdr), &clntAdr, addrLen);

        CreateIoCompletionPort((HANDLE)hClntSock, hComPort, (DWORD)handleInfo,0);

        ioInfo=(LPPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
        memset(&ioInfo->overlapped,0,sizeof(OVERLAPPED));
        ioInfo->wsaBuf.len=BUF_SIZE;
        ioInfo->wsaBuf.buf=ioInfo->buffer;
        // IOCP本身不会帮我区分输入完成还是输出完成状态,无论输入还是输出,只通知IO完成状态
        // 因此需要额外的变量来区分2中IO
        ioInfo->rwMode=Read;
        // 第7个参数overlapped的地址与PER_IO_DATA结构体ioInfo的地址相同,相当于传入了ioInfo结构体的地址
        WSARecv(handleInfo->hClntSock, &(ioInfo->wsaBuf), 1, &recvBytes, &flags, &(ioInfo->overlapped), NULL);
    }
    return 0;
}

包含overlapped的结构体可以包含其他参数,从而通过WSASend和WSARecv可以获得更多的参数信息。

通过其中的标志来决定是发送还是接收,完成端口本身是不会区分的。

而Completion Key是创建完成端口的时候设定的。

通过GetQueuedCompletionStatus的第三个参数和第四个参数,分别是在CreatIoCompletionPort和WSARecv或WSASend的时候绑定的,

2.例子

DWORD WINAPI EchoThreadMain(LPVOID pComPort)
{
    HANDLE hComport = (HANDLE)pComport;
    LPER_IO_DATA ioInfo;
    LPER_HANDLE_DATA handleInfo;
    DWORD transBytes
    SOCKET sock;

    GetQueuedCompletionStatus(hComport, &transBytes, (LPDWORD)&handleInfo, (LPOVERLAPPED*)&ioInof, INFINITE);

    sock = handleInfo->hClntSock;

    if(READ == ioInfo->rwMode)// 表明接收完毕 WSARecv完毕
    {
        puts("message recieve");
        if(transBytes == 0)
        {
            closesocket(sock);
            free(handleInfo);
            free(ioInfo);
            continue;
        }

        memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED));
        ioInfo->wsaBuf.len = transBytes;
        ioInfo->rwMode = WRITE;//表明接下来的操作是要写,发送
        WSASend(hComport, &(ioInfo->wsaBuf), 1, NULL, 0, &(ioInfo->overlapped), NULL);

        ioInfo = (LPER_IO_DATA)malloc(sizeof(PER_IO_DATA));
        memset(&(ioInfo->overlapped), 0, sizeof(OVERLAPPED));
        ioInfo->wsaBuf.buf = ioInfo->Buffer;
        ioInfo->wsaBuf.len = BUF_SIZE;
        ioInof->rwMode = READ;// 表明接下来的状态是要读,接收

        WSARecv(hComport, &(ioInfo->wsaBuf), 1, NULL, 0, &(ioInfo->overlapped), NULL);
    }
}

四、分析

在硬件性能和带宽充足的情况下,响应时间和并发数量出问题,优先考虑两点:

  • 低效的IO结构或低效的CPU使用
  • 数据库设计和查询语句结构

IOCP性能更优的原因:

  • 非阻塞IO,不会因为IO引发延迟
  • 查找已经完成IO的时候不需要循环,select需要遍历套接字数组
  • 无需将作为IO对象的套接字句柄保存到数组中管理
  • 可以调整处理的线程,所以可以在实验数据的基础上选用合适的线程数

最近有项目要做一个高性能网络服务器,决定下功夫搞定完成端口IOCP),最终花了一个星期终于把它弄清楚了,并用C++写了一个版本,效率很不错。 但,从项目的总体需求来考虑,最终决定上.net平台,因此又花了一天一夜弄出了一个C#版,在这与大家分享。 一些心得体会: 1、在C#中,不用去面对完成端口的操作系统内核对象,Microsoft已经为我们提供了SocketAsyncEventArgs类,它封装了IOCP的使用。请参考:http://msdn.microsoft.com/zh-cn/library/system.net.sockets.socketasynceventargs.aspx?cs-save-lang=1&cs-lang=cpp#code-snippet-1。 2、我的SocketAsyncEventArgsPool类使用List对象来存储对客户端来通信的SocketAsyncEventArgs对象,它相当于直接使用内核对象时的IoContext。我这样设计比用堆栈来实现的好处理是,我可以在SocketAsyncEventArgsPool池中找到任何一个与服务器连接的客户,主动向它发信息。而用堆栈来实现的话,要主动给客户发信息,则还要设计一个结构来存储已连接上服务器的客户。 3、对每一个客户端不管还发送还是接收,我使用同一个SocketAsyncEventArgs对象,对每一个客户端来说,通信是同步进行的,也就是说服务器高度保证同一个客户连接上要么在投递发送请求,并等待;或者是在投递接收请求,等待中。本例只做echo服务器,还未考虑由服务器主动向客户发送信息。 4、SocketAsyncEventArgs的UserToken被直接设定为被接受的客户端Socket。 5、没有使用BufferManager 类,因为我在初始化时给每一个SocketAsyncEventArgsPool中的对象分配一个缓冲区,发送时使用Arrary.Copy来进行字符拷贝,不去改变缓冲区的位置,只改变使用的长度,因此在下次投递接收请求时恢复缓冲区长度就可以了!如果要主动给客户发信息的话,可以new一个SocketAsyncEventArgs对象,或者在初始化中建立几个来专门用于主动发送信息,因为这种需求一般是进行信息群发,建立一个对象可以用于很多次信息发送,总体来看,这种花销不大,还减去了字符拷贝和消耗。 6、测试结果:(在我的笔记本上时行的,我的本本是T420 I7 8G内存) 100客户 100,000(十万次)不间断的发送接收数据(发送和接收之间没有Sleep,就一个一循环,不断的发送与接收) 耗时3004.6325 秒完成 总共 10,000,000 一千万次访问 平均每分完成 199,691.6 次发送与接收 平均每秒完成 3,328.2 次发送与接收 整个运行过程中,内存消耗在开始两三分种后就保持稳定不再增涨。 看了一下对每个客户端的延迟最多不超过2秒。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值