completionport模型
- 假若一个应用程序同时需要管理为数众多的套接字,那么采用这种模型,可以达到最佳的系统性能。
- 从本质上说,完成端口模型要求创建一个Windows完成端口对象,该对象通过指定数量的线程,对重叠I/O请求进行管理,以便为已经完成的重叠I/O请求提供服务。
- 完成端口实际是Windows采用的一种I/O构造机制,除套接字之外,还可接受其他东西。
- 使用这种模型之前,首先要创建一个I/O完成端口对象,用它面向任何数量的套接字句柄,管理多个I/O请求。则需要调用CreateIoCompletionPort函数:
HANDLE WINAPI CreateIoCompletionPort( _In_ HANDLE FileHandle, _In_opt_ HANDLE ExistingCompletionPort, _In_ ULONG_PTR CompletionKey, _In_ DWORD NumberOfConcurrentThreads )
函数目的:1. 用于创建一个完成端口对象 2. 将一个句柄同完成端口关联到一起
目的一:最开始创建一个完成端口时,我们唯一感兴趣的参数便是NumberOfConcurrentThreads:前三个参数都不太重要。NumberOfConcurrentThreads参数的特殊之处在于,它定义了在一个完成端口上,同时允许执行的线程数量。在理想情况下,我们希望每个处理器各自负责一个线程的运行,为完成端口提供服务,避免过于频繁的线程任务转换。若将该参数设为0则告诉系统,安装了多少个处理器,则允许同时同时运行多少个线程。下面用于创建一个I/O完成端口:
CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,0)
该语句的作用是返回一个句柄,在为完成端口分配一个套接字句柄后,用来对那个端口进行辨识。
目的二:一旦在完成端口上拥有足够多的工作器线程来为I/O请求提供服务,便可着手将套接字句柄同完成端口关联到一起。这需要在一个现有的完成端口上,调用CreateIoCompletionPort函数。
FileHandle参数指定一个要同完成端口关联在一起的套接字句柄。
ExistingCompletionPort参数标识的是一个现有的完成端口套接字句柄已经与它关联在一起。
CompletionKey(完成键) 参数则标识的是要与某个特定套接字句柄关联在一起的单句柄数据。
CompletionKey 这个参数中,应用程序可保存于一个套接字对应的任意类型的信息。之所以把它叫做单句柄数据,是由于它代表了与套接字句柄关联在一起的数据。
下面构建了一个基本的应用程序框架。阐述了如何使用完成端口模型,来开发一个回应服务器的应用程序
- 创建一个完成端口。第四个参数保持为0,它指定在完成端口上每个处理器一次只运行执行一个工作器线程。
- 判断系统内有多少个处理器。
- 创建工作器线程,根据步骤2得到的处理器信息,在完成端口上为已完成的I/O请求提供服务。在这个简单的例子中,我们为每个处理器都只创建一个工作线程。这是由于事先已预计到,到时不会有任何线程进入暂停状态,造成由于线程数量不足而使处理器空闲的局面(没有足够的线程可供执行)。调用CreateThread函数时,必须同时提供一个工作器例程,由线程创建好后执行。
- 准备好一个监听套接字,在端口5150上监听传入的连接请求。
- 使用accept函数,接受入站的连接请求。
- 创建一个数据结构,用于容纳单句柄数据,同时在结构中存入接受的套接字句柄。
- 调用CreateIoCompletionPort,将自accept返回的新套接字句柄同完成端口关联到一起。通过CompletionKey参数,将单句柄数据结构传递给CreateIoCompletionPort。
- 开始在已接受的连接上进行I/O操作。在此,我们希望通过重叠I/O机制,在新建的套接字上投递一个或多个异步WSARecv或WSASend请求。这些I/O请求完成后,工作器线程会为I/O请求提供服务,同时继续处理以后的I/O请求。在步骤3指定的工作器例程中,我们将看到这一点。
- 重复步骤5~8,直至服务器终止。
typedef struct _PER_HANDLE_DATA { SOCKET Socket; SOCKADDR_STORAGE ClientAddr; //将和这个句柄关联的其他有用信息 }PER_HANDLE_DATA,*LPPER_HANDLE_DATA; int main() { HANDLE CompletionPort; WSADATA wsd; SYSTEM_INFO SystemInfo; SOCKADDR_IN InternetAddr; SOCKET Listen; int i; //加载Winsock WSAStartup(MAKEWORD(2, 2), &wsd); //第一步 创建一个I/O完成端口 CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); //第二步 确定系统中有多少个处理器 GetSystemInfo(&SystemInfo); //第三步 基于系统中可用的处理器数量创建工作器线程 //对这个简单例子而言,我们为每个处理器都创建一个工作线程 for (i = 0; i < SystemInfo.dwNumberOfProcessors; ++i) { HANDLE ThreadHandle; //创建一个服务器的工作线程,并将完成端口传递到该线程 //注意:ServerWorkerThread进程并不是在这个列表中定义的 ThreadHandle = CreateThread(NULL, 0, ServerWorkerTharead, CompletionPort, 0, NULL); //关闭线程句柄 CloseHandle(ThreadHandle); } //第四步 创建一个监听的套接字 Listen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED); InternetAddr.sin_family = AF_INET; InternetAddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY); InternetAddr.sin_port = htons(5150); bind(Listen, (PSOCKADDR)&InternetAddr, sizeof(InternetAddr)); //让套接字为监听做好准备 listen(Listen, 5); while (TRUE) { PER_HANDLE_DATA *PerHandleData = NULL; SOCKADDR_IN saRemote; SOCKET Accept; int RemoteLen; //第五步 接受连接,并分配到完成端口 RemoteLen = sizeof(saRemote); Accept = WSAAccept(Listen, (SOCKADDR *)&saRemote, &RemoteLen,......); //第六步 创建用来和套接字关联的单句柄数据信息结构 PerHandleData = (LPPER_HANDLE_DATA)GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA)); printf("Socket number %d connected \n", Accept); PerHandleData->Socket = Accept; memcpy(&PerHandleData->ClientAddr, &saRemote, RemoteLen); //第七步 将接受套接字和完成端口关联起来 CreateIoCompletionPort((HANDLE)Accept, CompletionPort, (DWORD)PerHandleData, 0); //第八步 开始在接受套接字上处理I/O //使用重叠I/O,在套接字上投递一个或多个WSASend(),或WSARecv调用 WSARecv(.....); } } DWORD WINAPI ServerWorkerThread(LPVOID lpParam) { //工作器线程的必备条件和稍后讨论 return 0; }
—–>完成端口和重叠I/O<—–
将套接字句柄与一个完成端口关联到一起后,便能以套接字句柄为基础,投递重叠发送与接收请求,开始对I/O请求进行处理。之后,可开始依赖完成端口,接收有关I/O操作完成情况的通知。从本质上说,完成端口模型利用了Windows重叠I/O机制。在这种机制中,类似WSASend和WSARecv这样的Winsock API调用会立即返回。此时,需要由应用程序负责在以后的某个时间,通过OVERLAPPED结构来检索调用的结果。在完成端口模型中,要想做到这一点,需要使用GetQueuedCompletionStatus函数,让一个或多个工作器线程在完成端口上等待。函数定义如下:
BOOL WINAPI GetQueuedCompletionStatus( _In_ HANDLE CompletionPort, //线程所在的完成端口 _Out_ LPDWORD lpNumberOfBytes, //I/O操作(WSASend WSARecv等)实际传输的字节数 _Out_ PULONG_PTR lpCompletionKey, //原先传递到CreateCompletionPort套接字返回的单句柄数据 _Out_ LPOVERLAPPED *lpOverlapped, //接收已完成的I/O操作的WSAOVERLAPPED结构 _In_ DWORD dwMilliseconds //等待一个完成数据包在完成端口上出现的时间(毫秒) );
单句柄数据和单I/O操作数据
当一个工作器线程从GetQueuedCompletionStatus这个API调用中接收到I/O完成通知后,在lpCompletionKey和lpOverlapped参数中,会包含一些必要的套接字信息。利用这些信息,可通过完成端口,继续在一个套接字上进行I/O处理。通过这些参数,可获得两种重要的套接字数据类型:单句柄数据以及单I/O操作数据。
因为在一个套接字首次与完成端口关联到一起的时候,单句柄数据便于一个特定的套接字句柄对应起来了,所以lpCompletionKey参数也就包含了单句柄数据。这些数据正是在进行CreateIoCompletionPort API调用的时候,通过CompletionKey参数传递的。如早先所述,应用程序可通过该参数传递任意类型的数据。通常情况下,应用程序会将I/O请求有关的套接字句柄保存在这里。
lpOverlapped参数则包含了一个OVERLAPPED结构,在它后面跟随单I/O操作数据。工作器线程处理一个完成数据包时(向应用程序、接受连接以及投递另一个线程等),这些信息是它必须要知道的。单I/O操作数据是包含在一个结构内的、任意数量的字节,这个结构本身也包含了一个结构传递进去,以满足它的要求。想要做到这一点,一个简单的方法是定义一个结构,然后将OVERLAPPED结构作为新结构的第一个元素使用。例如,可定义下述数据结构,实现对单I/O操作数据的管理:
typedef struct { OVERLAPPED Overlapped; char Buffer[DATA_BUFSIZE]; int BufferLen; int OperationType; }PER_IO_DATA;
该结构演示了通常要与I/O操作关联在一起的某些重要数据元素,例如刚才完成的那个I/O操作(发送或接收请求)的类型。在这个结构中,我们认为提供给已完成的I/O操作的数据缓冲区是非常有用的。要想调用一个Winsock API函数,同时为其分配一个OVERLAPPED结构,只要简单地撤销对结构中的OVERLAPPED元素的引用即可。
PER_IO_OPERATION_DATA PerIoData; WSABUF wbuf; DWORD Bytes,Flags; //初始化 wbuf..... WSARecv(socket,&wbuf,&Bytes,&Flags,&(PerIoData.Overlapped),NULL);
在工作器线程的后面部分,GetQueuedCompletionStatus函数返回了一个重叠结构和完成键。获取单I/O数据应使用宏CONTAINING_RECORD。例如:
PER_IO_DATA *PerIoData = NULL; OVERLAPPED *lpOverlapped = NULL; ret = GetQueuedCompletionStatus(ComportHandle, &Transferred, (PULONG_PTR)&CompletionKey, &lpOverlapped, INFINITE); //检查成功的返回 PerIoData = CONTAINING_RECORD(lpOverlapped, PER_IO_DATA, Overlapped);
应该使用这个宏,否则,结构PER_IO_DATA的成员OVERLAPPED就始终不得不首先出现,这会成为一个危险的假设。可以使用单I/O结构的一个字段来指示被投递的操作类型,从而可以确定到底是哪个操作投递到了句柄之上。
- 和Windows 完成端口有关的另一个重要方面:所有重叠操作可确保按照应用程序安排好的顺序执行。然而,不能确保从完成端口返回到完成通知也按照上述顺序执行。也就是说,对于两个重叠WSARecv操作,如果一个应用程序提供前者10KB的缓冲,而后者是12KB的缓冲,则10KB缓冲先被填充。应用程序的工作器线程可能会在10KB操作完成事件之前从GetQueuedCompletionStatus接收到12KB WSARecv的通知。当然,在一个套接字上投递多重操作时,这个问题不会造成多大影响。
为了完成前面所述的简单回应服务器示例,需要提供一个ServerWorkerThread函数。
//一个简单的IOCP的例子 网络上看到的,稍微修改了点
#include <WinSock2.h>
#include <windows.h>
#include <iostream>
#pragma comment(lib,"WS2_32")
using namespace std;
#define PORT 5050
#define DATA_BUFSIZE 8192
#define OutErr(a) cout<<(a)<<endl<<"file:"<<__FILE__<<endl<<"line:"<<__LINE__<<endl
#define OutMsg(a) cout<<(a)<<endl;
void InitWinsock()
{
WSADATA wsd;
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
{
OutErr("WSAstartup()");
}
}
#if 0
////////////////////////////////////client
#define MAXCNT 30000
#include <WS2tcpip.h>
int main()
{
InitWinsock();
static int nCnt = 0;
char SendBuf[2000];
while (nCnt < MAXCNT)//循环测试套接字创建关闭的情况 看服务端消息处理
{
SOCKET sockClient = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addrSrv;
//addrSrv.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
inet_pton(AF_INET, "127.0.0.1", (void*)&addrSrv.sin_addr.S_un.S_addr);
addrSrv.sin_family = AF_INET;
addrSrv.sin_port = htons(5050);//和服务器端的端口号保持一致
connect(sockClient, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));//连接服务器端(套接字,地址转换,长度)
sprintf_s(SendBuf, "This is TestNo : %d\n", ++nCnt);
send(sockClient, SendBuf, strlen(SendBuf), 0);
printf("send:%s", SendBuf);
// memset(recvBuf,0,100);
// recv(sockClient,recvBuf,100,0);//接收数据
// printf("%s\n",recvBuf);//打印
closesocket(sockClient);//关闭套接字
Sleep(1000);
}
WSACleanup();//终止对这个套接字库的使用
return 1;
}
#endif
//////////////////////////////////////////
#if 1
//************************************
// 函数名称: BindServerOverlapped
// 函数说明: 绑定端口,并返回一个Overlapped的ListenSocket
// 返 回 值: SOCKET
// 参数说明: int nPort
// 作 者: xcyk
// 日 期: 2017/04/21
//************************************
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(PORT);
servAddr.sin_addr.S_un.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 Filed!");
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 << "closeingsocket : " << 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 << " Send message is :" << PerIoData->Buffer << endl;
//继续向Socket投递WSARecv操作
DWORD Flags = 0;
DWORD dwRecv = 0;
memset(PerIoData, 0, 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;
}
int 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 + 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();
memset(PerIoData, 0,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);
return 0;
}
#endif
声明:以上整理于Windows网络编程(第二版)