代码下载地址
3.4 创建服务线程
在创建了完成端口之后,接下来就要创建“1 完成端口简介”中提到的线程池中的服务线程。
SYSTEM_INFO SystemInfo;
GetSystemInfo(&SystemInfo);
for (int i = 0; i < SystemInfo.dwNumberOfProcessors * 2; i++)
{
HANDLE hThreadHandle;
DWORD dwThreadID;
if ((hThreadHandle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ServerWorkerThread, NULL, 0, &dwThreadID)) == NULL)
{
printf("CreateThread错误%d\n", GetLastError());
return -1;
}
CloseHandle(hThreadHandle);
}
线程池中线程的数量要大于电脑CPU的数量,一般将线程的数量设置为CPU数量的两倍。
相关链接
1同时运行的服务线程数量:同时运行的服务线程的数量必须与电脑CPU的数量保持一致,这样每个CPU都对应一个线程,减少了线程切换的时间;
2线程池中服务线程的数量:一定大于同时运行的服务线程数量。这样是为了保证CPU尽可能的忙碌。例如,在一台单CPU的计算机上,创建一个完成端口应用程序,为其指定并发线程数目为1。在应用程序中,创建2个线程在完成端口上等待。假如在一次为客户端服务时,被唤醒的线程因调用Sleep()之类的函数而处于阻塞状态,此时另外一个I/O完成包被发送到完成端口上。完成端口会唤醒另外一个线程为该客户端提供服务。这就是线程池中线程数量要大于完成端口指定的并发线程数量的原因。
3.5 服务线程函数
3.5.1 定义
在“3.4 创建服务线程”中提到,使用CreateThread()函数创建服务线程。而CreateThread()函数要求线程函数的格式为
DWORD WINAPI ThreadProc(LPVOID LpParameter);
因此,服务线程函数的定义为
DWORD WINAPI ServerWorkerThread(LPVOID pParam);
3.5.2 实现
在服务线程函数ServerWorkerThread()中,通过“2 相关API函数”中提到的GetQueuedCompletionStatus()函数等待完成端口消息队列中完成消息。一旦消息队列中有完成消息,则GetQueuedCompletionStatus()函数会获取完成重叠I/O操作的套接字以及重叠I/O操作的类型。最后,根据重叠I/O操作的类型,调用套接字的相关函数进行后续处理。
DWORD WINAPI ServerWorkerThread(LPVOID pParam)
{
DWORD dwIoSize;
CClient *pClient;
LPOVERLAPPED lpOverlapped;
while (TRUE)
{
dwIoSize = -1;
lpOverlapped = NULL;
pClient = NULL;
BOOL bIoRet = GetQueuedCompletionStatus(
hComPort
, &dwIoSize
, (LPDWORD)&pClient
, &lpOverlapped
, THREAD_SLEEP_TIME
);
if (!bIoRet)
{
//没有完成消息
}
else
{
PIO_OPERATION_DATA pIO = CONTAINING_RECORD(lpOverlapped, IO_OPERATION_DATA, overlapped);
if (pIO->type == READ)
{
//处理接收到的数据
}
else
{
if (pIO->type == WRITE)
{
//发送数据之后的处理
}
}
}
}
return 0;
}
在以上代码中,每个服务线程都通过while循环等待完成端口上的完成消息。GetQueuedCompletionStatus()函数的第三个参数即为完成键,通过这个完成键,服务线程就可以获取完成重叠I/O操作的套接字,通过该套接字实现对该套接字的后续操作,CClient类在“3.7 CClient类”中详细介绍。GetQueuedCompletionStatus()函数的第四个参数lpOverlapped保存了在发出重叠I/O请求时指定的重叠操作OVERLAPPED结构的指针,该指针在“3.7 CClient类”中提到的CClient类的Recv()函数和Send()函数中指定。通过宏CONTAINING_RECORD,服务线程可以获取在套接字上操作的类型。
CONTAINING_RECORD宏根据结构的类型和结构中的一个成员变量的地址,返回该结构实例的基地址。该宏的格式为
PCHAR CONTAINING_RECORD(PCHAR Address, TYPE Type, PCHAR Field);
其中,参数Address为结构中的一个成员变量的地址;Type是结构的类型;Field是在该结构中,Address的名称。该宏的返回值即为该结构实例的基地址。
在以上代码中,
PIO_OPERATION_DATA pIO = CONTAINING_RECORD(lpOverlapped, IO_OPERATION_DATA, overlapped);
的作用是根据GetQueuedCompletionStatus()函数获取到的重叠结构的指针lpOverlapped,获取在进行重叠I/O操作时使用的IO_OPERATION_DATA结构的指针,即在“3.7 CClient类”中提到的CClient类的Recv()函数和Send()函数中使用的CClient类的成员变量m_IO。接下来,通过m_IO得到重叠I/O的类型,最后,根据不同的操作进行不同的后续处理。
3.6 接收客户端的连接
使用WSAAccept()函数接受来自客户端的连接。正常情况下,在主程序中创建接收客户端的连接的子线程,之后使用WSAEventSelect模型的事件通知方式来接受客户端的连接。但是从简化流程的目的出发,我们简单使用while语句循环等待客户端的连接。
while (true)
{
SOCKADDR_IN servAddr;
int serAddrLen = sizeof(servAddr);
if ((sAccept = WSAAccept(sListen, (sockaddr*)&servAddr, &serAddrLen, NULL, 0)) == SOCKET_ERROR)
{
printf("WSAAccept()执行错误%d\n", WSAGetLastError());
break;
}
......
}
以上代码中,WSAAccept()函数的第一个参数sListen即为在“3.2.2 创建套接字”中创建的套接字;第二个参数servAddr保存了连入的客户端的IP地址等信息。WSAAccept()函数的返回值sAccept为新创建的套接字,服务端通过sAccept与客户端进行通信。在完成端口与套接字关联时,在“3.3 创建完成端口”中创建的完成端口hComPort 就是要与sAccept套接字关联。