分析
我们的重叠IO模型还有什么问题吗?
去掉我编写时候的逻辑bug,就网络模型来说主要问题还有:
- 循环询问,延迟高,做了很多无用功
- 采用多线程,数量可能太多,我们很难管理,切换线程消耗大量cpu资源/时间
那最优线程数量是多少?
网上的几种答案:
- CPU核数
- CPU核数*2
- CPU核数*2+2
其中CPU核数*2+2 这个数量,是根据实际应用中的经验得来。线程函数中可能会调用Sleep(),WSAWaitForMultipleEvents()…这类函数,会使线程挂起(不占cpu时间片),从而使得CPU某个核空闲了,所以一般我们多建个两三根,以解决此类情况,让CPU不停歇,从而在整体上保证程序执行效率。
完成端口也是windows的一种机制,是在重叠IO模型基础上的优化:
- 模仿消息队列,创造一个通知队列,系统创建(保证有序,不做无用功)
- 创建最佳数量的线程(充分利用cpu的性能)
服务器
创建完成端口
HANDLE WINAPI CreateIoCompletionPort(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE ExistingCompletionPort,
_In_ ULONG_PTR CompletionKey,
_In_ DWORD NumberOfConcurrentThreads
);
功能:创建一个完成端口
参数:
- FileHandle — INVALID_HANDLE_VALUE
- ExistingCompletionPort — NULL
- CompletionKey — 0
- NumberOfConcurrentThreads — 允许此端口上最多同时运行的线程数量(填写0表示默认是cpu核数)
返回值:
- 如果成功,返回一个新的完成端口。
- 如果失败,返回NULL。
代码:
// 创建完成端口
hPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
if (NULL == hPort)
{
printf("CreateIoCompletionPort 创建失败 error:%d\n", WSAGetLastError());
closesocket(socketServer);
WSACleanup();
return -1;
}
绑定端口与SOCKET
HANDLE WINAPI CreateIoCompletionPort(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE ExistingCompletionPort,
_In_ ULONG_PTR CompletionKey,
_In_ DWORD NumberOfConcurrentThreads
);
功能:将完成端口与SOCKET绑定在一起
参数:
- FileHandle — 服务器socket
- ExistingCompletionPort — 完成端口变量
- CompletionKey — 用户定义的完成密钥
- NumberOfConcurrentThreads — 忽略此参数填0
返回值:
- 如果成功,返回与socket绑定后的端口。
- 如果失败,返回NULL。
代码:
// 绑定端口与SOCKET
HANDLE hPort1 = CreateIoCompletionPort((HANDLE)socketServer, hPort, 0, 0);
if (hPort1 != hPort)
{
printf("CreateIoCompletionPort 绑定失败 error:%d\n", GetLastError());
CloseHandle(hPort);
closesocket(socketServer);
WSACleanup();
return -1;
}
创建线程
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
__drv_aliasesMem LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
功能:创建一个线程
参数:
- lpThreadAttributes — 确定线程句柄是否能被继承(默认NULL不继承)
- dwStackSize — 线程栈大小,以字节为单位。(填0,系统使用默认大小1M)
- lpStartAddress — 线程函数地址
- lpParameter — 外部给线程传递的数据
- dwCreationFlags — 控制线程创建的标志
值 含义 0 线程在创建后立即运行 CREATE_SUSPENDED 该线程以挂起状态创建,并且直到调用ResumeThread函数后才运行 。 STACK_SIZE_PARAM_IS_A_RESERVATION 所述dwStackSize参数指定堆栈的初始保留大小。如果未指定此标志,则dwStackSize指定提交大小。 - lpThreadId — 指向接收线程标识符的变量的指针。如果此参数为 NULL,则不返回线程标识符。
返回值:
- 如果成功,返回新线程的句柄。
- 如果失败,返回NULL。
代码:
// 获取要创建的线程数量
SYSTEM_INFO systemProcessorsCount;
GetSystemInfo(&systemProcessorsCount);
nProcessorsCount = systemProcessorsCount.dwNumberOfProcessors;
// 创建线程
pThread = (HANDLE*)malloc(sizeof(HANDLE)*nProcessorsCount);
for (int i = 0; i < nProcessorsCount; ++i)
{
pThread[i] = CreateThread(NULL, 0, ThreadProc, hPort, 0, NULL);
if (NULL == pThread[i])
{
printf("CreateThread 失败 error:%d\n", GetLastError());
CloseHandle(hPort);
closesocket(socketServer);
WSACleanup();
return -1;
}
}
线程内部代码
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,
LPDWORD lpNumberOfBytesTransferred,
PULONG_PTR lpCompletionKey,
LPOVERLAPPED *lpOverlapped,
DWORD dwMilliseconds
);
功能:尝试从指定的I / O完成端口出列I / O完成数据包(无通知时,线程挂起,不占用CPU的时间)
参数:
- CompletionPort — 完成端口
- lpNumberOfBytesTransferred — 接收或者发送得字节数
- lpCompletionKey — 接收与I / O操作已完成的文件句柄关联的完成键值
- lpOverlapped — 重叠结构
- dwMilliseconds — 等待时间,INFINITE为一直等
返回值:
- 如果成功,返回TRUE。
- 如果失败,返回FALSE。
代码:
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
HANDLE port = (HANDLE)lpParameter;
DWORD NumberOfBytes;
ULONG_PTR index;
LPOVERLAPPED lpOverlapped;
while (g_flag)
{
BOOL bFlag = GetQueuedCompletionStatus(port, &NumberOfBytes, &index, &lpOverlapped, INFINITE);
if (FALSE == bFlag)
{
if (64 == GetLastError())
{
printf("客户端异常下线\n");
}
continue;
}
// 分类处理
if (0 == index) // accept
{
printf("accept\n");
// 绑定到完成端口
HANDLE hPort1 = CreateIoCompletionPort((HANDLE)g_allSock[g_count], hPort, g_count, 0);
if (hPort1 != hPort)
{
printf("CreateIoCompletionPort 绑定失败 error:%d\n", GetLastError());
closesocket(g_allSock[g_count]);
continue;
}
PostRecv(g_count); // 投递recv
++g_count; // 客户端数量++
PostAccept(); // 投递accept
}
else
{
if (0 == NumberOfBytes)
{
printf("客户端正常下线\n");
closesocket(g_allSock[index]);
WSACloseEvent(g_allOlp[index].hEvent);
// 从数组中删掉
g_allSock[index] = 0;
g_allOlp[index].hEvent = NULL;
}
else
{
if (0 != g_strRecv[index][0]) // recv
{
// 接收到客户端消息
printf("Client Data : %s \n", g_strRecv[index]);
memset(g_strRecv[index], 0, sizeof(g_strRecv[index]));
PostSend(index); // 给客户回信
PostRecv(index); // 对自己投递接收
}
}
}
}
return 0;
}
投递accept、send、recv
代码:
int PostAccept()
{
g_allSock[g_count] = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
g_allOlp[g_count].hEvent = WSACreateEvent();
char str[1024] = { 0 };
DWORD dwRecvcount;
AcceptEx(g_allSock[0], g_allSock[g_count], str, 0, sizeof(struct sockaddr_in) + 16,
sizeof(struct sockaddr_in) + 16, &dwRecvcount, &g_allOlp[0]);
if (ERROR_IO_PENDING != WSAGetLastError())
{
return 1; // 函数执行出错
}
return 0;
}
int PostRecv(int index)
{
WSABUF wsabuf;
wsabuf.buf = g_strRecv[index];
wsabuf.len = MAX_RECV_COUNT;
DWORD dwRecvCount;
DWORD dwFlag = 0;
WSARecv(g_allSock[index], &wsabuf, 1, &dwRecvCount, &dwFlag, &g_allOlp[index], NULL);
if (ERROR_IO_PENDING != WSAGetLastError())
{
return 1; // 函数执行出错
}
return 0;
}
int PostSend(int index)
{
WSABUF wsabuf;
wsabuf.buf = "ok";
wsabuf.len = MAX_RECV_COUNT;
DWORD dwSendCount;
DWORD dwFlag = 0;
WSASend(g_allSock[index], &wsabuf, 1, &dwSendCount, dwFlag, &g_allOlp[index], NULL);
if (ERROR_IO_PENDING != WSAGetLastError())
{
return 1; // 函数执行出错
}
return 0;
}
运行结果
模型流程图
也是解决了send、recv、accept的执行阻塞问题,是重叠io的优化。
源码链接
百度云链接:https://pan.baidu.com/s/1xBOiSADlAG2gO1TC6BBO_A
提取码:sxbd