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
);
创建/绑定完成端口 | 绑定完成端口 | |
---|---|---|
参数1 | INVALID_HANDLE_VALUE | 服务器SOCKET句柄,前面要加(HANDLE)进行类型转换 |
参数2 | NULL | 完成端口变量 |
参数3 | 0 | 再次传递服务器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.完成端口模型的主要内容,如果未能解决你的问题,请参考以下文章
使用片段时 Intellij 无法正确识别 Thymeleaf 模型变量