IOCP详解

article/2025/11/8 10:35:41

IOCP详解

IOCP(I/O Completion Port,I/O完成端口)是性能最好的一种I/O模型。它是应用程序使用线程池处理异步I/O请求的一种机制。在处理多个并发的异步I/O请求时,以往的模型都是在接收请求是创建一个线程来应答请求。这样就有很多的线程并行地运行在系统中。而这些线程都是可运行的,Windows内核花费大量的时间在进行线程的上下文切换,并没有多少时间花在线程运行上。再加上创建新线程的开销比较大,所以造成了效率的低下。

Windows Sockets应用程序在调用WSARecv()函数后立即返回,线程继续运行。当系统接收数据完成后,向完成端口发送通知包(这个过程对应用程序不可见)。

应用程序在发起接收数据操作后,在完成端口上等待操作结果。当接收到I/O操作完成的通知后,应用程序对数据进行处理。

        

完成端口其实就是上面两项的联合使用基础上进行了一定的改进

一个完成端口其实就是一个通知队列,由操作系统把已经完成的重叠I/O请求的通知放入其中。当某项I/O操作一旦完成,某个可以对该操作结果进行处理的工作者线程就会收到一则通知。而套接字在被创建后,可以在任何时候与某个完成端口进行关联。

众所皆知,完成端口是在WINDOWS平台下效率最高,扩展性最好的IO模型,特别针对于WINSOCK的海量连接时,更能显示出其威力。其实建立一个完成端口的服务器也很简单,只要注意几个函数,了解一下关键的步骤也就行了。

分为以下几步来说明完成端口:

0)       同步IO与异步IO

1)       函数

2)       常见问题以及解答

3)       步骤

4)       例程

 

0、同步IO与异步IO

同步I/O首先我们来看下同步I/O操作,同步I/O操作就是对于同一个I/O对象句柄在同一时刻只允许一个I/O操作,原理图如下:

        

由图可知,内核开始处理I/O操作到结束的时间段是T2~T3,这个时间段中用户线程一直处于等待状态,如果这个时间段比较短,则不会有什么问题,但是如果时间比较长,那么这段时间线程会一直处于挂起状态,这就会很严重影响效率,所以我们可以考虑在这段时间做些事情。

异步I/O操作则很好的解决了这个问题,它可以使得内核开始处理I/O操作到结束的这段时间,让用户线程可以去做其他事情,从而提高了使用效率

       

由图可知,内核开始I/O操作到I/O结束这段时间,用户层可以做其他的操作,然后,当内核I/O结束的时候,可以让I/O对象或者时间对象通知用户层,而用户线程GetOverlappedResult来查看内核I/O的完成情况

1、函数

我们在完成端口模型下会使用到的最重要的两个函数是:

CreateIoCompletionPort、GetQueuedCompletionStatus

CreateIoCompletionPort  的作用是创建一个完成端口和把一个IO句柄和完成端口关联起来:

// 创建完成端口

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

 

// 把一个IO句柄和完成端口关联起来,这里的句柄是一个socket 句柄

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

 

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

第二个就是我们上面创建出来的完成端口,这里就把两个东西关联在一起了。

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

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

 

GetQueuedCompletionStatus的作用就是取得完成端口的结果:

// 从完成端口中取得结果

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

第一个参数是完成端口

第二个参数是表明这次的操作传递了多少个字节的数据

第三个参数是OUT类型的参数,就是前面CreateIoCompletionPort传进去的单句柄数据,这里就是前面的SOCKET句柄以及与之相对应的数据,这里操作系统给我们返回,让我们不用自己去做列表查询等操作了。

第四个参数就是进行IO操作的结果,是我们在投递WSARecv / WSASend 等操作时传递进去的,这里操作系统做好准备后,给我们返回了。非常省事!!

个人感觉完成端口就是操作系统为我们包装了很多重叠IO的不爽的地方,让我们可以更方便的去使用,下篇我将会尝试去讲述完成端口的原理。

2、常见问题和解答

1)什么是单句柄数据(PerHandle)和单IO数据(PerIO)

单句柄数据就是和句柄对应的数据,像socket句柄,文件句柄这种东西。

单IO数据,就是对应于每次的IO操作的数据。例如每次的WSARecv/WSASend等等

其实我觉得PER是每次的意思,翻译成每个句柄数据和每次IO数据还比较清晰一点。

在完成端口中,单句柄数据直接通过GetQueuedCompletionStatus 返回,省去了我们自己做容器去管理。单IO数据也容许我们自己扩展OVERLAPPED结构,所以,在这里所有与应用逻辑有关的东西都可以在此扩展。

 

2)如何判断客户端的断开

我们要处理几种情况

a)如果客户端调用了closesocket,我们就可以这样判断他的断开:

if(0== GetQueuedCompletionStatus(CompletionPort, &BytesTransferred, 。。。)

{

}

if(BytesTransferred == 0)

{

    // 客户端断开,释放资源

}

b)如果是客户端直接退出,那就会出现64错误,指定的网络名不可再用。这种情况我们也要处理的:

if(0== GetQueuedCompletionStatus(。。。))

{

   if( (GetLastError() == WAIT_TIMEOUT) ||(GetLastError() == ERROR_NETNAME_DELETED) )

   {

        // 客户端断开,释放资源

   }

}

3)什么是IOCP?

我们已经提到IOCP 只不过是一个专门实现用来进行线程间的通信的技术,和信号量(semaphore)相似,因此IOCP并不是一个复杂的概念。一个IOCP 对象是与多个I/O对象关联的,这些对象支持挂起异步IO调用。直到一个挂起的异步IO调用结束为止,一个访问IOCP的线程都有可能被挂起。

完成端口的目标是使CPU保持在满负荷状态下工作。

4)为什么使用IOCP?

使用IOCP,我们可以克服”一个客户端一个线程”的问题。我们知道,这样做的话,如果软件不是运行在一个多核及其上性能就会急剧下降。线程是系统资源,他们既不是无限制的、也不是代价低廉的。

IOCP提供了一种只使用一些(I/O worker)线程去“相对公平地”完成多客户端的”输入输出”。线程会一直被挂起,而不会使用CPU时间片,直到有事情做完为止。

5IOCP是如何工作的?

当使用IOCP时,你必须处理三件事情:a)将一个Socket关联到完成端口;b)创建一个异步I/O调用; c)与线程进行同步。为了获得异步IO调用的结果,比如哪个客户端执行了调用,你必须传入两个参数:pCompletionKey参数和OVERLAPPED结构。

3、步骤

编写完成端口服务程序,无非就是以下几个步骤:

  1、创建一个完成端口

  2、根据CPU个数创建工作者线程,把完成端口传进去线程里

  3、创建侦听SOCKET,把SOCKET和完成端口关联起来

  4、创建PerIOData,向连接进来的SOCKET投递WSARecv操作

  5、线程里所做的事情:

 a、GetQueuedCompletionStatus,在退出的时候就可以使用PostQueudCompletionStatus使线程退出;

 b、取得数据并处理;

4、例程

下面是服务端的例程,可以使用sunxin视频中中的客户端程序来测试服务端。稍微研究一下,也就会对完成端口模型有个大概的了解了。

实例结果服务器、客户端如下:



/*

   完成端口服务器

   接收到客户端的信息,直接显示出来

*/

 

#include"winerror.h"
#include"Winsock2.h"
#pragmacomment(lib, "ws2_32")
#include"windows.h"
#include<iostream>
usingnamespace std;/// 宏定义
#define PORT 5050
#define DATA_BUFSIZE 8192#define OutErr(a) cout << (a) << endl \<< "出错代码:"<< WSAGetLastError() << endl \<< "出错文件:"<< __FILE__ << endl  \<< "出错行数:"<< __LINE__ << endl \#define OutMsg(a) cout << (a) << endl;/// 全局函数定义///
//
// 函数名       : InitWinsock
// 功能描述     : 初始化WINSOCK
// 返回值       : void
//
///
void InitWinsock()
{// 初始化WINSOCKWSADATA wsd;if( WSAStartup(MAKEWORD(2, 2), &wsd) != 0){OutErr("WSAStartup()");}
}///
//
// 函数名       : BindServerOverlapped
// 功能描述     : 绑定端口,并返回一个 Overlapped 的ListenSocket
// 参数         : int nPort
// 返回值       : SOCKET
//
///
SOCKET BindServerOverlapped(int nPort)
{// 创建socketSOCKET 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(nPort);servAddr.sin_addr.s_addr = htonl(INADDR_ANY);if(bind(sServer, (struct sockaddr*)&servAddr, sizeof(servAddr)) < 0){OutErr("bind Failed!");return NULL;}// 设置监听队列为200if(listen(sServer, 200) != 0){OutErr("listen Failed!");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 << "closingsocket" << 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<< "发送过来的消息:" << PerIoData->Buffer<< endl;// 继续向 socket 投递WSARecv操作DWORD Flags = 0;DWORD dwRecv = 0;ZeroMemory(PerIoData,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;
}void 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; i++){HANDLE hProcessIO = CreateThread(NULL, 0,ProcessIO, CompletionPort, 0, NULL);if(hProcessIO){CloseHandle(hProcessIO);}}//创建侦听SOCKETSOCKET 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();ZeroMemory(PerIoData, 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);
}

 

/*--------------------------------------------

**---------客户端例程序-----------------------

---------------------------------------------*/

#include<stdio.h>
#include<Winsock2.h>
#define MAXCNT 30000
void main()
{WORD wVersionRequested;WSADATA wsaData;int err;wVersionRequested = MAKEWORD( 2, 2);err = WSAStartup( wVersionRequested,&wsaData );//WSAStartup()加载套接字库if ( err != 0 ) {return;}if ( LOBYTE( wsaData.wVersion ) != 2 ||HIBYTE( wsaData.wVersion ) != 2 ){WSACleanup( );return;}static int nCnt = 0;char sendBuf[2000];
//     char recvBuf[100];while(nCnt < MAXCNT){SOCKETsockClient=socket(AF_INET,SOCK_STREAM,0);SOCKADDR_IN addrSrv;addrSrv.sin_addr.S_un.S_addr=inet_addr("127.0.0.1");//本地回路地址127,用于一台机器上测试的IPaddrSrv.sin_family=AF_INET;addrSrv.sin_port=htons(5050);//和服务器端的端口号保持一致connect(sockClient,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR));//连接服务器端(套接字,地址转换,长度)sprintf(sendBuf,"This is TestNo : %d\n",++nCnt);send(sockClient,sendBuf,strlen(sendBuf)+1,0);//向服务器端发送数据,"+1"是为了给'\0'留空间printf("send:%s",sendBuf);//           memset(recvBuf,0,100);
//           recv(sockClient,recvBuf,100,0);//接收数据
//           printf("%s\n",recvBuf);//打印closesocket(sockClient);//关闭套接字,释放为这个套接字分配的资源Sleep(1);}WSACleanup();//终止对这个套接字库的使用
}



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

相关文章

IOCP技术详解

这几周我接触了Windows网络通讯中的IOCP模型,自己在网上找了相关的知识进行学习&#xff0c;自己又下了好多服务器端的代码&#xff0c;但都运行不了&#xff0c;也是自己菜&#xff0c;能力还需加强。幸好我师父资助了我一个能运行的服务端IOCP代码&#xff0c;自己参照网上的…

Python正则表达式模式

在 Python 程序中&#xff0c;模式字符串使用如下特殊的语法来表示一个正则表达式&#xff1a; 字母和数字表示它们自身&#xff0c;一个正则表达式模式中的字母和数字匹配同样的字符串&#xff1b;当大多数字母和数字前加一个反斜杠时&#xff0c;它们会拥有不同的含义&#…

Python正则表达式实例详解

一、正则表达式语法 正则表达式是用匹配或者描述字符串的工具。 用处&#xff1a; a.判断字符串是否满足某个条件—判断输入的字符串是否是邮箱/手机号码。是否是ip地址 b.提取满足条件的字符串 c.字符串替换 Python中通过re模块中相应的方法来支持正则表达式的匹配、查找和替…

Python正则表达式语法快速入门

文章目录 1 正则符号初阶代码举例1&#xff1a;不同符号的组合代码举例2&#xff1a;符号加&#xff0c;代表连续的一个或多个代码举例3&#xff1a;匹配字符串开始代码举例4&#xff1a;匹配字符串结束代码举例5&#xff1a;匹配单词边界 2 正则符号进阶代码举例1&#xff1a;…

python正则表达式入门

&#x1f64a;今天我们来学习python的正则表达式的部分&#xff0c;先说下为什么要学习这一部分呢&#xff0c;当然是因为正则表达式处理文本类型的数据实在是太方便了。为以后进入nlp领域打打基础&#xff01; 先给大家推荐一个网站: 用于正则表达式验证. 大致就长这个样子。…

python使用正则表达式

一、使用正则表达式步骤 1、寻找规律&#xff1b; 2、使用正则符号表示规律&#xff1b; 3、提取信息&#xff0c;如果每一个字符都能匹配&#xff0c;则匹配成功&#xff1b;一旦有匹配不成功的字符则匹配失败。 二、正则表达式中常见的基本符号 1&#xff0e;点号“.”一…

Python 正则表达式

1.正则表达式的定义: 正则表达式是对字符串进行解析&#xff08;获取一大串字符串信息中&#xff0c;你想要的部分&#xff09;。 正则表达式是一种文本模式&#xff0c;模式描述在搜索文本时要匹配的一个或多个字符串。 正则表达式&#xff0c;又成正规表示式&#xff0c;正规…

python正则表达式详解

正则表达式是一个很强大的字符串处理工具&#xff0c;几乎任何关于字符串的操作都可以使用正则表达式来完成&#xff0c;作为一个爬虫工作者&#xff0c;每天和字符串打交道&#xff0c;正则表达式更是不可或缺的技能&#xff0c;正则表达式的在不同的语言中使用方式可能不一样…

详解Python正则表达式(含丰富案例)

前言&#xff1a;正则表达式在网络爬虫、数据分析中有着广泛使用&#xff0c;掌握正则表达式能够达到事半功倍的效果。本文详细介绍正则表达式中各种规则及其符号含义&#xff0c;并结合Python中的Re库进行演示&#xff0c;由浅入深&#xff0c;即学即练即用&#xff0c;内容丰…

Python超详细的正则表达式

目录 介绍 常用的正则表达式匹配规则表格 1.match() .group() 常用知识点 1.通用匹配 2.贪婪与非贪婪 3.修饰符 4.转义匹配 2.search() 3.findall() 4.sub() 总结 Hello大家好我是&#xff0c;PYmili&#xff01;今天给大家带来正则表达式的使用教程即匹配规则表格…

Python正则表达式(一看就懂)

目录 哈喽O(∩_∩)O&#x1f604; 什么是正则表达式(⊙_⊙) 简单说&#xff0c;正则表达式是… 正则表达式怎么用❓ sreach的用法&#x1f34a; 匹配连续的多个数值&#x1f349; 字符""重复前面一个匹配字符一次或者多次&#x1f349; 字符"*"重复前…

Python正则表达式详解 (超详细,看完必会!)

正则表达式详解 正则表达式 英文名称叫 Regular Expression简称RegEx&#xff0c;是用来匹配字符的一种工具&#xff0c;它常被用在网页爬虫&#xff0c;文稿整理&#xff0c;数据筛选等方面&#xff0c;最常用的就是用在网页爬虫&#xff0c;数据抓取。 一、正则表达式的各种…

Python 正则表达式详解(建议收藏!)

目录 match 匹配字符串 单字符匹配 . 匹配任意一个字符 \d 匹配数字 \D 匹配非数字 \s 匹配特殊字符&#xff0c;如空白&#xff0c;空格&#xff0c;tab等 \S 匹配非空白 \w 匹配单词、字符&#xff0c;如大小写字母&#xff0c;数字&#xff0c;_ 下划线 \W 匹配非…

Android工程中方法数超过65536解决方法(Kotlin)

Android Studio报错&#xff1a; The number of method references in a .dex file cannot exceed 64K. Caused by: com.android.tools.r8.utils.AbortException: Error: Cannot fit requested classes in a single dex file (# methods: 68815 > 65536) 解决方案 build.…

postman/EOLINKER测试报错 RangeError:Port should be 0 and 65536. Received 80892.

postman/EOLINKER测试报错 RangeError&#xff1a;Port should be > 0 and < 65536. Received 80892. RangeError&#xff1a;范围错误 RangeError是当一个只超出有效范围时发生的错误。主要的有几种情况&#xff0c;第一是数组长度为负数&#xff0c;第二是Number对象…

Android Studio Cannot fit requested classes in a single dex file (# methods: 72633 > 65536)解决办法

今天在Android Studio中构建Android工程时&#xff0c;出现了这样报错&#xff1a;“Cannot fit requested classes in a single dex file (# methods: 72633 &#xff1e; 65536)” 在网上找了一圈儿&#xff0c;总结个简单快速的解决方法 问题&#xff1a; Cannot fit req…

计算机端口号65536,65536端口能不能用

楼主asdd3000(asdd) 16位计算机端口65536个,32位计算机端口是65536/2个还是65536*2个?为什么? 问题点数:10、回复次数:20 Top 1 楼EvilOctal(冰血封情) 回复于 2004-04-08 20:41:04 得分 0 这个问题我真的不太清楚. 但是 好象和位数没关系吧? 服务端口是国际组织规定的,不是…

为什么89C51单片机里面有TH0=(65536-50000)/256;TL0=(65536-50000)%256;

由于89C51的晶振频率为12MHZ&#xff0c;机器周期是1us,则每1us产生一次计数&#xff0c;例如需要进行50ms的计时&#xff0c;对机器周期进行50000计数就可以得到&#xff0c;T0可以对机器周期进行65536次计数&#xff0c;为了不让T0溢出&#xff0c;我们需要对定时器/计数器赋…

NXP MCUXPresso - cc1plus.exe: out of memory allocating 65536 bytes

文章目录 NXP MCUXPresso - cc1plus.exe: out of memory allocating 65536 bytes概述实验结论补充END NXP MCUXPresso - cc1plus.exe: out of memory allocating 65536 bytes 概述 在尝试迁移 openpnp - Smoothieware project 从gcc命令行 MRI调试方式 到NXP MCUXpresso工程…

65536 65535 65534

65536 65535 65534 通常在导入模型的时候会遇到65534的限制&#xff0c;比如Unity里的“Meshes may not have more than 65534 vertices or triangles at the moment”提示。开起来像是2的16次方&#xff0c;但为什么不是65536&#xff1f;65536和65535会有什么问题&#xff1…