完成端口模型即Win32通过一个完成端口对象,统筹指定数量的工作线程对重叠I/O请求进行管理,以便为已经完成的重叠I/O请求提供服务的I/O模型。在使用这种模型之前,首先要创建一个I/O完成端口对象,用它面向任意数量的套接字句柄,管理多个I/O请求。将套接字句柄与一个完成端口关联在一起后,便可以套接字句柄为基础,投递发送或接收请求,开始I/O请求的处理。接下来,可开始依赖完成端口,接收有关I/O操作完成情况的通知。
对完成端口来说,将一个套接字绑定到完成端口后,WSARecv和WSASend会立即返回,可以调用GetQueuedCompletionStatus来判断WSARecv和WSASend是否完成。这样主程序就可以完全等待接收新的连接,线程程序来等待WSARecv和WSASend是否完成。
涉及的函数:
(1)
HANDLE CreateIoCompletionPort(
HANDLE FileHandle,
HANDLE ExistingCompletionPort,
ULONG_PTR CompletionKey,
DWORD NumberOfConcurrentThreads
);
该函数实际用于两个明显有别的目的:
[1]. 用于创建一个完成端口对象。
最开始创建一个完成端口时,唯一感兴趣的参数便是NumberOfConcurrentThreads(并发线程的数量);前面三个参数都会被忽略。NumberOfConcurrentThreads参数的特殊之处在于,它定义了在一个完成端口上,同时允许执行的线程数量。理想情况下,我们希望每个处理器各自负责一个线程的运行,为完成端口提供服务,避免过于频繁的线程“场景”切换。若将该参数设为0,表明系统内安装了多少个处理器,便允许同时运行多少个线程!可用下述代码创建一个I/O完成端口: hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
[2]. 将一个句柄同完成端口关联到一起。
NumberOfConcurrentThreads参数明确指示系统:在一个完成端口上,一次只允许n个工作者线程运行。 FileHandle参数指定一个要同完成端口关联在一起的套接字句柄。ExistingCompletionPort参数指定的是一个现有的完成端口。CompletionKey(完成键)参数则指定要与某个特定套接字句柄关联在一起的“单句柄数据”
(2)
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,
LPDWORD lpNumberOfBytes,
PULONG_PTR lpCompletionKey,
LPOVERLAPPED* lpOverlapped,
DWORD dwMilliseconds
);
其中,CompletionPort参数对应于要在上面等待的完成端口。lpNumberOfBytes参数负责在完成了一次I/O操作后(如WSASend或WSARecv),接收实际传输的字节数。lpCompletionKey参数为原先传递进入CreateIoCompletionPort函数的套接字返回“单句柄数据”。如我们早先所述,大家最好将套接字句柄保存在这个“键”(Key)中。lpOverlapped参数用于接收完成的I/O操作的重叠结果。这实际是一个相当重要的参数,因为可用它获取每个I/O操作的数据。而最后一个参数,dwMilliseconds,用于指定调用者希望等待一个完成数据包在完成端口上出现的时间。假如将其设为INFINITE,调用会无休止地等待下去。
(3)BOOL PostQueuedCompletionStatus(
HANDLE CompletionPort,
DWORD dwNumberOfBytesTransferred,
ULONG_PTR dwCompletionKey,
LPOVERLAPPED lpOverlapped
);
CompletionPort参数指定想向其发送一个完成数据包的完成端口对象。而就dwNumberOfBytesTransferred、dwCompletionKey和lpOverlapped这三个参数来说,每一个都允许我们指定一个值,直接传递给GetQueuedCompletionStatus函数中对应的参数。这样一来,一个工作者线程收到传递过来的三个GetQueuedCompletionStatus函数参数后,便可根据由这三个参数的某一个设置的特殊值,决定何时应该退出。例如,可用dwCompletionPort参数传递0值,而一个工作者线程会将其解释成中止指令。一旦所有工作者线程都已关闭,便可使用CloseHandle函数,关闭完成端口,最终安全退出程序。
在此模式下的编程步骤如下:
【1】创建一个完成端口。第四个参数保持为0,指定在完成端口上,每个处理器一次只允许执行一个工作者线程。
【2】 判断系统内到底安装了多少个处理器。
【3】 创建工作者线程,根据步骤2)得到的处理器信息,在完成端口上,为已完成的I/O请求提供服务
【4】准备好一个监听套接字,在端口5150上监听进入的连接请求。
【5】 使用accept函数,接受进入的连接请求。
【6】 创建一个数据结构,用于容纳“单句柄数据”,同时在结构中存入接受的套接字句柄。
【7】 调用CreateIoCompletionPort,将自accept返回的新套接字句柄同完成端口关联到一起。通过完成键(CompletionKey)参数,将单句柄数据结构传递给CreateIoCompletionPort。
【8】 开始在已接受的连接上进行I/O操作。在此,我们希望通过重叠I/O机制,在新建的套接字上投递一个或多个异步WSARecv或WSASend请求。这些I/O请求完成后,一个工作者线程会为I/O请求提供服务,同时继续处理未来的I/O请求,稍后便会在步骤3 )指定的工作者例程中,体验到这一点。
【9】 重复步骤5 ) ~8 ),直至服务器中止。
#include <WINSOCK2.H>
#include <stdio.h>
#define PORT 5150
#define MSGSIZE 1024
#pragma comment(lib, "ws2_32.lib")
typedef enum
{
RECV_POSTED
}OPERATION_TYPE;
typedef struct
{
WSAOVERLAPPED overlap;
WSABUF Buffer;
char szMessage[MSGSIZE];
DWORD NumberOfBytesRecvd;
DWORD Flags;
OPERATION_TYPE OperationType;
}PER_IO_OPERATION_DATA, *LPPER_IO_OPERATION_DATA;
DWORD WINAPI WorkerThread(LPVOID);
int main()
{
WSADATA wsaData;
SOCKET sListen, sClient;
SOCKADDR_IN local, client;
DWORD i, dwThreadId;
int iaddrSize = sizeof(SOCKADDR_IN);
HANDLE CompletionPort = INVALID_HANDLE_VALUE;
SYSTEM_INFO systeminfo;
LPPER_IO_OPERATION_DATA lpPerIOData = NULL;
// Initialize Windows Socket library
WSAStartup(0x0202, &wsaData);
// Create completion port
CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
// Create worker thread
GetSystemInfo(&systeminfo);
for (i = 0; i < systeminfo.dwNumberOfProcessors; i++)
{
CreateThread(NULL, 0, WorkerThread, CompletionPort, 0, &dwThreadId);
}
// Create listening socket
sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Bind
local.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
local.sin_family = AF_INET;
local.sin_port = htons(PORT);
bind(sListen, (struct sockaddr *)&local, sizeof(SOCKADDR_IN));
// Listen
listen(sListen, 3);
while (TRUE)
{
// Accept a connection
sClient = accept(sListen, (struct sockaddr *)&client, &iaddrSize);
printf("Accepted client:%s:%d/n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
// Associate the newly arrived client socket with completion port
CreateIoCompletionPort((HANDLE)sClient, CompletionPort, (DWORD)sClient, 0);
// Launch an asynchronous operation for new arrived connection
lpPerIOData = (LPPER_IO_OPERATION_DATA)HeapAlloc(
GetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(PER_IO_OPERATION_DATA));
lpPerIOData->Buffer.len = MSGSIZE;
lpPerIOData->Buffer.buf = lpPerIOData->szMessage;
lpPerIOData->OperationType = RECV_POSTED;
WSARecv(sClient,
&lpPerIOData->Buffer,
1,
&lpPerIOData->NumberOfBytesRecvd,
&lpPerIOData->Flags,
&lpPerIOData->overlap,
NULL);
}
PostQueuedCompletionStatus(CompletionPort, 0xFFFFFFFF, 0, NULL);
CloseHandle(CompletionPort);
closesocket(sListen);
WSACleanup();
return 0;
}
DWORD WINAPI WorkerThread(LPVOID CompletionPortID)
{
HANDLE CompletionPort=(HANDLE)CompletionPortID;
DWORD dwBytesTransferred;
SOCKET sClient;
LPPER_IO_OPERATION_DATA lpPerIOData = NULL;
while (TRUE)
{
GetQueuedCompletionStatus(
CompletionPort,
&dwBytesTransferred,
&sClient,
(LPOVERLAPPED *)&lpPerIOData,
INFINITE);
if (dwBytesTransferred == 0xFFFFFFFF)
{
return 0;
}
if (lpPerIOData->OperationType == RECV_POSTED)
{
if (dwBytesTransferred == 0)
{
// Connection was closed by client
closesocket(sClient);
HeapFree(GetProcessHeap(), 0, lpPerIOData);
}
else
{
lpPerIOData->szMessage[dwBytesTransferred] = '/0';
send(sClient, lpPerIOData->szMessage, dwBytesTransferred, 0);
// Launch another asynchronous operation for sClient
memset(lpPerIOData, 0, sizeof(PER_IO_OPERATION_DATA));
lpPerIOData->Buffer.len = MSGSIZE;
lpPerIOData->Buffer.buf = lpPerIOData->szMessage;
lpPerIOData->OperationType = RECV_POSTED;
WSARecv(sClient,
&lpPerIOData->Buffer,
1,
&lpPerIOData->NumberOfBytesRecvd,
&lpPerIOData->Flags,
&lpPerIOData->overlap,
NULL);
}
}
}
return 0;
}