技术经验分享:IOCP详解

简介: 技术经验分享:IOCP详解

简介: IOCP(I/O Completion Port,I/O完成端口)是性能最好的一种I/O模型。它是应用程序使用线程池处理异步I/O请求的一种机制。


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时间片,直到有事情做完为止。


5)IOCP是如何工作的?


当使用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


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


{


// 初始化WINSOCK


WSADATA wsd;


if( WSAStartup(MAKEWORD(2, 2), &wsd) != 0)


{


OutErr("WSAStartup()");


}


}


///////////////////////////////////////////////////////////////////////


//


// 函数名 : BindServerOverlapped


// 功能描述 : 绑定端口,并返回一个 Overlapped 的ListenSocket


// 参数 : int nPort


// 返回值 : SOCKET


//


///////////////////////////////////////////////////////////////////////


SOCKET BindServerOverlapped(int nPort)


{


// 创建socket


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


}


// 设置监听队列为200


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


}


}


//创建侦听SOCKET


SOCKET 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


#include


#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 //代码效果参考:http://hnjlyzjd.com/xl/wz_24487.html

recvBuf【100】;

while(nCnt [span style="color: rgba(0, 0, 0, 1)"> 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,用于一台机器上测试的IP


addrSrv.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);//关闭套接字,释放为这个套接字分配的资源


//代码效果参考:http://hnjlyzjd.com/hw/wz_24485.html

Sleep(1);

}


WSACleanup();//终止对这个套接字库的使用

相关文章
|
5月前
|
网络协议 算法 Linux
【嵌入式软件工程师面经】Linux网络编程Socket
【嵌入式软件工程师面经】Linux网络编程Socket
160 1
|
3月前
|
Rust 网络协议 安全
揭开Rust网络编程的神秘面纱:全新的Socket体验,让你告别内存泄漏的噩梦!
【8月更文挑战第31天】Rust语言凭借其卓越的内存安全性和高性能,在网络编程领域展现出独特优势。本文将带你探索Rust中的Socket编程,展示如何使用标准库`std::net`模块轻松实现TCP服务器与客户端。通过简洁的代码示例,你将看到Rust如何简化网络通信流程,并通过`async/await`异步模型高效处理并发连接。此外,Rust社区提供的优秀库如`tokio`和`async-std`进一步增强了异步网络编程的能力。无论是从基础示例还是高级应用,Rust都将为你带来耳目一新的网络编程体验。
265 0
|
5月前
|
关系型数据库 MySQL 数据库
经验大分享:Socket网络编程
经验大分享:Socket网络编程
|
4月前
|
Java
Socket网络编程实战指南
Socket网络编程实战指南
|
5月前
|
编解码 网络协议 Java
技术笔记:Mina框架(实战详解)
技术笔记:Mina框架(实战详解)
|
6月前
Netty Review - 借助SimpleTalkRoom初体验异步网络编程的魅力
Netty Review - 借助SimpleTalkRoom初体验异步网络编程的魅力
79 0
|
Linux 数据库 数据安全/隐私保护
C++实战-Linux多线程(入门到精通)(三)
C++实战-Linux多线程(入门到精通)(三)
52 0
|
Linux 调度 C语言
C++实战-Linux多线程(入门到精通)(二)
C++实战-Linux多线程(入门到精通)(二)
40 0
|
NoSQL Linux 调度
C++实战-Linux多线程(入门到精通)(一)
C++实战-Linux多线程(入门到精通)(一)
162 0
|
Linux
【Linux网络编程】服务端编程初体验
【Linux网络编程】服务端编程初体验
103 0