写在前面
基于重叠IO模型实现回声服务器一文中基于重叠IO模型实现了一个简单的回声服务器,这里再回看,可以发送有一些明显的缺点:在循环中重复调用非阻塞模式的accept和进入alertable wait(可警告等待状态)的SleepEx函数,这两个函数的频繁调用将会影响整体性能。
可以考虑使用以下方式解决:在main主线程中调用accept函数,再单独创建一个线程负责客户端IO。
这就是IOCP完成端口中采用的服务器端模型。即IOCP模型将创建专用的IO线程,该线程负责与所有客户端进行IO交互。
IOCP完成端口的使用
创建完成端口
IOCP中已完成的IO信息将注册到完成端口对象(Completion Port,简称CP对象),即该套接字的IO完成时,操作系统会把套接字的状态信息注册到指定对应的CP对象。
该过程称为”套接字和CP对象之间的连接“。
为完成上述连接,需进行以下两个步骤:
①创建套接字和完成端口对象
②建立完成端口对象和套接字之间的连接
注意:这里的套接字必须被赋予重叠属性。
这两步可使用一个函数完成,只是在两次调用时的参数意义有些区别。
#include <windows.h>
HANDLE CreateIoCompetionPort(HANDLE FileHandle, HANDLE ExistingCompletionPort, ULONG_PTR CompletionKey, DWORD NumberOfConcurrentThreads);
该函数调用成功时返回CP对象句柄,失败时返回NULL。
这里分步骤解释各参数含义。
以创建CP对象为目的调用CreateIoCompletionPort
以创建CP对象为目的调用CreateIoCompletionPort函数时,只有最后一个参数才具有意义,其他参数均可忽略。
FileHandle: 创建CP对象时无意义,传递INVALID_HANDLE_VALUE
ExistingCompletionPort:创建CP对象时无意义,传递NULL
CompletionKey:创建CP对象时无意义,传递0
NumberOfConcurrentThreads:分配给CP对象用于处理IO的线程数量。例,该参数为2时,说明分配给CP对象的可以同时运行的线程数最多为2个,如果该参数为0,则系统中CPU的个数就是可同时运行的最大线程数。
创建完成端口对象示例:
//创建可同时运行2个线程处理IO的完成端口
HANDLE hCpObject = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 2);
以连接完成端口和套接字为目的调用CreateIoCompletionPort
需要将CP对象连接到套接字,这样才能使已完成的套接字IO信息注册到CP对象。
FileHandle: 要连接的CP对象的套接字句柄,注意需要重叠属性的套接字
ExistingCompletionPort:要连接套接字的CP对象句柄
CompletionKey:传递已完成IO的相关信息,该参数在稍后介绍的GetQueuedCompletionStatus函数中再详细说明
NumberOfConcurrentThreads:无论传递何值,只要该函数的第二个参数非NULL就会忽略
连接完成端口和套接字示例:
//创建具有重叠属性的套接字
SOCKET hSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
//创建可同时运行2个线程处理IO的完成端口
HANDLE hCPObject = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 2);
//连接套接字和完成端口,暂时忽略IO信息参数
CreateIoCompletionPort(hSock, hCPObject, (DWORD)ioInfo, 0);
第二次调用CreateIoCompletionPort函数后,只要针对hSock的IO完成,相关信息就会注册到hCPObject指向的CP对象。
确认完成端口已完成的IO和线程的IO处理
在IOCP中,使用GetQueuedCompletionStatus确认CP对象中注册的已完成的IO,并得到对应IO信息。
#include <windows.h>
BOOL GetQueuedCompletionStatus(HANDLE CompletionPort, LPDWORD lpNumberOfBytes, PULONG_PTR lpCompletionKey, LPOVERLAPPED* lpOverlapped, DWROD dwMilliseconds);
返回值及参数说明:
返回值:成功时返回TRUE,失败时返回FALSE
CompletionPort: 注册已完成的IO信息的CP对象句柄
lpNumberOfBytes:用于保存IO过程中传输的数据大小的变量地址值
lpCompletionKey:用于保存CreateIoCompletionPort函数的第三个参数值的变量地址值,即上面提到打IO信息变量。
lpOverlapped:用于保存调用WSASend、WSARecv函数时传递的OVERLAPPED结构体地址的变量地址值,注意这是一个双指针,用于保存地址的地址。
dwMilliseconds:超时信息,超过该指定时间后将返回FALSE并跳出函数。传递INFINITE时,程序将阻塞,直到已完成IO信息写入CP对象。
该函数主要需要理解第三、四个参数,这2个参数主要是为了获取需要的信息(IO以及WSAOVERLAPPED信息)。
通过GetQueuedCompletionStatus函数的第三个参数得到的是 ”以连接套接字和CP对象为目的调用CreateIoCompletionPort“ 中函数传入的第三个参数值。即上面的 (DWORD)ioInfo,待IO完成后,操作系统会将该信息注册到CP对象,传递到该函数的第三个参数中。
通过GetQueuedCompletionStatus函数的第四个参数得到的是调用WSASend、WSARecv函数时传入的WSAOVERLAPPED结构体变量地址值。
了解确认完成端口已完成的IO后,还需要确定在何时何地调用该函数。
还记得 ”以创建完成端口对象为目的调用CreateIoCompletionPort“ 时指定的线程数参数吗,在IOCP模型中,将在处理IOCP中已完成的IO的线程调用该函数。
注意这里线程不会自动创建,”以创建完成端口对象为目的调用CreateIoCompletionPort“ 时指定的只是最大线程数量,实际的线程创建需由开发人员手动创建调用WSASend和WSARecv等IO函数的线程。
并且在这些线程中会调用GetQueuedCompletionStatus以确认IO是否完成。
虽然任何线程都能调用GetQueuedCompletionStatus,但实际得到IO完成信息的线程数不会超过 ”以创建完成端口对象为目的调用CreateIoCompletionPort“ 时指定的最大线程数量。
有点像信号量,比如声明一个值为3(指定的最大线程数量)的信号量对象(IO完成信息),当有3个(处理IO的)线程得到信号量(IO完成信息)后,信号量就会变成non-signaled状态,其他信息就获取不到信号量对象了(IO完成信息)。
基于IOCP的回声服务器
// IOCPEchoServer.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <WinSock2.h>
#include <Windows.h> //需确保Windows.h 的包含在Winsock2.h后
#include <process.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 100
#define READ 3
#define WRITE 5
//自定义客户端套接字信息结构体,后会传递到CreateIoCompletionPort的第三个参数
typedef struct //socket info
{
SOCKET cltSock;
SOCKADDR_IN cltAddr;
}PER_HANDLE_DATA, *LPPER_HANDLE_DATA;
typedef struct //buffer info
{
OVERLAPPED overlapped;
WSABUF wsaBuf;
char buffer[BUF_SIZE];
int rwMode; //READ or WRITE
}PER_IO_INFO, *LPPER_IO_INFO;
unsigned int WINAPI EchoThreadMain(LPVOID CompletionPortIO);
int _tmain(int argc, _TCHAR* argv[])
{
if (argc != 2)
{
puts("argc error!");
return -1;
}
WSADATA wsaData;
if (0 != WSAStartup(MAKEWORD(2, 2), &wsaData))
{
puts("WSAStartup error!");
return -1;
}
//创建完成端口对象
HANDLE hComPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, NULL, 0);
SYSTEM_INFO sysInfo;
GetSystemInfo(&sysInfo);
//分配系统中CPU的个数,即可同时运行的最大线程数给完成端口处理IO
for (int i = 0; i < sysInfo.dwNumberOfProcessors; i++)
{
_beginthreadex(NULL, 0, EchoThreadMain, (LPVOID)hComPort, 0, NULL);
}
//创建监听套接字
SOCKET srvSock = WSASocket(PF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
SOCKADDR_IN srvAddr;
memset(&srvAddr, 0, sizeof(srvAddr));
srvAddr.sin_family = PF_INET;
srvAddr.sin_addr.s_addr = htonl(ADDR_ANY);
srvAddr.sin_port = htons(_ttoi(argv[1]));
bind(srvSock, (sockaddr*)&srvAddr, sizeof(srvAddr));
listen(srvSock, 5);
int nRecvLen = 0, flags = 0;
while (true)
{
SOCKET cltSock;
SOCKADDR_IN cltAddr;
int nCltAddrSize = sizeof(cltAddr);
memset(&cltAddr, 0, nCltAddrSize);
puts("wait client connect...");
cltSock = accept(srvSock, (sockaddr*)&cltAddr, &nCltAddrSize);
if (INVALID_SOCKET == cltSock)
{
puts("accept error!");
continue;
}
//更新客户端地址信息
LPPER_HANDLE_DATA pCltInfo = new PER_HANDLE_DATA;
pCltInfo->cltSock = cltSock;
memcpy(&(pCltInfo->cltAddr), &cltAddr, nCltAddrSize);
//将客户端套接字连接到完成端口
CreateIoCompletionPort((HANDLE)cltSock, hComPort, (ULONG_PTR)pCltInfo, 0);
LPPER_IO_INFO pCltIOInfo = new PER_IO_INFO;
memset(&(pCltIOInfo->overlapped), 0, sizeof(WSAOVERLAPPED));
pCltIOInfo->wsaBuf.len = BUF_SIZE;
pCltIOInfo->wsaBuf.buf = pCltIOInfo->buffer;
pCltIOInfo->rwMode = READ;
//调用IO函数等待IO完成, 非阻塞IO,nRecvLen无意义,这里会直接返回到下一循环的accept阻塞
WSARecv(cltSock, &(pCltIOInfo->wsaBuf), 1, (LPDWORD)&nRecvLen, (LPDWORD)&flags, &(pCltIOInfo->overlapped), NULL);
}
closesocket(srvSock);
WSACleanup();
puts("任意键继续...");
getchar();
return 0;
}
unsigned int WINAPI EchoThreadMain(LPVOID CompletionPortIO)
{
//得到通过参数传来的完成端口对象
HANDLE hComPort = (HANDLE)CompletionPortIO;
DWORD dwBytesTrans;
LPPER_HANDLE_DATA pCltInfo;
LPPER_IO_INFO pCltIOInfo;
int flags = 0;
while (true)
{
//等待IO完成
GetQueuedCompletionStatus(hComPort, &dwBytesTrans, (PULONG_PTR)&pCltInfo, (LPOVERLAPPED*)&pCltIOInfo, INFINITE);
SOCKET cltSock = pCltInfo->cltSock;
if (pCltIOInfo->rwMode == READ)
{
//puts("message received!");
//printf("dwBytesTrans: %d, len: %d \n", dwBytesTrans, pCltIOInfo->wsaBuf.len);
if (dwBytesTrans == 0)
{
closesocket(cltSock);
if (pCltIOInfo != nullptr)
{
delete pCltIOInfo;
pCltIOInfo = nullptr;
}
if (pCltInfo != nullptr)
{
delete pCltInfo;
pCltInfo = nullptr;
}
continue;
}//end if (dwBytesTrans == 0)
memset(&(pCltIOInfo->overlapped), 0, sizeof(OVERLAPPED));
pCltIOInfo->wsaBuf.len = dwBytesTrans;
pCltIOInfo->rwMode = WRITE;
WSASend(cltSock, &(pCltIOInfo->wsaBuf), 1, NULL, 0, &(pCltIOInfo->overlapped), NULL);
//这里等待下一次接收,而下一次接收不确定来自同一个(上面WSASend的)套接字还是其他客户端的套接字,因此这里需要重新申请OVERLAPPED
pCltIOInfo = new PER_IO_INFO;
memset(&(pCltIOInfo->overlapped), 0, sizeof(OVERLAPPED));
memset(&(pCltIOInfo->buffer), 0, BUF_SIZE);
pCltIOInfo->wsaBuf.len = BUF_SIZE;
pCltIOInfo->wsaBuf.buf = pCltIOInfo->buffer;
pCltIOInfo->rwMode = READ;
WSARecv(pCltInfo->cltSock, &(pCltIOInfo->wsaBuf), 1, NULL, (LPDWORD)&flags, &(pCltIOInfo->overlapped), NULL);
}//end if (pCltIOInfo->rwMode == READ)
}
return 0;
}
IOCP性能更优的原因
相比此前介绍的:
select实现IO复用
异步通知IO模型
重叠IO模型
在代码上和select对比,可以发现IOCP具有如下特点:
①因为是非阻塞模式的IO,所以不会由IO引发延迟或阻塞
②查找已完成IO时无需再循环查找,因为有对应CP对象可以快速找到对应的已完成IO的信息
③无需将IO对象的套接字保存到数组进行管理(select实现IO复用中使用数组分IO类型同一监视管理)。IOCP中CP对象和套接字一一对应。
④可以调整处理IO的线程数量,可在实验数据的基础上选用合适的线程数。
IOCP是Windows特有的功能,因此需熟练掌握以在实际工作中应用。
总结
在此之前,我们介绍了Windows网络编程中常见的几种IO模型及应用,相信后续在实际工作中碰到也能了解其实现,本文介绍的最后一种IO模型–IOCP完成端口在Windows网络编程中极为常见,因此需熟练掌握应用,本文的最后也简单分析了一下几种IO模型的差异以加深对几种IO模型的理解。
至此,网络编程的学习将告一段落,这段的学习中均使用简单的回声服务器作为示例介绍,在更为复杂的工作环境中,应学会在公司现有框架中变通。