4.3.5完成端口模型(IOCP)
选择模型是5种模型中效率最低的,而完成端口则是5种模型中效率最高的IO模型。
//完成端口TCP服务器
#include <iostream>
#include <winsock2.h>
#include <windows.h>
#include <process.h>
#pragma comment (lib, "Ws2_32.lib")
using namespace std;
#define PORT 6000
#define SIZE 1024
//创建单IO结构体
typedef struct
{
WSAOVERLAPPED overlap; //每一个socket连接需要关联一个WSAOVERLAPPED对象
WSABUF Buffer; //与WSAOVERLAPPED对象绑定的缓冲区
char szMessage[SIZE]; //初始化buffer的缓冲区
DWORD NumberOfBytesRecvd; //指定接收到的字符的数目
DWORD Flags;
}MY_WSAOVERLAPPED, *LPMY_WSAOVERLAPPED;
UINT WINAPI WorkerThread(LPVOID lpParameter);
int main(int argc,char ** argv)
{
//步骤1:当前应用程序和相应的socket库绑定
WORD wVersionRequested;
WSADATA wsaData;
int err;
wVersionRequested = MAKEWORD(2, 2);
err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
{
cout << "WSAStartup Failed!" << endl;
return -1;
}
//步骤2:创建完成端口
HANDLE h_CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
//步骤3:根据系统中CPU核心的数量建立对应的Worker线程
SYSTEM_INFO systeminfo;
GetSystemInfo(&systeminfo);
unsigned int thread_id = 0;
for (int i = 0; i < systeminfo.dwNumberOfProcessors*2; i++)//建立CPU核心数量*2的线程,可以充分利用CPU资源
{
_beginthreadex(NULL, 0, WorkerThread, h_CompletionPort, 0, &thread_id);
}
//步骤4:创建监听套接字和服务器端IP/PORT
//SOCKET sockListen = WSASocket(AF_INET, SOCK_STREAM, 0,NULL,0, WSA_FLAG_OVERLAPPED);
SOCKET sockListen = socket(AF_INET, SOCK_STREAM, 0);
SOCKADDR_IN addr_server;
memset(&addr_server, 0, sizeof(addr_server));
addr_server.sin_family = AF_INET;
addr_server.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY表示绑定电脑上所有网卡IP
addr_server.sin_port = htons(PORT);//不能使用公认端口,即端口>= 1024
//步骤5:套接字绑定和监听
bind(sockListen, (SOCKADDR*)&addr_server, sizeof(addr_server));
listen(sockListen, 5);
cout << "Start Listen..." << endl;
SOCKADDR_IN addr_client;
int len = sizeof(SOCKADDR);
SOCKET sockClient;
LPMY_WSAOVERLAPPED lp_OVERLAPPED;
while (1)
{
//步骤6:等待客户端连接
sockClient = accept(sockListen, (struct sockaddr *)&addr_client, &len);
printf("Accepted Client IP:%s,PORT:%d\n", inet_ntoa(addr_client.sin_addr), ntohs(addr_client.sin_port));
//步骤7:完成端口和套接字绑定
CreateIoCompletionPort((HANDLE)sockClient, h_CompletionPort, (DWORD)sockClient, 0);
//步骤8:分配一个单IO数据结构
lp_OVERLAPPED = (LPMY_WSAOVERLAPPED)HeapAlloc(
GetProcessHeap(),
HEAP_ZERO_MEMORY,
sizeof(MY_WSAOVERLAPPED));
//步骤9:初始化单IO数据结构
lp_OVERLAPPED->Buffer.len = SIZE;//接收缓冲区的长度
lp_OVERLAPPED->Buffer.buf = lp_OVERLAPPED->szMessage;//接收缓冲区
//步骤10:接收数据
WSARecv(
sockClient,//接收套接字
&lp_OVERLAPPED->Buffer,//接收缓冲区
1,
&lp_OVERLAPPED->NumberOfBytesRecvd,//操作完成,接收数据的字节数
&lp_OVERLAPPED->Flags,
&lp_OVERLAPPED->overlap,//指向WSAOVERLAPPED结构指针
NULL);
}
//步骤13:唤醒工作者线程
PostQueuedCompletionStatus(h_CompletionPort, 0xFFFFFFFF, 0, NULL);
//步骤14:关闭套接字和库解绑
CloseHandle(h_CompletionPort);
closesocket(sockListen);
WSACleanup();
return 0;
}
UINT WINAPI WorkerThread(LPVOID lpParameter)
{
HANDLE h_CompletionPort = (HANDLE)lpParameter;
DWORD NumberOfBytesTransferred;
SOCKET sockClient;
LPMY_WSAOVERLAPPED lpWSAOVERLAPPED = NULL;
while (1)
{
//步骤11:获取完成端口的状态。若完成端口出现已完成的IO请求,则线程被唤醒;否则继续睡眠
GetQueuedCompletionStatus(
h_CompletionPort,
&NumberOfBytesTransferred,
(LPDWORD)&sockClient,
(LPOVERLAPPED*)&lpWSAOVERLAPPED,
INFINITE);
//步骤12:数据处理
if (NumberOfBytesTransferred == 0xFFFFFFFF)
{
return 0;
}
if (NumberOfBytesTransferred == 0)
{
cout << "客户端断开连接" << endl;
closesocket(sockClient);
HeapFree(GetProcessHeap(), 0, lpWSAOVERLAPPED);
}
else
{
//二次开发
char Buf[SIZE] = "\0";
strcpy_s(Buf, 1024, lpWSAOVERLAPPED->szMessage);
cout << Buf << endl;
strcat(Buf, ":Server Received");
send(sockClient, Buf, strlen(Buf) + 1, 0);
memset(lpWSAOVERLAPPED, 0, sizeof(MY_WSAOVERLAPPED));
lpWSAOVERLAPPED->Buffer.len = SIZE;
lpWSAOVERLAPPED->Buffer.buf = lpWSAOVERLAPPED->szMessage;
WSARecv(sockClient,
&lpWSAOVERLAPPED->Buffer,
1,
&lpWSAOVERLAPPED->NumberOfBytesRecvd,
&lpWSAOVERLAPPED->Flags,
&lpWSAOVERLAPPED->overlap,
NULL);
}
}
return 0;
}
//完成端口TCP客户端
#include <WinSock2.h>
#include <iostream>
#pragma comment(lib, "ws2_32.lib")
using namespace std;
int main()
{
// Initialize Windows socket library
WSADATA wsaData;
WSAStartup(0x0202, &wsaData);
// Create client socket
SOCKET sockClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Connect to server
SOCKADDR_IN server;
memset(&server, 0, sizeof(SOCKADDR_IN));
server.sin_family = AF_INET;
server.sin_addr.S_un.S_addr = inet_addr("192.168.137.144");
server.sin_port = htons(6000);
connect(sockClient, (struct sockaddr *)&server, sizeof(SOCKADDR_IN));
while (1)
{
cout << "send:";
char Buf[1024] = "\0";
cin.getline(Buf, 1024);
// Send message
send(sockClient, Buf, strlen(Buf) + 1, 0);
// Receive message
recv(sockClient, Buf, 1024, 0);
printf("Received: '%s'\n", Buf);
}
// Clean up
closesocket(sockClient);
WSACleanup();
return 0;
}
HANDLE CreateIoCompletionPort(HANDLE FileHandle,HANDLE ExistingCompletionPort,ULONG_PTR CompletionKey,DWORD NumberOfConcurrentThreads):创建完成端口、或完成端口和套接字绑定
参数FileHandle:打开重叠IO完成端口的文件句柄。如果设置这个参数为INVALID_HANDLE_VALUE,CreateIoCompletionPort会创建一个不关联任何文件的完成端口,而且ExistingCompletionPort必须设置为NULL,CompletionKey也将被忽略;
参数ExistingCompletionPort:完成端口句柄。如果指定一个已经存在的完成端口,函数将关联FileHandle指定的文件;
参数CompletionKey:单文件句柄,包含指定文件每次IO完成数据包信息;
参数NumberOfConcurrentThreads:系统允许在完成端口上并发处理IO完成包的最大线程数量。
BOOL GetQueuedCompletionStatus(HANDLE CompletionPort,LPDWORD lpNumberOfBytesTransferred,PULONG_PTR lpCompletionKey,LPOVERLAPPED* lpOverlapped,DWORD dwMilliseconds):查询完成端口状态
参数CompletionPort:指定的完成端口(IOCP),由CreateIoCompletionPort函数创建;
参数lpNumberOfBytesTransferred:一次I/O操作完成后,所传送数据的字节数;
参数lpCompletionKey:当文件I/O操作完成后,用于存放与完成端口关联的socket;
参数lpOverlapped:为调用IOCP机制所引用的OVERLAPPED结构;
参数dwMilliseconds: 等待完成端口的超时时间,INFINITE表示一直等待。
BOOL PostQueuedCompletionStatus(HANDLE CompletionPort,DWORD dwNumberOfBytesTransferred,ULONG_PTR dwCompletionKey,LPOVERLAPPED lpOverlapped):唤醒工作者线程
参数CompletionPort:指定想向其发送一个完成数据包的完成端口对象;
参数dwNumberOfBytesTransferred:指定—个值,直接传递给GetQueuedCompletionStatus函数中对应的参数 ;
参数dwCompletionKey:指定—个值,直接传递给GetQueuedCompletionStatus函数中对应的参数;
参数lpOverlapped:指定—个值,直接传递给GetQueuedCompletionStatus函数中对应的参数。
当调用GetQueuedCompletionStatus,若完成端口无IO请求,则线程睡眠。如果完成端口上一直都没有已经完成的I/O请求,那么这些线程将无法被唤醒,这也意味着线程没法正常退出。PostQueuedCompletionStatus函数让我们手动的添加一个完成端口I/O操作,这样处于睡眠等待的状态的线程就会有一个被唤醒,如果为我们每一个工作线程都调用一次PostQueuedCompletionStatus(),那么所有的线程也就会因此而被唤醒了。
总结:完成端口是C/S通信模式中最好的I/O模型,也是最复杂的I/O模型。
重点1:一般情况下,我们只需要建立一个完成端口,保存好它的句柄,后面会经常用到。
重点2:系统中有多少个处理器,可以建立处理器数量*2个线程。
重点3:可以用accept来接收客户端的连接请求,也可以使用性能更好的AcceptEx来接收客户端连接请求。accept阻塞;AcceptEx非阻塞。
重点4:客户端连接后,就可以在socket上提交网络请求,如WSARecv。
推荐小猪的博客:https://blog.csdn.net/piggyxp/article/details/6922277,这是国内对完成端口解释最详细的博客。
4.3.6总结
该如何挑选最适合自己应用程序的 I / O模型?
1. 客户机的开发
若打算开发一个客户机应用,令其同时管理一个或多个套接字,那么建议采用重叠I/O或WSAEventSelect模型,以便在一定程度上提升性能。然而,假如开发的是一个以Windows为基础的应用程序,要进行窗口消息的管理,那么WSAAsyncSelect模型恐怕是一种最好的选择,因为WSAAsyncSelect本身便是从Windows消息模型借鉴来的。若采用这种模型,我们的程序一开始便具备了处理消息的能力。
2. 服务器的开发
若开发的是一个服务器应用,要在一个给定的时间,同时控制几个套接字,建议大家采用重叠I/O模型,这同样是从性能出发点考虑的。但是,如果预计到自己的服务器在任何给定的时间,都会为大量I/O请求提供服务,便应考虑使用I/O完成端口模型,从而获得更好的性能。
节选自《网络编程》第8章。