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;
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");
if (2 != HIBYTE(wsaDATA.wVersion) || 2 != LOBYTE(wsaDATA.wVersion))
{
printf("版本有问题!");
WSACleanup();
return 0;
}
printf("校验版本成功\n");
socketServer = WSASocket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, NULL, 0, WSA_FLAG_OVERLAPPED);
if (INVALID_SOCKET == socketServer)
{
WSACleanup();
return 0;
}
printf("创建SOCKET成功\n");
struct sockaddr_in lsi;
lsi.sin_family = AF_INET;
lsi.sin_port = htons(9527);
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
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句柄绑定
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
);
具体代码:
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部分,这里不复述。
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(1000);
}
9.线程绑定函数/操作通知队列
DWORD __stdcall ThreadProc(LPVOID lpParam)
{
while (1)
{
DWORD NumberOfBytesTransferred;
ULONG_PTR CompletionKey;
WSAOVERLAPPED* lpOverlapped;
if (FALSE == GetQueuedCompletionStatus(hPort, &NumberOfBytesTransferred, &CompletionKey, &lpOverlapped, INFINITE))
{
continue;
}
WSAResetEvent(lpOverlapped->hEvent);
if (0 == recvBuff[0])
{
printf("成功取到信号,缓冲区无数据,SEND可执行\n");
}
else
{
printf("成功取到信号,缓冲区有数据,RECV可执行\n");
printf("%s\n", recvBuff);
PostSendTo(&gsi);
recvBuff[0] = 0;
PostRecvFrom();
}
}
return 0;
}
其他
f1.投递PostRecvFrom f2.根据需求对客户端套接字投递WSASend 这两个函数可以把立即执行去掉,因为在线程函数中会有相应的处理。
|