WinSock完成端口I/O模型

article/2025/9/26 18:19:22

关于重叠I/O,参考《WinSock重叠I/O模型》;关于完成端口的概念及内部机制,参考译文《深度探索I/O完成端口》。

完成端口对象取代了 WSAAsyncSelect 中的消息驱动和 WSAEventSelect 中的事件对象,当然完成端口模型的内部机制要比 WSAAsyncSelect 和 WSAEventSelect 模型复杂得多。

IOCP 内部机制如下图所示:


1.创建完成端口

   在 WinSock 中编写完成端口程序,首先要调用 CreateIoCompletionPort 函数创建完成端口。其原型如下:

WINBASEAPI HANDLE WINAPI

CreateIoCompletionPort(

       HANDLE FileHandle,

       HANDLE ExistingCompletionPort,

       DWORD CompletionKey,

       DWORD NumberOfConcurrentThreads );


第一次调用此函数创建一个完成端口时,通常只关注 NumberOfConcurrentThreads,它定义了在完成端口上同时允许执行的线程数量。一般设为0,表示系统内安装了多少个处理器,便允许同时运行多少个线程为完成端口提供服务。每个处理器各自负责一个线程的运行,避免了过于频繁的线程上下文切换。


hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);


这个类比重叠I/O事件通知模型中(WSA)CreateEvent


然后再调用 GetSystemInfo(&SystemInfo);取得系统安装的处理器的个数 SystemInfo.dwNumberOfProcessors,根据CPU创建线程池,在完成端口上,为已完成的I/O请求提供服务。一般线程池的规模,即 线程数 = CPU*2+2


下面的代码片段演示了线程池的创建。


// 创建线程池,规模为CPU数的两倍

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

   {

      HANDLE ThreadHandle;

      // 创建一个工作线程,并将完成端口作为参数传递给它。

      if ((ThreadHandle = CreateThread(NULL, 0, WorkerThread, hCompletionPort,

         0, &ThreadID)) == NULL)

      {

         printf("CreateThread() failed with error %d/n", GetLastError());

         return;

      }

      // 关闭线程句柄

      CloseHandle(ThreadHandle);

   }


2.将套接字句柄关联到完成端口

然后需要将一个句柄与已经创建的完成端口关联起来这里主要指套接字(AcceptSocket),以后针对这个套接字的I/O完成状态交由完成端口通知,程序接到完成通知后做善后处理。

这需要再次调用 CreateIoCompletionPort 函数(囧)。参数四 NumberOfConcurrentThreads 依旧填0,参数一一般就是 AcceptSocket,参数二为上面创建的完成端口 hCompletionPort。参数三即完成键,一般存放套接字句柄的背景信息,也就是所谓的单句柄数据。之所以把它叫作单句柄数据,因为它是用来保存参数一套接字句柄的关联信息。一般可简单定义如下:


typedef struct {

        SOCKET Socket;

} PER_HANDLE_DATA, * LPPER_HANDLE_DATA;


下面的代码片段演示了每次 Accept 返回时,调用 CreateIoCompletionPort 使返回的 AcceptSocket 与完成端口关联,并传递一个 PerHandleData


AcceptSocket = WSAAccept(Listen, NULL, NULL, NULL, 0);

PerHandleData->Socket = AcceptSocket;

CreateIoCompletionPort((HANDLE) AcceptSocket, hCompletionPort, (DWORD) PerHandleData, 0);


这个类比重叠I/O事件通知模型中设置(WSAOVERLAPPED结构中的 hEvent 字段,使一个事件对象句柄同一个文件/套接字关联起来。


3.投递重叠I/O请求

将套接字句柄与一个完成端口关联在一起后,便可以套接字句柄为基础,投递发送与接收请求,开始对I/O请求的处理。接下来,可开始依赖完成端口,来接收有关I/O操作完成情况的通知。从本质上说,完成端口模型利用了Win32重叠I/O机制。在这种机制中,像 WSARecv()和 WSASend()这样的 WinSock API 调用会立即返回。此时,需要由我们的应用程序负责在以后的某个时间,通过一个OVERLAPPED结构,来接收调用的结果。


(WSA)OVERLAPPED 扩展结构的一种定义如下:


typedef struct{

   OVERLAPPED Overlapped;

   WSABUF DataBuf;

   CHAR Buffer[DATA_BUFSIZE];

   DWORD BytesSEND;

   DWORD BytesRECV;

}OVERLAPPEDPLUS,PER_IO_OPERATION_DATA,*LPPER_IO_OPERATION_DATA;


这里的最后两个参数 BytesSEND 和 BytesRECV 与 GetQueuedCompletionStatus 函数返回时的 ByteTransfered 参数一起同步收发操作。

一般在调用 CreateIoCompletionPort 将套接字句柄与完成端口 hCompletionPort 关联后,还需要为 AcceptSocket 创建 PerIOData,以便为后面调用 WSARecv()/WSASend() 提供(WSA)OVERLAPPED结构和缓冲区。

为确保单I/O数据的生存周期延续到I/O完成,一般动态分配,待I/O完成再回收。对于I/O频繁的系统,则可以使用内存池,每次只是回收到空闲池,最后再真正释放。这样,可避免频繁的内存分配。可参考《MFC基于CPlex结构的内存池化管理》。

下面的是Accept返回,调用CreateIoCompletionPort之后的代码片段。


ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));

    PerIoData->BytesSEND = 0;

    PerIoData->BytesRECV = 0;

    PerIoData->DataBuf.len = DATA_BUFSIZE;

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


然后调用 WSARecv,投递一个等待接收数据的I/O请求:


WSARecv(AcceptSocket, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags,  &(PerIoData->Overlapped), NULL);


注意参数一、参数二和参数六,实际上完成了每个 AcceptSocket 与 PerIoData 的捆绑


4.获取完成通知

在完成端口模型中,工作者线程 WorkerThread 需要调用 GetQueuedCompletionStatus 函数,尝试在完成端口上获取完成通知包。

GetQueuedCompletionStatus函数原型如下:


WINBASEAPI BOOL WINAPI

GetQueuedCompletionStatus(

    HANDLE CompletionPort,

    LPDWORD lpNumberOfBytesTransferred,

    LPDWORD lpCompletionKey,

    LPOVERLAPPED *lpOverlapped,

    DWORD dwMilliseconds );


When you perform an input/output operation with a file handle that has an associated input/output completion port, the I/O system sends a completion notification packet to the completion port when the I/O operation completes. The completion port places the completion packet in a first-in-first-out queue. The GetQueuedCompletionStatus function retrieves these queued completion packets. —MSDN


这个类比重叠I/O事件通知模型中的WSAWaitForMultipleEvents/WSAGetOverlappedResult获得I/O操作结果。


参数一为创建线程池时传递的完成端口句柄 hCompletionPort,参数二提供一个DWORD指针,用来接收当I/O完成时实际传输的字节数。

参数三即第二次调用 CreateIoCompletionPort 时传入的单句柄完成键,这里用于确定与 CompletionPort 绑定的具体哪个(套接字)句柄完成了I/O操作导致该函数返回。

参数四即第二次调用 CreateIoCompletionPort 时传入的(套接字)句柄(AcceptSocket)投递重叠I/O请求(WSARecv/WSASend)时指定的 (WSA)OVERLAPPED 结构。实际操作中往往提供一个(WSA)OVERLAPPED 扩展结构,这就是常说的I/O数据


由于调用 CreateIoCompletionPort 将套接字句柄与完成端口hCompletionPort关联起来了,所以针对 AcceptSocket 这个套接字句柄上的I/O请求(WSARecv)完成时,一个完成通知包将被投递到完成端口 hCompletionPort 消息队列中。GetQueuedCompletionStatus 函数是用来获取排队完成状态,它使调用线程挂起,直到收到一个完成通知包才返回。


If the function dequeues a completion packet for a successful I/O operation from the completion port, the return value is nonzero. The function stores information in the variables pointed to by the lpNumberOfBytesTransferred, lpCompletionKey, and lpOverlapped parameters.

If *lpOverlapped is NULL and the function does not dequeue a completion packet from the completion port, the return value is zero. The function does not store information in the variables pointed to by the lpNumberOfBytesTransferred and lpCompletionKey parameters. —MSDN


在工作者线程WorkerThread中调用GetQueuedCompletionStatus


    while(TRUE)

{

GetQueuedCompletionStatus(CompletionPort, &BytesTransferred,  (LPDWORD)&PerHandleData, (LPOVERLAPPED *) &PerIoData, INFINITE)

if (BytesTransferred == 0) // 出错

       {

           printf("Closing socket %d/n", PerHandleData->Socket);

          if (closesocket(PerHandleData->Socket) == SOCKET_ERROR)

           {

               printf("closesocket() failed with error %d/n", WSAGetLastError());

               return 0;

}

           GlobalFree(PerHandleData);

           GlobalFree(PerIoData);

continue;

 }

// 根据lpNumberOfBytesTransferred, lpCompletionKey, and lpOverlapped参数进行处理

// ……

}


GetQueuedCompletionStatus传递的参数三将PerIOData强制转换为(LPOVERLAPPED *) 结构,后面又要配合使用PerIOData的其他字段,这体现了扩展二字的用意。

若 GetQueuedCompletionStatus 返回 FALSE,可以调用 (WSA)GetOverlappedResult/(WSA)GetLastError 获知具体错误。


5.完成端口流程小结

如前面所言,完成端口模型利用了Win32重叠I/O机制,它是在利用完成端口队列对象来管理线程池。下面总结一下编写基于完成端口的Winsock服务器程序的要点。

1)首先,当然要调用CreateIoCompletionPort创建一个完成端口,一般一个应用程序只创建一个完成端口。

2)然后,创建一个线程池,把完成端口作为参数传给线程参数,以使工作线程调用GetQueuedCompletionStatus在完成端口上等待I/O完成,收到完成通知后提供I/O数据处理服务。

3)每当Accept(Ex)成功返回后,调用CreateIoCompletionPortAcceptSocket与完成端口关联起来,并传递AcceptSocket的上下文信息(即单句柄数据)给完成键参数。同时为AcceptSocket创建一个I/O缓冲区(即I/O数据,扩展OVERLAPPED结构)。

4)接着,AcceptSocket调用异步I/O操作函数,如WSARecvWSASend,抛出重叠的I/O请求。这时需要将单I/O数据的第一个字段OVERLAPPED结构传递给WSARecvWSASend,以表示它们投递的是重叠I/O请求,需要等待系统的I/O完成通知。

5)至此,当上一步抛出的重叠I/O操作完成时,完成端口上会有一个完成通知包,工作线程收到完成通知,从GetQueuedCompletionStatus返回。通过完成键即单句柄数据提供的客户套接字上下文信息、重叠结构参数以及实际I/O的字节数,就可以正式提供I/O数据服务了。

简言之,涉及两个重要的数据结构:单句柄数据I/O数据(扩展的OVERLAPPED结构);涉及两个重要的API CreateIoCompletionPortGetQueuedCompletionStatus;当然,不要忘记重叠请求的投递者WSARecvWSASend,它们是导火索通信程序的本质工作就是通信

因为完成端口模型本质上利用了Win32重叠I/O机制,故(扩展的)OVERLAPPED结构提供的沟通机制依然是数据通信重要的线索。另外,要理解完成端口内部机制和工作原理及其在通信中的作用。


6.完成端口应用

下面补充完成端口的项目应用实例。

Windows下的IIS采用了完成端口模型,参考完成端口与高性能服务器程序开发》、《I/O完成端口(Windows核心编程》、A simple application using I/O Completion Ports and WinSock

Apache Httpd/httpd/server/mpm/winnt/child.c中的winnt_accept()AcceptExwinnt_get_connection()使用了完成端口ThreadDispatchIOCP但并没有关联套接字,而是自己构造完成包(mpm_post_completion_contextPostQueuedCompletionStatus),完成键为枚举io_state_e单句柄为PCOMP_CONTEXT

// Apache Httpd/httpd/server/mpm/winnt/mpm_winnt.h

typedef enum {

    IOCP_CONNECTION_ACCEPTED = 1,

    IOCP_WAIT_FOR_RECEIVE = 2,

    IOCP_WAIT_FOR_TRANSMITFILE = 3,

    IOCP_SHUTDOWN = 4

} io_state_e;

Apache源码中只使用到IOCP_CONNECTION_ACCEPTEDIOCP_SHUTDOWN两种状态。除此之外,Apache里面没有真正的完成端口成分,ntmpm似乎依然是线程池加进程池来处理。具体I/O过程参考Apache源码Apache Httpd/httpd-2.2.15/srclib/apr/file_io/win32/readwrite.cApache Httpd/httpd/srclib/apr/network_io/win32/sendrecv.c

Nginx是由Igor Sysoev为俄罗斯访问量第二的Rambler.ru站点开发的,其特点是占有内存少,并发能力强,Nginx的并发能力确实在同类型的网页伺服器中表现较好。新浪、网易、腾讯、迅雷、CSDN等大型网站都采用了NginxNginx每一个客户端请求进来以后会通过事件处理机制,在LinuxEpoll,在FreeBSD下是KQueue放到空闲的连接里。相关源码参考nginx/src/event/modules下的ngx_epoll_module.cngx_kqueue_module.h(c)。参考基于IO完成端口与WSAEventSelectnginx事件处理模块:ngx_iocp_module》、《Windows下完成端口移植Linux下的epoll》。


参考:

Network Programming for Microsoft Windows  Anthony Jones,Jim Ohlund

Windows Internals  Mark E. Russinovich,David A. Solomon

Windows 2000 Systems Programming Black Book  Al Williams

Multithreading Applications in Win32  Jim Beveridge,Robert Wiener.

Windows网络与通信程序设计  王艳平

 

IOCP完成端口原理

Write Scalable Winsock Apps Using Completion Ports

Design Issues When Using IOCP in a Winsock Server


《简单说一个IOCP不好的地方》


http://chatgpt.dhexx.cn/article/BXs3eqU7.shtml

相关文章

Windows 完成端口编程

Windows 完成端口编程 本文为转载, 原文地址: http://xingzhesun.blogbus.com/logs/3925649.html Table of Contents 1 基本概念 2 OVERLAPPED数据结构 3 完成端口的内部机制 3.1 创建完成端口 3.2 完成端口线程的工作原理 3.3 线程间数据传递 3.4 线程的安全退出 1 基本…

完成端口IOCP详解

本系列里完成端口的代码在两年前就已经写好了&#xff0c;但是由于许久没有写东西了&#xff0c;不知该如何提笔&#xff0c;所以这篇文档总是在酝酿之中……酝酿了两年之后&#xff0c;终于决定开始动笔了&#xff0c;但愿还不算晚….. 这篇文档我非常详细并且图文并茂的介绍了…

Win socket编程--IOCP完成端口模型

引言 要想编写一个高性能的服务器应用程序&#xff0c;必须实现一个高效的线程模型。让太少或者太多的服务器线程来处理客户的请求&#xff0c;都可能导致性能问题。例如&#xff0c;如果一个服务器创建单个线程来处理所有的请求&#xff0c;那么客户端可能长期等待而得不到响…

深度探索I/O完成端口

引言 要想编写一个高性能的服务器应用程序&#xff0c;必须实现一个高效的线程模型。让太少或者太多的服务器线程来处理客户的请求&#xff0c;都可能导致性能问题。例如&#xff0c;如果一个服务器创建单个线程来处理所有的请求&#xff0c;那么客户端可能长期等待而得不到响…

Windows中I/O完成端口机制详解

Windows中I/O完成端口机制详解 引言 要想编写一个高性能的服务器应用程序&#xff0c;必须实现一个高效的线程模型。让太少或者太多的服务器线程来处理客户的请求&#xff0c;都可能导致性能问题。例如&#xff0c;如果一个服务器创建单个线程来处理所有的请求&#xff0c;那么…

c++使用完成端口实现服务器的高性能并发

如何使用c,借助完成端口完成大并发服务器的搭建,是今天要讨论的问题&#xff0c;套路如下&#xff1a; 套路总结一下&#xff1a; 创建完成端口 依据CPU核数创建一定数量的线程 线程中不断调用GetQueuedCompletionStatus检查完成端口状态&#xff0c;分别给予处理 创建一个…

C#高性能大容量SOCKET并发完成端口例子(有C#客户端)完整实例源码

遥望星空 好好干&#xff0c;有前途&#xff01; 博客园首页新随笔联系管理订阅 随笔- 1082 文章- 0 评论- 151 C#高性能大容量SOCKET并发(转) C#高性能大容量SOCKET并发&#xff08;零&#xff09;&#xff1a;代码结构说明 C#高性能大容量SOCKET并发&#xff08;一…

完成端口学习笔记(一):完成端口+控制台 实现文件拷贝

最近在整理手里一个项目的后台服务端归档程序&#xff0c;重新梳理了一下有关“完成端口”的知识&#xff0c;发现还是有很多模棱两可的地方&#xff0c;下面记录一下再次学习的点滴&#xff0c;该篇博文还会有后续的补充章节&#xff0c;不知道什么时间会再补充^_^。 IO概念 还…

Socket编程模型之完成端口模型

转载请注明来源&#xff1a;http://blog.csdn.net/caoshiying?viewmodecontents 一、回顾重叠IO模型 用完成例程来实现重叠I/O比用事件通知简单得多。在这个模型中&#xff0c;主线程只用不停的接受连接即可&#xff1b;辅助线程判断有没有新的客户端连接被建立&#xff0c;如…

完成端口(IOCP)详解[2/2](转载)

版权声明&#xff1a;本文为CSDN博主「PiggyXP」的原创文章&#xff0c;遵循CC 4.0 BY-SA版权协议&#xff0c;转载请附上原文出处链接及本声明。 原文链接&#xff1a;https://blog.csdn.net/piggyxp/article/details/6922277 五 使用完成端口的基本流程 说了这么多的废话&a…

Windows io完成端口

Windows 提供一种称为I/O完成端口(I/O Completion Port)机制&#xff0c;能够让I/O的完成处理交由一个专门的线程池来完成&#xff0c;而线程池的线程数量是一个可配置的参数。这种做法将I/O请求的发起动作与完成处理分离到了不同的线程中。 I/O完成端口是内核对象。个人的感觉…

完成端口(Completion Port)详解

http://blog.csdn.net/piggyxp/article/details/6922277 手把手叫你玩转网络编程系列之三 完成端口(Completion Port)详解 ----- By PiggyXP(小猪) 前 言 本系列里完成端口的代码在两年前就已经写好了&#xff0c;但是由于许久没有写东西了&#xff0c;不知该如何提笔&#xf…

完成端口(CompletionPort)详解 - 手把手教你玩转网络编程系列之三

手把手叫你玩转网络编程系列之三 完成端口(Completion Port)详解 ----- By PiggyXP(小猪) 前 言 本系列里完成端口的代码在两年前就已经写好了&#xff0c;但是由于许久没有写东西了&#xff0c;不知该如何提笔&#xff0c;所以这篇文档总是在酝酿之中……酝酿了两年之后&…

树同构-树哈希

树同构-树哈希 题目描述 题解 对于无根树&#xff0c;由于数据范围较小&#xff0c;可以直接以每个点为根dfs一次&#xff0c;维护其树哈希的值&#xff0c;然后用并查集维护 &#xff08;若数据范围大一些&#xff0c;可以以树的重心跑dfs&#xff09; 代码实现 #include&…

2.3 树的同构(树,c)

树的同构 树的同构输入格式:输出格式:输入样例1&#xff08;对应图1&#xff09;&#xff1a;输出样例1:输入样例2&#xff08;对应图2&#xff09;&#xff1a;输出样例2: 题意理解输入两棵二叉树的信息&#xff0c;判断是否同构&#xff08;对应图1&#xff09; 求解思路二叉…

03-树1 树的同构

题目 给定两棵树T1和T2。如果T1可以通过若干次左右孩子互换就变成T2&#xff0c;则我们称两棵树是“同构”的。例如图1给出的两棵树就是同构的&#xff0c;因为我们把其中一棵树的结点A、B、G的左右孩子互换后&#xff0c;就得到另外一棵树。而图2就不是同构的。 图1 图2 现给…

『树同构的判定(树Hash)』CF718D:Andrew and Chemistry

题目描述 题解 这道题目的难点在于如何判断树的同构&#xff0c;这就是所谓的树的哈希。 我们假设需要求解以x为根的子树的hash值&#xff0c;我们可以将子树的hash值存储到vector内&#xff0c;排序以后用map来判断重复。这个写法十分简单。具体如下&#xff1a; int dfs(i…

数据结构之树的同构

目录 前言 题意理解 求解思路 二叉树表示 程序框架搭建 读数据建二叉树 二叉树同构判别 前言 本篇主要讲有关二叉树的同构判断。 题意理解 给定两棵树T1和T2。如果T1可以通过若干次左右孩子互换就变成T2&#xff0c;则称这两棵树是“同构”的。 例如&#xff1a; 左图…

2020牛客多校10:Identical Trees(树hash + 树同构 + 费用流模板)

题意&#xff1a;给出两棵同构的有根树&#xff0c;同构修改点的标号使得两棵树完全一样&#xff0c;至少需要修改多少次。 分析&#xff1a;肯定是将子树和另外一棵的某个子树对应&#xff0c;而两棵子树的问题是一个子问题&#xff0c;显然只有同构的子树才可以对应&#xf…

哈希算法在判定树同构方面的应用(下)

哈希算法在判定树同构方面的应用 在上一篇文章中我们介绍了 枚举根节点哈希 和 求重心哈希 两种方法来判断两棵无根树是否同构。 但是如果有些题目中我必须要计算出每个根节点的 f f f 值&#xff0c;且 n ≤ 1 e 5 n\le 1e5 n≤1e5&#xff0c;我们要怎么办呢&#xff1f;…