UDP.07.完成端口模型

Posted oldmao_2001

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UDP.07.完成端口模型相关的知识,希望对你有一定的参考价值。

https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-socket
基于UDP的网络编程有5种模型:
SELECT模型
事件选择模型
异步选择模型
重叠IO模型
完成端口模型

这次讲最后一种:完成端口模型。
基本理论知识:核与线程的关系、线程数量的优化等看下TCP篇即可,这里不赘述

完成端口模型逻辑

1.将重叠套接字(UDP只有一个服务器SOCKET)与一个完成端口(完成端口是某一个类型的变量)绑定在一起;
2.使用WSARecvFrom、WSASendTo投递请求(和重叠IO模型一样的代码,但是立即完成部分可以不要,因为完成端口会处理请求);
3.当系统异步完成请求,就会把通知存进一个队列,我们就叫它通知队列,该队列由操作系统系统创建,维护;
4.完成端口可以理解为这个队列的头,可通过GetQueuedCompletionStatus从队列头往外取请求,一个一个处理。

完成端口模型代码

这里也是分步骤将代码按步骤分解。

1-5通用部分

1.加载网络头文件网络库
2.打开网络库
3.校验版本
4.创建SOCKET
5.绑定地址与端口

	WORD wVersionRequested = MAKEWORD(2, 2);//版本
	WSADATA wsaDATA;

	//2、打开网络库
	int iret = WSAStartup(wVersionRequested, &wsaDATA);
	if (iret != 0)
	{
		//有错
		switch (iret)
		{
		case WSASYSNOTREADY:
			printf("解决方案:重启。。。");
			break;
		case WSAVERNOTSUPPORTED:
			printf("解决方案:更新网络库");
			break;
		case WSAEINPROGRESS:
			printf("解决方案:重启。。。");
			break;
		case WSAEPROCLIM:
			printf("解决方案:网络连接达到上限或阻塞,关闭不必要软件");
			break;
		case WSAEFAULT:
			printf("解决方案:程序有误");
			break;
		}
		getchar();
		return 0;
	}
	printf("WSAStartup成功\\n");

	//3、校验版本,只要有一个不是2,说明系统不支持我们要的2.2版本	
	if (2 != HIBYTE(wsaDATA.wVersion) || 2 != LOBYTE(wsaDATA.wVersion))
	{
		printf("版本有问题!");
		WSACleanup();//关闭网络库
		return 0;
	}
	printf("校验版本成功\\n");
	// 4、创建SOCKET
	socketServer = WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, NULL, 0, WSA_FLAG_OVERLAPPED);
	if (INVALID_SOCKET == socketServer)
	{
		//清理网络库,不关闭句柄
		WSACleanup();
		return 0;
	}
	printf("创建SOCKET成功\\n");

	//5、绑定地址与端口
	struct sockaddr_in lsi;
	lsi.sin_family = AF_INET;//这里要和创建SOCKET句柄的参数1类型一样
	lsi.sin_port = htons(9527);//用htons宏将整型转为端口号的无符号整型
	lsi.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

	if (bind(socketServer, (struct sockaddr*)&lsi, sizeof(lsi)) == SOCKET_ERROR)
	{
		int err = WSAGetLastError();//取错误码
		printf("服务器bind失败错误码为:%d\\n", err);
		closesocket(socketServer);//释放
		WSACleanup();//清理网络库
	}
	printf("绑定SOCKET成功\\n");

6.创建/绑定完成端口

完成这两个功能只用一个函数,但是传的参数不一样。
https://docs.microsoft.com/en-us/windows/win32/fileio/createiocompletionport

HANDLE WINAPI CreateIoCompletionPort(
  _In_     HANDLE    FileHandle,
  _In_opt_ HANDLE    ExistingCompletionPort,
  _In_     ULONG_PTR CompletionKey,
  _In_     DWORD     NumberOfConcurrentThreads
);
创建/绑定完成端口绑定完成端口
参数1INVALID_HANDLE_VALUE服务器SOCKET句柄,前面要加(HANDLE)进行类型转换
参数2NULL完成端口变量
参数30再次传递服务器SOCKET句柄,也可以传递一个下标(用句柄数组下标)做编号,便于客户端绑定指定完成端口(后面要用这个编号)但是对于UDP来说,没有必要用这个参数,因为只涉及到一个SOCKET句柄
参数4允许此端口上最多同时运行的线程数量,可以用GetSystemInfo获取CPU核数,也可以用0表示默认CPU的核数0
返回值成功:返回一个新的完成端口;失败:用GetLastError()获取错误码成功:返回一个与服务器SOCKET句柄绑定后的完成端口变量,实际上也就是原来的完成端口;失败:用GetLastError()获取错误码

6.1 创建完成端口
先定义完成端口句柄/内核对象的全局变量,因为要在点×关闭事件关闭该句柄

HANDLE hPort//全局完成端口句柄
//6.1创建完成端口
	hPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

	if (hPort == 0)//出错
	{
		int err = WSAGetLastError();//取错误码
		printf("CreateIoCompletionPort失败错误码为:%d\\n", err);
		closesocket(socketServer);//释放
		WSACleanup();//清理网络库
	}

CreateIoCompletionPort最后一个参数用0表示用CPU的核数来创建线程,也可以用函数来获取CPU核数。

6.2 绑定完成端口
将完成端口与服务器SOCKET句柄绑定

//6.2绑定完成端口	
	hPort = CreateIoCompletionPort((HANDLE)socketServer, hPort, socketServer, 0);

	if (hPort == 0)//出错
	{
		int err = WSAGetLastError();//取错误码
		printf("6.2绑定完成端口失败错误码为:%d\\n", err);
		CloseHandle(hPort);
		closesocket(socketServer);//释放
		WSACleanup();//清理网络库
		return 0;
	}

7.创建线程

这里要用到CPU核数。
https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getsysteminfo
信息是放在SYSTEM_INFO结构里面。

void GetSystemInfo(
  LPSYSTEM_INFO lpSystemInfo
);

具体代码:

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

创建线程函数介绍看:https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createthread
例子看:https://docs.microsoft.com/en-us/windows/win32/procthread/creating-threads

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  SIZE_T                  dwStackSize,
  LPTHREAD_START_ROUTINE  lpStartAddress,
  __drv_aliasesMem LPVOID lpParameter,
  DWORD                   dwCreationFlags,
  LPDWORD                 lpThreadId
);

具体参数看TCP部分,这里不复述。

//7.创建线程
	//获取CPU核数
	SYSTEM_INFO si;
	GetSystemInfo(&si);
	int nProcessorsCount = si.dwNumberOfProcessors;

	//按核数创建线程
	for (int i = 0; i < nProcessorsCount; i++)
	{
		DWORD dwThreadId;
		if (NULL == CreateThread(NULL, 0, ThreadProc, hPort, 0, &dwThreadId))
		{
			//创建线程失败
			int err = WSAGetLastError();//取错误码
			printf("7创建线程失败失败错误码为:%d\\n", err);
			i--;//创建失败就不算,重新来过
		}
	}

8.阻塞主线程

在写线程函数之前要在主函数里面将主线程设置为阻塞,不然主函数不断的运行到后面就return了。
在创建好线程后,投递f1,然后加入以下代码:

//设置主线程进入阻塞,子线程进入工作状态	
	while(1)
	{
		//Sleep是线程挂起状态,不占用CPU时间片
		Sleep(1000);
	}

9.线程绑定函数/操作通知队列

/// <summary>
/// 9.线程绑定函数/操作通知队列,相当于回调函数
/// </summary>
/// <param name="lpParam">创建线程CreateThread函数中参数4传进来的参数</param>
/// <returns></returns>
DWORD __stdcall ThreadProc(LPVOID lpParam)
{
	while (1)
	{
		DWORD  NumberOfBytesTransferred;//接收或发送数据的长度
		ULONG_PTR CompletionKey;//创建完成端口CreateIoCompletionPort的参数3
		WSAOVERLAPPED* lpOverlapped;
		//9.1取队头信号
		if (FALSE == GetQueuedCompletionStatus(hPort, &NumberOfBytesTransferred, &CompletionKey, &lpOverlapped, INFINITE))
		{
			continue;//取队头信号失败,跳过,下次再取
		}

		//9.2信号置空
		WSAResetEvent(lpOverlapped->hEvent);

		//9.3成功取到信号,分类处理
		if (0 == recvBuff[0])
		{
			printf("成功取到信号,缓冲区无数据,SEND可执行\\n");
		}
		else
		{
			printf("成功取到信号,缓冲区有数据,RECV可执行\\n");
			printf("%s\\n", recvBuff);
			PostSendTo(&gsi);//f2.根据需求对客户端套接字投递WSASend
			//memset(recvBuff,0,548);//mark法1:清空接收缓存,逐位设置为0
			recvBuff[0] = 0;//mark法2:为第一位设置特殊值
			PostRecvFrom();//f1.投递PostRecvFrom
		}
	}
	return 0;
}

其他

f1.投递PostRecvFrom
f2.根据需求对客户端套接字投递WSASend
这两个函数可以把立即执行去掉,因为在线程函数中会有相应的处理。

以上是关于UDP.07.完成端口模型的主要内容,如果未能解决你的问题,请参考以下文章

WINSOCK.07.完成端口模型

菜鸟 关于WSASend 函数,IO完成端口模型

完成端口线程的 OutOfMemoryException

使用片段时 Intellij 无法正确识别 Thymeleaf 模型变量

windows网络模型之完成端口(CompletionPort)详解 (转)

C# IOCP完成端口模型(简单实用高效)