网络编程--IOCP完成端口

写在前面

基于重叠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模型的理解。

至此,网络编程的学习将告一段落,这段的学习中均使用简单的回声服务器作为示例介绍,在更为复杂的工作环境中,应学会在公司现有框架中变通。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值