文章目录
当应用程序必须一次管理多个套接字时,完成端口模型提供了最好的系统性能。这个模型也提供了最好的伸缩性,它非常适合用来处理上百上千个套接字。
IOCP模型是事先开好了N个线程,存储在线程池中,让他们hold。然后将所有用户的请求都投递到一个完成端口上,然后N个工作线程逐一地从完成端口中取得用户消息并加以处理。这样就避免了为每个用户开一个线程。既减少了线程资源,又提高了线程的利用率。
一 什么是完成端口(completion port)对象
I/O完成端口是应用程序使用线程池处理异步I/O请求的一种机制。处理多个并发异步I/O请求时,使用I/O完成端口比在I/O请求时创建线程更快更有效。
二 使用IOCP的方法
创建完成端口对象
使用完成端口模型,首先要调用CreateIoCompletionPort函数创建一个完成端口对象,Winsock将使用这个对象为任意数量的套接字句柄管理I/O请求。函数定义如下:
HANDLE CreateIoCompletionPort(
HANDLE hFile, //要关联的套接字句柄
HANDLE hExistingCompletionPort, //创建的完成端口对象句柄
ULONG_PTR CompletionKey, //制定一个句柄唯一数据,它将与套接字句柄关联在一起。应用程序可以在此存储任意类型的信息,通常是一个指针
DWORD dwNumberOfConcurrentThreads //允许在完成端口上同时执行的线程的数量
);
此函数有两个不同的功能:
1 创建一个完成端口对象;
2 将一个或多个文件句柄(套接字句柄)关联到I/O完成端口对象
I/O服务线程和完成端口
成功创建完成端口对象之后,就可以向这个完成端口对象关联套接字句柄了。在关联套接字之前,需要先创建一个或多个工作线程(成为I/O服务线程),在完成端口上执行并处理投递到完成端口上的I/O请求。
有了足够的工作线程来处理完成端口上的I/O请求后,就该为完成端口关联套接字句柄,这使用到了CreateIoCompletionPort的前三个参数。CompletionKey参数通常用来描述与套接字相关的信息,所以称它为句柄唯一(per=handle)数据。
完成端口和重叠I/O
在完成端口关联套接字句柄之后,便可以通过在套接字上投递重叠发送和接收请求处理I/O了。这些I/O操作完成时,I/O系统会向完成端口对象发送一个完成通知封包。
I/O完成端口以先进先出的方式为这些封包排队。应用程序使用GetQueuedCompletionSatus函数可以取得这些队列中的封包。这个函数应该在处理完成端口对象I/O的服务线程中调用。
GetQueuedCompletionStatus(
HANDLE CompletionPort, //完成端口对象句柄
LPDWORD lpNumberOfBytes //取得I/O操作期间传输的字节数
PULONG_PTR lpCompletionKey, //取得在关联套接字时指定的句柄唯一数据(指针)
LPOVERLAPPED* lpOverlapped, //取得投递I/O操作时指定的OVERLAPPED 结构
DWORD dwMilliseconds //如果完成端口没有完成封包,此参数指定了等待的事件,INFINIE为无穷大
)
I/O服务线程调用GetQueuedCompletionStatus函数取得有事件发生的套接字的信息,通过lpNumberOfBytes参数得到传输的字节数量,通过lpCompletionKey参数得到与套接字关联的句柄唯一(per-handle)数据,通过lpOverlapped参数得到投递I/O请求时使用的重叠对象地址,进一步得到I/O唯一(per-I/O)数据。
lpCompletionKey参数包含了我们称为per-Handle的数据,因为当套接字第一次与完成端口关联时,这个数据就关联到一个套接字句柄。这是传递给CreateIoCompletionPort函数的CompletionKey参数。
lpOverlapped参数指向一个OVERLAPPED结构,结构后面便是我们称为per-I/O的数据,这可以是工作线程处理完成封包时想要知道的任何信息。
三 恰当地关闭IOCP
略
四 IOCP大概的处理流程
1 创建一个完成端口
2 创建一个线程A
3 A线程循环调用GetQueuedCompletionStatus()函数来得到IO操作结果
4 主线程循环里调用accept等待客户端连接
5 主线程里accept返回新连接后,把这个新的套接字句柄用CreateIoCompletionPort()关联到完成端口,然后发出一个异步WSASend或者WSARecv调用,因为是异步函数,WSASend/WSARecv会马上返回,实际的发送或者接收操作由Windows系统去做。
6 主线程继续下一次循环,阻塞在accept这里等待新的客户连接
7 Windows系统完成WSASend或者WSARecv的操作,把结果发到完成端口。
8 A线程里的GetQueuedCompletionStatus()马上返回,并从完成端口取得刚完成的WSASend/WSARecv的结果。
9 在A线程里对这些数据进行处理(如果处理过程很耗时,需要新开线程处理),然后紧接着发出WSASend/WSARecv,并继续下一次循环阻塞在GetQueuedCompletionStatus()这里。
流程图:
上述流程中有两种类型的线程 —主线程和它创建的线程,主线程创建监听套接字,创建额外的工作线程,关联IOCP,负责等待和接受到来的连接等;由主线程创建的线程负责处理I/O事件,这些线程调用GetQueuedCompletionStatus函数在完成端口对象上等待完成的I/O操作。
五 一个简单示例具体编程流程
1 创建一个完成端口对象,创建一个或多个服务线程,服务线程调用GetQueuedCompletionStatus取得完成I/O通知信息。主线程接收连接,将新套接字关联到完成端口上,然后在新连接上投递Read I/O异步请求。
2 服务线程从GetQueuedCompletionStatus函数返回后,根据per-handle I/O数据中的信息,做出相应的处理,并继续投递下一个Read I/O异步请求。
#define _WIN32_WINNT 0x0400
#include<windows.h>
#include<cstdio>
#include"InitSocket.h"
#define BUFFER_SIZE 2048
CInitSock initSock ; //进入main函数前已经进行了初始化
typedef struct _PER_HANDLE_DATA //per-handle数据
{
SOCKET s ; //对应的套接字句柄
sockaddr_in addr ; //客户方地址
} PER_HANDLE_DATA ,*PPER_HANDLE_DATA ;
/********************************************************/
//包含版本
typedef struct _PER_IO_DATA //per-I/O数据
{
OVERLAPPED ol ; //重叠结构,必须放作第一个结构,用C++派生的方法更好
char buf[BUFFER_SIZE] ; //数据缓冲区
int nOperationType ; //操作类型
#define OP_READ 1 //操作类型码
#define OP_WRITE 2
#define OP_ACCEPT 3
} PER_IO_DATA,*PPER_IO_DATA ;
/**********************************************************/
/***********************************************************
* 派生版本的
class _PER_IO_DATA : public OVERLAPPED
{
public :
char buf[BUFFER_SIZE] ;
int nOperationType ;
#define OP_READ 1 //操作类型码
#define OP_WRITE 2
#define OP_ACCEPT 3
} ;
typedef _PER_IO_DATA PER_IO_DATA ;
typedef PER_IO_DATA *PPER_IO_DATA;
***************************************************/
DWORD WINAPI ServerThread(LPVOID lpParam) ;
int main(void)
{
int nPort = 4567 ;
//创建完成端口对象,创建工作线程处理完成端口对象中的事件
HANDLE hCompletion = CreateIoCompletionPort(INVALID_HANDLE_VALUE,0,0,0) ;
CreateThread(NULL,0,ServerThread,(LPVOID)hCompletion,0,0) ;
//创建监听套接字,绑定到本地地址,开始监听
SOCKET sListen = socket(AF_INET,SOCK_STREAM,0) ;
SOCKADDR_IN si ;
si.sin_family = AF_INET ;
si.sin_port = ntohs(nPort) ;
si.sin_addr.s_addr = INADDR_ANY ;
bind(sListen,(sockaddr*)&si,sizeof(si)) ;
listen(sListen,5) ;
//循环处理到来的连接
while(TRUE)
{
//等待接受未决的连接请求
SOCKADDR_IN saRemote ;
int nRemoteLen = sizeof(saRemote) ;
SOCKET sNew = accept(sListen,(sockaddr*)&saRemote,&nRemoteLen) ;
//接受到新连接之后,为创建一个per-handle数据,并将它们关联到完成端口对象
PPER_HANDLE_DATA pPerHandle = (PPER_HANDLE_DATA)GlobalAlloc(GPTR,sizeof(PER_HANDLE_DATA)) ;
pPerHandle->s = sNew ;
memcpy(&pPerHandle->addr,&saRemote,nRemoteLen) ;
CreateIoCompletionPort((HANDLE)pPerHandle->s,hCompletion,(DWORD)pPerHandle,0) ; //关联,不是创建.涉及到转型,将指针转为DWORD,与pPerHandle->s关联的唯一数据就是PerHandle
//投递一个接收请求
PPER_IO_DATA pPerIO = (PPER_IO_DATA)GlobalAlloc(GPTR,sizeof(PER_IO_DATA)) ;
pPerIO->nOperationType = OP_READ ;
WSABUF buf ;
buf.buf = pPerIO->buf ;
buf.len = BUFFER_SIZE ;
DWORD dwRecv ;
DWORD dwFlags = 0 ;
//作为引子的接收请求,引发接收操作
//与该套接字相关的OVERLAPPED结构
WSARecv(pPerHandle->s,&buf,1,&dwRecv,&dwFlags,&pPerIO->ol,NULL) ;
}
return 0 ;
}
//服务线程
DWORD WINAPI ServerThread(LPVOID lpParam)
{
//得到完成端口对象句柄`
HANDLE hCompletion = (HANDLE)lpParam ;
DWORD dwTrans ;
PPER_HANDLE_DATA pPerHandle ;
PPER_IO_DATA pPerIO ;
while(TRUE)
{
//在关联到此完成端口的所有套接字上等待I/O完成,倒数第二个参数的作法其实是非常不适当的
BOOL bOK = GetQueuedCompletionStatus(hCompletion,&dwTrans,(LPDWORD)&pPerHandle,(LPOVERLAPPED*)&pPerIO,WSA_INFINITE) ;
if(!bOK) //在此套接字上有错误发生
{
closesocket(pPerHandle->s) ;
GlobalFree(pPerHandle) ;
GlobalFree(pPerIO) ;
continue ;
}
//套接字被对方关闭
if(dwTrans == 0 && (pPerIO->nOperationType == OP_READ || pPerIO->nOperationType == OP_WRITE))
{
closesocket(pPerHandle->s) ;
GlobalFree(pPerHandle) ;
GlobalFree(pPerIO) ;
continue ;
}
switch(pPerIO->nOperationType) //通过per-I/O数据中的nOperationType域查看什么I/O请求完成了
{
case OP_READ : //完成一个接收请求
{
pPerIO->buf[dwTrans] = '\0' ;
printf(pPerIO->buf) ;
//继续投递接收I/O请求
WSABUF buf ;
buf.buf = pPerIO->buf ;
buf.len = BUFFER_SIZE ;
pPerIO->nOperationType = OP_READ ;
DWORD nFlags = 0 ;
WSARecv(pPerHandle->s,&buf,1,&dwTrans,&nFlags,&pPerIO->ol,NULL) ; //继续引发在该套接字上面的接收操作
//投递一个发送请求I/O,我自己添加,为了测试OR_WRITE之用
PPER_IO_DATA pSendPerIO = (PPER_IO_DATA)GlobalAlloc(GPTR,sizeof(PER_IO_DATA)) ;
pSendPerIO->nOperationType = OP_WRITE ;
memcpy(pSendPerIO->buf,pPerIO->buf,dwTrans) ;
WSABUF dbuf ;
dbuf.buf = pSendPerIO->buf ;
dbuf.len = dwTrans ; //如果大于客户端的接收缓冲区,则客户端需要重复多次recv才能接收完所有数据
WSASend(pPerHandle->s,&dbuf,1,&dwTrans,nFlags,&pSendPerIO->ol,NULL); //引发在该套接字上面的发送操作
}
break ;
case OP_WRITE :
{
printf("发送数据完成\n");
}
break ;
case OP_ACCEPT : //这个多余的,因为主线程因为在accpet了
break ;
}
}
return 0 ;
}