C++ Windows Socket五种I/O模型之完成端口模型

前面几章已经分别介绍了window下socket 网络编程的几种模式,今天简单的介绍一下最后一个模型:完成端口(completionPort)模型。
关于他的一些优点网上有一堆,这边我也不再一一介绍,点而言之就是他充分利用内核对象的调度,只使用少量的几个线程来梳理和客户端所有通信,最大限度的提高了网络通信的性能。下面简单介绍一下主要涉及主要函数。

最优线程数
根据实际应用中发现,Cup核数*2+2这个数量是最优线程数(网上查询到的,我也不知道原因),获取系统内核数可调用

VOID GetSystemInfo(LPSYSTEM_INFO lpSystemInfo)

通过LPSYSTEM_INFO 结构体获取内核数

SYSTEM_INFO sys_info;
GetSystemInfo(&sys_info);
//获取CPU核数
int n = sys_info.dwNumberOfProcessors;

CreateIoCompletionPort函数
创建一个完成端口,函数原型为

HANDLE WINAPI CreateIoCompletionPort(
  _In_     HANDLE    FileHandle,
  _In_opt_ HANDLE    ExistingCompletionPort,
  _In_     ULONG_PTR CompletionKey,
  _In_     DWORD     NumberOfConcurrentThreads
);

参数:
FileHandle
打开的文件句柄或INVALID_HANDLE_VALUE。
该句柄必须指向支持重叠I / O的对象。
如果提供了句柄,则必须打开它才能完成I / O重叠。例如,在使用CreateFile函数获取句柄时,必须指定FILE_FLAG_OVERLAPPED标志 。
如果指定了INVALID_HANDLE_VALUE,那ExistingCompletionPort参数必须为NULL,并且CompletionKey参数将被忽略
ExistingCompletionPort 现有I / O完成端口或NULL的句柄。
如果此参数指定了现有的I / O完成端口,则该函数将其与FileHandle参数指定的句柄相关联。如果成功,该函数将返回现有I / O完成端口的句柄;它不会创建新的I / O完成端口。
如果此参数为NULL,则该函数将创建一个新的I / O完成端口,并且如果FileHandle参数有效,则将其与新的I / O完成端口关联。否则,不会发生文件句柄关联。如果成功,该函数将句柄返回到新的I / O完成端口。
CompletionKey
每个句柄用户定义的完成密钥,包含在指定文件句柄的每个I / O完成数据包中。
NumberOfConcurrentThreads
操作系统可以允许同时处理I / O完成端口的I / O完成数据包的最大线程数。如果ExistingCompletionPort参数不为NULL,则忽略此参数。
如果此参数为零,则系统允许的并发运行线程数与系统中的处理器数量一样。
综上:如果你要创建一个新的完成端口,第一个参数必须设置为INVALID_HANDLE_VALUE,第二个参数为NULL,第三个忽略,第四个参数是运行的最大线程数量,例如:

//创建完成端口
	HANDLE g_completionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

如果要在创建的端口单口上关联一个I/O操作,如sokcet 监听套接字,具体操作如下:

SOCKET listenSock=socket(AF_INET, SOCK_STREAM, 0);
//绑定监听套接字和端口
CreateIoCompletionPort((HANDLE)listenSock, g_completionPort, (DWORD)listenSock, 0);

返回值:
如果函数成功,则返回值是I / O完成端口的句柄:
如果ExistingCompletionPort参数为NULL,则返回值为新句柄。
如果ExistingCompletionPort参数是有效的I / O完成端口句柄,则返回值就是该句柄。
如果FileHandle参数是有效的句柄,则该文件句柄现在与返回的I / O完成端口关联。
如果函数失败,则返回值为NULL。若要获取扩展的错误信息,请调用GetLastError函数。

ACCEPTEX 函数
此函数是一个Microsoft特定的扩展,原型如下:

BOOL AcceptEx(  
  _In_  SOCKET       sListenSocket,  
  _In_  SOCKET       sAcceptSocket,  
  _In_  PVOID        lpOutputBuffer,  
  _In_  DWORD        dwReceiveDataLength,  
  _In_  DWORD        dwLocalAddressLength,  
  _In_  DWORD        dwRemoteAddressLength,  
  _Out_ LPDWORD      lpdwBytesReceived,  
  _In_  LPOVERLAPPED lpOverlapped  
);  

但是平不是所有的系统都支持使用这个API,并且获取的开销很大,不建议直接使用(好多资料上都是这么说的,具体未研究)
微软给了俩个特定指针来解析这个动作,固定的格式:

LPFN_ACCEPTEX        lpfnAcceptEx;
LPFN_GETACCEPTEXSOCKADDRS lpfnGetAcceptExSockaddrs;
GUID guidAcceptEx = WSAID_ACCEPTEX;
GUID guidGetAddr = WSAID_GETACCEPTEXSOCKADDRS;
	//两个扩展函数,成功都是返回0 
DWORD       bytes;

	//加载AccpetEx函数指针
	int rc = WSAIoctl(
		listenSock,
		SIO_GET_EXTENSION_FUNCTION_POINTER,
		&guidAcceptEx,
		sizeof(guidAcceptEx),
		&lpfnAcceptEx,
		sizeof(lpfnAcceptEx),
		&bytes,
		NULL,
		NULL
	);

	//加载GetAcceptExSockaddrs函数指针
	rc = WSAIoctl(
		listenSock,
		SIO_GET_EXTENSION_FUNCTION_POINTER,
		&guidGetAddr,
		sizeof(guidGetAddr),
		&lpfnGetAcceptExSockaddrs,
		sizeof(lpfnGetAcceptExSockaddrs),
		&bytes,
		NULL,
		NULL
	);

上面的写法是固定的,不明白的可以查询关资料。

GetQueuedCompletionStatus
获取完成端口的状态,当有重叠任务完成时,在多个调用该函数的线程中挑选一个线程返回,并返回相应的结构用于Accept,Recv,Send等操作。
函数原型:

BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,      
LPDWORD lpNumberOfBytes,   
PULONG_PTR lpCompletionKey,
LPOVERLAPPED *lpOverlapped,
DWORD dwMilliseconds
);

参数:
CompletionPort:指定的IOCP,该值由CreateIoCompletionPort函数创建的完成端口句柄
lpnumberofbytes:一次完成后的I/O操作所传送数据的字节数。
lpcompletionkey:当文件I/O操作完成后,用于存放与之关联的CK,如socket 等
lpoverlapped:为调用IOCP机制所引用的OVERLAPPED结构。
dwmilliseconds:用于指定调用者等待CP的时间,如果设置为 INFINITE,表示会一直等待下去
返回值:
调用成功,则返回非零数值,相关数据存于lpNumberOfBytes、lpCompletionKey、lpCompletionKey变量中。失败则返回零值。

DWORD dwBytesTransfered = 0;
LPPER_IO_OPERATION_DATA pIO = nullptr;
SOCKET sock = INVALID_SOCKET;
/*
四个参数分别是:
创建的完成端口句柄 g_completionPort 
接收的字节数
完成重叠动作的关联套接字,socket
等待时间,单位毫秒
*/
bool bRet = GetQueuedCompletionStatus(g_completionPort,
			&dwBytesTransfered,
			(PULONG_PTR)&sock,
			(LPOVERLAPPED*)&pIO,
			INFINITE

自定义的重叠结构
在实际的操作中,为了方便我们使用,一般我们需要自定义一个重叠结构体,基本都是大同小异,一般定义如下:

#define  MSGSIZE 1024
//定义枚举类型
enum OPERATION_TYPE
{
	Io_Send,
	Io_Recv,
	Io_Accept
};
//定义具体重叠结构体
typedef struct
{
	WSAOVERLAPPED overlap;
	SOCKET socket;
	WSABUF buffer;
	char          szMessage[MSGSIZE];
	DWORD         NumberOfBytesRecvd;
	DWORD         Flags;
	OPERATION_TYPE Type;
} PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA;

用到的知识点基本上就这么多,下面是具体的服务端代码.
编译环境: vs2019

// completionPort.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <thread>
#include <WinSock2.h>
#include <mswsock.h>
#define  MSGSIZE 1024
using namespace std;
#pragma  comment(lib,"ws2_32.lib")
LPFN_ACCEPTEX        lpfnAcceptEx;
LPFN_GETACCEPTEXSOCKADDRS lpfnGetAcceptExSockaddrs;
HANDLE g_completionPort;
SOCKET listenSock;
int ntotal = 0;
enum OPERATION_TYPE
{
	Io_Send,
	Io_Recv,
	Io_Accept
};
typedef struct
{
	WSAOVERLAPPED overlap;
	SOCKET socket;
	WSABUF buffer;
	char          szMessage[MSGSIZE];
	DWORD         NumberOfBytesRecvd;
	DWORD         Flags;
	OPERATION_TYPE Type;
} PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA;

bool initSocket()
{
	WSADATA wsdata;

	WORD sVer = MAKEWORD(2, 2);

	int  ret = WSAStartup(sVer, &wsdata);
	if (ret != 0)
	{
		return false;
	}
	return true;
}

bool  PostAccept()
{
	DWORD dwbytes;
	LPPER_IO_OPERATION_DATA pIO = nullptr;
	pIO = (LPPER_IO_OPERATION_DATA)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PER_IO_OPERATION_DATA));

	pIO->buffer.len = MSGSIZE;
	pIO->buffer.buf = pIO->szMessage;
	SOCKET psock = socket(AF_INET, SOCK_STREAM, 0);
	if (psock==INVALID_SOCKET)
	{
		return false;
	}
	pIO->socket = psock;
	pIO->Type = Io_Accept;

	bool rc = lpfnAcceptEx(listenSock, psock, &pIO->buffer, 0,
		sizeof(sockaddr_in) + 16,
		sizeof(sockaddr_in) + 16,
		&dwbytes,
		&pIO->overlap);

	if (ERROR_IO_PENDING!=WSAGetLastError())
	{
		return false;
	}
	return true;
}
bool postSend(LPPER_IO_OPERATION_DATA& lap)
{
	ZeroMemory(&lap->overlap, sizeof(WSAOVERLAPPED));
	ZeroMemory(lap->szMessage, MSGSIZE);
	lap->buffer.buf = lap->szMessage;
	lap->Type = Io_Send;
	//继续投递重叠结构
	WSASend(lap->socket,
		&lap->buffer,
		1,
		&lap->NumberOfBytesRecvd,
		lap->Flags,
		&lap->overlap,
		NULL);
	if (ERROR_IO_PENDING != WSAGetLastError())
	{
		return false;
	}
	return true;

}

bool postRecv(LPPER_IO_OPERATION_DATA &lap)
{

	//清空重叠结构和缓冲区
	ZeroMemory(&lap->overlap, sizeof(WSAOVERLAPPED));
	ZeroMemory(lap->szMessage, MSGSIZE);
	//重叠结构体的长度设置不可省略,省略之后调试发现长度是0,无法接收数据
	lap->buffer.len = MSGSIZE;
	lap->buffer.buf = lap->szMessage;
	lap->Type = Io_Recv;
	//继续投递重叠结构
	WSARecv(lap->socket,
		&lap->buffer,
		1,
		&lap->NumberOfBytesRecvd,
		&lap->Flags,
		&lap->overlap,
		NULL);
	if (ERROR_IO_PENDING != WSAGetLastError())
	{
		return false;
	}
	return true;
}
void workThread()
{
	DWORD dwBytesTransfered = 0;
	LPPER_IO_OPERATION_DATA pIO = nullptr;
	SOCKET sock = INVALID_SOCKET;
	while (true)
	{

		bool bRet = GetQueuedCompletionStatus(g_completionPort,
			&dwBytesTransfered,
			(PULONG_PTR)&sock,
			(LPOVERLAPPED*)&pIO,
			INFINITE
		);
		if (dwBytesTransfered == 0 && ((pIO->Type == Io_Recv) || (pIO->Type == Io_Send)))
		{
			CancelIo((HANDLE)pIO->socket);
			closesocket(pIO->socket);
			HeapFree(GetProcessHeap(), 0, pIO);
		}
		else
		{
			if (pIO->Type == Io_Accept)
			{
				SOCKADDR_IN* addrClient = NULL, * addrLocal = NULL;
				int nClientLen = sizeof(SOCKADDR_IN), nLocalLen = sizeof(SOCKADDR_IN);
				//调用lpfnGetAcceptExSockaddrs 获取客户端连接信息
				lpfnGetAcceptExSockaddrs(&pIO->buffer, 0,
					sizeof(SOCKADDR_IN) + 16, sizeof(SOCKADDR_IN) + 16,
					(LPSOCKADDR*)&addrLocal, &nLocalLen,
					(LPSOCKADDR*)&addrClient, &nClientLen);
				//关键点:要把完成端口操作和socket绑定起来
				CreateIoCompletionPort((HANDLE)pIO->socket, g_completionPort, (DWORD)pIO->socket, 0);
				/*收到连接重叠结构,说明有客户端连接上来
				1>要投递一个接收数据重叠结构,来接收数据
				2>申请的等待连接的socket 已经被占用,再重新投递重叠结构,等待客户端的连接
				*/
				postRecv(pIO);
				PostAccept();

			}
			else if (pIO->Type == Io_Recv)
			{
				cout << pIO->szMessage << endl;
				postRecv(pIO);

			}
			else if (pIO->Type == Io_Send)
			{
				postSend(pIO);

			}
		}

	}

}
int main()
{

	lpfnAcceptEx = NULL;
	lpfnGetAcceptExSockaddrs = NULL;
	SYSTEM_INFO sys_info;

	g_completionPort = INVALID_HANDLE_VALUE;
	//初始化com
	if (!initSocket())
	{
		return -1;
	}
	sockaddr_in Sa;
	int len = sizeof(Sa);
	Sa.sin_port = htons(8888);
	Sa.sin_addr.S_un.S_addr = INADDR_ANY;
	Sa.sin_family = AF_INET;
	listenSock = socket(AF_INET, SOCK_STREAM, 0);

	if (listenSock == INVALID_SOCKET)
	{
		cout << "Create socket fail" << endl;
		return -1;
	}
	if (bind(listenSock, (sockaddr*)&Sa, len) == SOCKET_ERROR)
	{
		cout << "bind fail" << endl;
		return -1;
	}
	if (listen(listenSock, 5) == SOCKET_ERROR)
	{
		cout << "listen fail" << endl;
		return -1;
	}
	//创建完成端口
	g_completionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
	//绑定监听套接字和端口
	CreateIoCompletionPort((HANDLE)listenSock, g_completionPort, (DWORD)listenSock, 0);
	GetSystemInfo(&sys_info);
	//获取CPU核数
	int n = sys_info.dwNumberOfProcessors;
	GUID guidAcceptEx = WSAID_ACCEPTEX;
	GUID guidGetAddr = WSAID_GETACCEPTEXSOCKADDRS;
	//两个扩展函数,成功都是返回0 
	DWORD       bytes;

	//加载AccpetEx函数指针
	int rc = WSAIoctl(
		listenSock,
		SIO_GET_EXTENSION_FUNCTION_POINTER,
		&guidAcceptEx,
		sizeof(guidAcceptEx),
		&lpfnAcceptEx,
		sizeof(lpfnAcceptEx),
		&bytes,
		NULL,
		NULL
	);

	//加载GetAcceptExSockaddrs函数指针
	rc = WSAIoctl(
		listenSock,
		SIO_GET_EXTENSION_FUNCTION_POINTER,
		&guidGetAddr,
		sizeof(guidGetAddr),
		&lpfnGetAcceptExSockaddrs,
		sizeof(lpfnGetAcceptExSockaddrs),
		&bytes,
		NULL,
		NULL
	);
	//关键点 首先要先投递出一个接收客户端连接的重叠操作,等待客户端连接上来,这一点不可缺少
	PostAccept();

	thread t(workThread);
	t.detach();
// 	for (int i = 0; i < n * 2 + 2; i++)
// 	{
// 		thread t(workThread);
// 		t.detach();
// 	}
	while (true)
	{
		Sleep(1000);

	}
	return 0;
}

注意点:

  1. 在主线程里需要先投递一个接收soceket,即PostAccept();
  2. 在postRecv()中要重新设定重叠结构的长度
 lap->buffer.len = MSGSIZE;
 lap->buffer.buf = lap->szMessage;
  1. 接收到完成动作之后,如果type=Io_Accept,需要关联新socket和完成端口动作,并且投递新的接收接收重叠结构和新的套接:
//关键点:要把完成端口操作和socket绑定起来
CreateIoCompletionPort((HANDLE)pIO->socket, g_completionPort, (DWORD)pIO->socket, 0);
/*收到连接重叠结构,说明有客户端连接上来
	1>要投递一个接收数据重叠结构,来接收数据
	2>申请的等待连接的socket 已经被占用,再重新投递重叠结构,等待客户端的连接*/
	postRecv(pIO);
	postAccept();

到此为止,socket 五种模式都已经介绍完了,由于能力有限,一些Demo中代码可能有所疏漏,欢迎给位指正。这几篇文章主要是自己记录一下自己所学知识,当然如果能够给需要的人提供微弱的帮助,我之所愿矣.

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值