在使用socket网络编程,实现一个服务器时,初学者最容易想到方法是当服务器监听的socket接受(accept)到一个客户端时创建一个线程,然后在线程中调用recv函数实时监控客户端是否有数据发送过来。这种方法是可以实现服务器对客户端数据的监听,但是这种方法效率很低,无法支持大量客户端同时连接。这种方法需要对每个连接的客户端创建一个线程,如果有一千可客户端就要创建一千个线程,线程多了对于服务器会产生极大的压力。首先线程本身也是需要使用资源的,每个线程有内核对象,线程栈,创建和销毁线程也都比较费时。其次CPU中线程的切换也是比较费时的,当CPU唤醒一个睡眠的线程是需要切换线程上下文,即把睡眠线程的运行数据(如:寄存器数据)载入到CPU中,如果线程过多CPU花费在切换线程上的时间会比执行服务器程序代码时间还多。
线程过多会占用太多的系统资源,频繁的切换线程消耗过多的CPU, IO完成端口(IOCP)和线程池就是用来解决上面提到的两个问题。要理解IOCP需要先了解异步IO读取,这里从异步IO开始讲起。
使用socket的recv/send函数发送接收数据都是同步的,即调用时需要读取到数据,或者发送完成才会返回。异步IO则是当调用recv/send函数是立即返回,系统会在后台处理发送接收数据,当后台完成数据的时候再通知调用者。这种机制和DMA(Direct Memory Access,直接内存存取)类似,当发送完读取命令后则不需要CUP继续处理,等数据到达后再通知CPU处理。因为文件的读取和socket的recv类似,实际上socket也就相当于文件句柄,这里举一个文件异步IO的例子,文件异步读取的步骤如下:
以下是完整的异步读取文件例子代码,代码下载地址。
#include
#include
#include
using namespace std;
const int BufferSize = 256;
struct IORequest : public OVERLAPPED
{
IORequest()
{
Internal = InternalHigh = 0;
Offset = OffsetHigh = 0;
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);//创建读取事件对象,当读取完成后事件会变成触发状态
}
~IORequest()
{
if (NULL != hEvent)
{
CloseHandle(hEvent);
}
}
};
int main()
{
HANDLE hFile = NULL;
//使用FILE_FLAG_OVERLAPPED创建异步文件句柄
hFile = CreateFile(TEXT("Text.txt"), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
if (INVALID_HANDLE_VALUE == hFile)
{
cout << "Invaild File:"<<"Text.txt" << endl;
return 1;
}
LARGE_INTEGER liFileSize = { 0 };
GetFileSizeEx(hFile, &liFileSize);
long lNextReadOffset = 0, lNeedProcess = liFileSize.LowPart;
string text;
text.resize(liFileSize.LowPart);//分配好缓存
char *buffer = (char*)text.data();
IORequest req[4]; //创建四个请求同时在后台读取
HANDLE hs[4];
for (int i = 0; i< 4; i++)
{
hs[i] = req[i].hEvent;
ReadFile(hFile, buffer, 0, NULL, &req[i]);//发送空请求,请求可以立即完成,从而进入wile循环中的WaitForMultipleObjects
}
//处理已完成IO请求,以下代码可以创建单独的线程处理,这里为了方便没有创建新线程处理
DWORD WaitResult = 0;
while (lNeedProcess > 0)
{
WaitResult = WaitForMultipleObjects(4, hs, FALSE, INFINITE);//等待4个读取请求中任意一个完成
if (WaitResult >= WAIT_OBJECT_0 && WaitResult <= WAIT_OBJECT_0+4)//数据已经在后台读取完成,前台处理读取的数据
{
int i = WaitResult - WAIT_OBJECT_0;
DWORD dwNumBytes = 0;
BOOL Result = GetOverlappedResult(hFile, &req[i], &dwNumBytes, FALSE);//无需等待因为数据已经完成读取
if (Result == TRUE && dwNumBytes > 0)//读取成功且读取到数据
{
lNeedProcess -= BufferSize;
}
if (lNeedProcess > 0)
{
//读取请求完成,重新发送一个读取请求
ResetEvent(req[i].hEvent);
req[i].Offset = lNextReadOffset;
ReadFile(hFile, buffer+ lNextReadOffset, BufferSize, NULL, &req[i]);//发送异步读取请求
lNextReadOffset += BufferSize;//增加读取偏移量
}
}
}
CloseHandle(hFile);
cout << text << endl<
如果想要更深入了解异步文件读取可以参考书籍《Windows核心编程(第5版》——第10章 同步设备I/O与异步设备I/O。
在使用同步IO时我们需要一个单独线程调用recv等待读取数据,这把整个socket的读取当成了一个大的任务,使用单独的一条线程执行, 由于recv会阻塞线程,这个线程也就同时只能执行一个socket的读取任务。异步IO可以把socket的IO任务分成很多个小任务,在线程中可以使用WaitForMultipleObjects同时等待多个IO请求的完成,这也就可以实现了一个线程同时监测多个客户端socket的recv。
WaitForMultipleObjects可等待的最大事件数为MAXIMUM_WAIT_OBJECTS,这个宏的值为64,也就是说使用WaitForMultipleObjects最多可同时等待64个IO请求的完成。如果使用WaitForMultipleObjects去监测socket异步IO,一条线程监测64个socket客户端,而且这里还会有另一个限制,就是一个异步IO请求发出后,只能有一条线程去等待它的完成,不能多条线程去同时等待一个请求完成。
为了更好的等待异步IO请求的完成,Windows引入了IO完成端口(Input/Output Completion Port,IOCP)。我们可以把一个异步文件句柄与一个IOCP关联,当发出异步IO请求后,我们只需要使用函数GetQueuedCompletionStatus监测IOCP就可以监测IO请求是否完成。使用IOCP监测IO请求时,不需要事件内核对象,这更节省资源。一个IOCP可以同时与多个异步文件句柄关联,可以同时监测IO请求的数量也不受限制,而且可多个线程同时监测一个IOCP,当一个请求完成时,只会分配到其中一个线程处理。
如果电脑只有一个单核CPU多线程并没有意义,反而会因为线程切换让费CPU的运算。但现在的电脑,特别是服务器,不可能只有一个单核CPU,现在电脑的CPU都是多核设计,对于一个四核四线程的CPU来说可以同时四条线程运行,如果只用一条线程则会浪费CPU运算。
对于IOCP,我们可以同时用多条线程监测完成的IO请求,并行处理IO请求,如果是一个四核CPU四条线程同时在处理完成的IO请求最好,四条线程并行运行没有线程的切换。但是有时线程处理IO请求时可能会因为等待互斥量等情况而暂停,这是会有一个CPU空闲,所以只创建四条线程处理是不够的。依据书上所说的经验,大多数情况下处理IOCP创建的线程数量应该为CPU核数的两倍。
我们可以通过函数GetSystemInfo获取系统中CPU核数。
减少线程切换
IOCP会记录所有调用GetQueuedCompletionStatus阻塞的线程,当有IO请求完成时,IOCP会将请求分配给最后调用GetQueuedCompletionStatus阻塞的线程。如果同时有多个IO请求完成,IOCP会把请求分配给多个线程处理,分配的线程同样是最后几个调用GetQueuedCompletionStatus阻塞的线程。以下是一个IOCP调度线程的例子:
|
以上过程中的第7步,第二个IO请求完成时,依然使用的时B线程处理,因为上一次IO请求也是B处理,这样就无需切换线程。如果IO请求完成的速度比较慢,每次都在B处理完后到达,那所有的IO请求都会有B线程完成,只有当B线程“处理不过来”时才会调用A线程处理。
在使用函数CreateIoCompletionPort创建IOCP时,最后一个参数NumberOfConcurrentThreads可以控制同时最多有多少线程处理已完成的IO请求,如果传入0,则使用CPU核数。控制的最大同时运行的线程数量,防止过度的线程同时运行,从而减少线程的切换。
IOCP通过将请求分配给最后调用GetQueuedCompletionStatus阻塞的线程,以及控制同时处理请求的最大数量来减少线程的切换。
以下是IOCP调用线程测试的例子,完整代码下载地址:
例子分析,这是一个使用IOCP的socket客户端,该客户端需要配合socket服务器使用,socket服务器代码下载地址。运行客户端前,需要先打开服务器,否则客户端连接不到服务器会自动退出,在测试时我是从服务向客户发送数据。在客户端处理IO请求函数ProcessIOThread中我故意增加了一个Sleep(1000)模拟长时间处理IO请求。代码中有大量注释,具体原理请看代码。
通过对IOCP的分析,IOCP已经可以避免服务器创建过多的线程以及过多的线程调度。前面说过一般处理IOCP的IO请求需要创建处理一般创建线程数量为CPU数的两倍,但在很多情况下这并不是效率最高的,毕竟CPU不光要处理IO请求,而且如果同时有大量处理IO请求的对象阻塞(如等待互斥量),可能会出现没有线程处理IO请求。为了更好的处理线程调度,使得CPU运行效率更高,就需要使用线程池。
线程池会同时创建多个线程在后台运行,用来处理各种各样的任务,当处理完任务后又会回到线程池中等待下一个任务的到来。线程池内部的线程全部由线程池自己管理,无需我们创建销毁,线程池内部会对线程进行调度以保证CPU以最高的效率运行,高效的处理各种任务。有了线程池,我们只需要将任务发送给线程池,线程池会自动分配线程完成相应的任务。
有了线程池,我们可以把一个大的任务分成几个小任务发送给线程池,让线程池执行,这样执行速度更快。Windows本身已经提供了线程池函数,我们只需要直接调就行,Windows自带的线程池已经足够高效,可以满足我们大多数的情况。我们可以向Windows线程池中发送以下四种任务:
当我们第一次向线程池中发送任务时,系统会自动创建一个线程,具体使用可以参考书籍《Windows核心编程(第5版》——第10章 同步设备I/O与异步设备I/O。
异步的socket可以把读取和写入分成了各个小的任务,这些任务可以发送给线程池处理。使用步骤如下:
|
注意每次调用异步读取写入前需要调用一次StartThreadpoolIo。
CreateThreadpoolIo内部会自动将sockct与系统创建的一个IOCP进行关联,无需我们自己创建IOCP。
对于服务器监听accept客户端的socket,也可以使用AcceptEx将accept变为异步,这使得服务器更加统一,而且AcceptEx的速度比accept更快。
AcceptEx是windows对标准socket的一个扩展函数,位于头文件Mswsock.h中
以下线程池管理Socket服务器完整例子,代码下载地址。
#include
#include //使用异步的AcceptEx
#include
#include
#include
#include
#include
#include
using namespace std;
#pragma comment(lib, "Ws2_32.lib") // Socket动态链接库
//#pragma comment(lib, "Mswsock.lib") // AcceptEx动态链接库
enum class IOOption
{
Invaild,
Accept,
Receive,
Send,
Start,
Exit
};
struct SocketClient
{
SOCKET socket;
PTP_IO io;
bool isConnect;
sockaddr_in ClientAddr;
};
struct IORequest : public OVERLAPPED
{
static const size_t BufferSize = 2043;
char buffer[BufferSize + 1];//多一个空间存储截至字符 \0
WSABUF wsabuf;
bool isUsing;
SocketClient *client;
IOOption option;
IORequest() :OVERLAPPED{ 0 }, buffer{ 0 }
{
option = IOOption::Invaild;
wsabuf.len = BufferSize;
wsabuf.buf = buffer;
isUsing = false;
client = nullptr;
}
};
VOID CALLBACK IoCompletionCallback(PTP_CALLBACK_INSTANCE, PVOID, PVOID, ULONG, ULONG_PTR, PTP_IO);
int AddAcceptRequest();
const DWORD AcceptBufferReservedLen = sizeof(SOCKADDR_IN) + 16;//具体参考AcceptEx的dwLocalAddressLength参数,URL https://docs.microsoft.com/zh-cn/windows/desktop/api/mswsock/nf-mswsock-acceptex
LPFN_ACCEPTEX lpfnAcceptEx = nullptr;
GUID GuidAcceptEx = WSAID_ACCEPTEX;
LPFN_GETACCEPTEXSOCKADDRS lpfnGetAcceptExSockaddrs = nullptr;
GUID GuidGetAcceptExSockaddrs = WSAID_GETACCEPTEXSOCKADDRS;
LPFN_DISCONNECTEX lpfnDisconnectEx = nullptr;//DisconnectEx函数可以关闭客户端的socket,关闭后可以传递给AcceptEx重用。
GUID GuidDisconnectEx = WSAID_DISCONNECTEX;
const int MaxRequestCount = 400;
SOCKET server;
IORequest sendRequest[MaxRequestCount];
static LONG CurrentAcceptRequcest = 0;
CRITICAL_SECTION cs;
set clients;
PTP_IO acceptIO;
int main()
{
WORD wVersionRequested = MAKEWORD(2, 2);
WSADATA wsaData;
int err = WSAStartup(wVersionRequested, &wsaData);
if (err != 0)
{
cout << "Socket Initialize failed!" << endl;
return 1;
}
server = socket(AF_INET, SOCK_STREAM, 0);
if (server == INVALID_SOCKET) {
cout << "Create Socket Error: " << WSAGetLastError() << endl;
WSACleanup();
return 2;
}
SOCKADDR_IN ipAddress;
ipAddress.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
ipAddress.sin_family = AF_INET;
ipAddress.sin_port = htons(60001);
int result = bind(server, (SOCKADDR*)&ipAddress, sizeof(SOCKADDR));
if (0 != result) {
cout << "Bind Error: " << GetLastError() << endl;
return 3;
}
result = listen(server, SOMAXCONN);
if (0 != result) {
cout << "Listen Error: " << GetLastError() << endl;
return 4;
}
DWORD dwBytes;
int iResult = WSAIoctl(server, SIO_GET_EXTENSION_FUNCTION_POINTER, &GuidAcceptEx, sizeof(GuidAcceptEx),
&lpfnAcceptEx, sizeof(lpfnAcceptEx), &dwBytes, NULL, NULL);
iResult = WSAIoctl(server, SIO_GET_EXTENSION_FUNCTION_POINTER, &GuidGetAcceptExSockaddrs, sizeof(GuidGetAcceptExSockaddrs),
&lpfnGetAcceptExSockaddrs, sizeof(lpfnGetAcceptExSockaddrs), &dwBytes, NULL, NULL);
iResult = WSAIoctl(server, SIO_GET_EXTENSION_FUNCTION_POINTER, &GuidDisconnectEx, sizeof(GuidDisconnectEx),
&lpfnDisconnectEx, sizeof(lpfnDisconnectEx), &dwBytes, NULL, NULL);
InitializeCriticalSection(&cs);
acceptIO = CreateThreadpoolIo((HANDLE)server, IoCompletionCallback, NULL, NULL);
AddAcceptRequest();
string str;
cout <<"Server Port:"<< 60001 << ", Input Send: " << endl;
IORequest* pReq = nullptr;
do
{
cin >> str;
if ((str == "Exit"))
{
break;
}
for each (auto c in clients)
{
if (!c->isConnect)
{
continue;
}
pReq = new IORequest();
pReq->isUsing = true;
pReq->option = IOOption::Send;
pReq->client = c;
memcpy(pReq->buffer, str.data(), str.size());
pReq->wsabuf.len = str.size();
StartThreadpoolIo(c->io);//每次发送异步数据前需要调用一次,否则线程池无法收到完成消息并有可能会导致崩溃
iResult = WSASend(c->socket, &pReq->wsabuf, 1, NULL, 0, pReq, NULL);//异步发送
if (iResult != 0 && WSAGetLastError() != WSA_IO_PENDING)
{
CancelThreadpoolIo(c->io);
}
}
} while (true);
CancelIoEx((HANDLE)server, NULL);
WaitForThreadpoolIoCallbacks(acceptIO, FALSE);//
closesocket(server);
CloseThreadpoolIo(acceptIO);
while (clients.size() > 0)
{
auto c = *clients.begin();
CancelIoEx((HANDLE)c->socket, NULL);//取消请求
WaitForThreadpoolIoCallbacks(c->io, FALSE);//等待取消,取消处会删除相应的内存
}
return 0;
}
int AddAcceptRequest()
{
if (nullptr == lpfnAcceptEx)
{
DWORD dwBytes = 0;
int iResult = WSAIoctl(server, SIO_GET_EXTENSION_FUNCTION_POINTER, &GuidAcceptEx, sizeof(GuidAcceptEx),
&lpfnAcceptEx, sizeof(lpfnAcceptEx), &dwBytes, NULL, NULL);
}
const LONG MaxAcceptRequest = 5;//最大等待Accept的数量,这个数量需要根据实际情况调整,
const LONG MinAcceptRequest = 2;//最少等待Accept的数量
static IORequest acceptRequest[MaxAcceptRequest];
if (CurrentAcceptRequcest >= MinAcceptRequest)
{
return 0;
}
EnterCriticalSection(&cs);
int n = 0;
for (int i = 0; i < MaxAcceptRequest; ++i)
{
if (acceptRequest[i].isUsing)
{
continue;
}
SOCKET sclient = socket(AF_INET, SOCK_STREAM, 0);
StartThreadpoolIo(acceptIO);
BOOL Ret = lpfnAcceptEx(server, sclient, acceptRequest[i].buffer,
0, //设为0表示accept客户端时不接收数据
AcceptBufferReservedLen, AcceptBufferReservedLen, NULL, &acceptRequest[i]);
if (Ret == TRUE || (Ret == FALSE && WSAGetLastError() == ERROR_IO_PENDING))
{
cout << "Add Pending Accept" << endl;
acceptRequest[i].isUsing = true;
acceptRequest[i].option = IOOption::Accept;
}
else
{
CancelThreadpoolIo(acceptIO);//发送异步请求失败,取消上一次的StartThreadpoolIo(acceptIO),防止内存泄漏。
cout << "Add Accept Failed" << endl;
break;;
}
SocketClient *client = new SocketClient();
client->isConnect = false;
client->socket = sclient;
client->io = CreateThreadpoolIo((HANDLE)sclient, IoCompletionCallback, NULL, NULL);
acceptRequest[i].client = client;
clients.insert(client);
++n;
}
LeaveCriticalSection(&cs);
InterlockedExchange(&CurrentAcceptRequcest, MaxAcceptRequest);
return n;
}
VOID CALLBACK IoCompletionCallback(
_Inout_ PTP_CALLBACK_INSTANCE Instance,
_Inout_opt_ PVOID Context,
_Inout_opt_ PVOID Overlapped,
_In_ ULONG IoResult,
_In_ ULONG_PTR NumberOfBytesTransferred,
_Inout_ PTP_IO Io)
{
IORequest *pReq = (IORequest*)Overlapped;
IOOption option = pReq->option;
DWORD flag = 0;
switch (option)
{
case IOOption::Invaild:
break;
case IOOption::Accept:
{
if (NO_ERROR != IoResult)
{
bool isFind = false;
EnterCriticalSection(&cs);
isFind = 0 < clients.erase(pReq->client);
LeaveCriticalSection(&cs);
if (isFind)
{
cout << "Port: " << ntohs(pReq->client->ClientAddr.sin_port) << " Disconnect." << endl;
closesocket(pReq->client->socket);
CloseThreadpoolIo(pReq->client->io);
delete pReq->client;
pReq->client = nullptr;
}
}
else
{
int iResult = 0;
iResult = setsockopt(pReq->client->socket, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT, (char *)&server, sizeof(server));
sockaddr_in *local = nullptr, *remote= nullptr;
int localLen = 0, remoteLen = 0;
lpfnGetAcceptExSockaddrs(pReq->buffer, 0, AcceptBufferReservedLen, AcceptBufferReservedLen, (sockaddr**)&local, &localLen, (sockaddr**)&remote, &remoteLen);
if (remoteLen == sizeof(sockaddr_in))
{
memcpy(&pReq->client->ClientAddr, remote, remoteLen);
cout << "Client Connected. Port: " << ntohs(remote->sin_port) << endl;
}
IORequest *recvReq = new IORequest();
recvReq->option = IOOption::Receive;
recvReq->isUsing = true;
recvReq->client = pReq->client;
StartThreadpoolIo(recvReq->client->io);
iResult = WSARecv(recvReq->client->socket, &recvReq->wsabuf, 1, NULL, &flag, recvReq, nullptr);
if (iResult != 0 && WSAGetLastError() != WSA_IO_PENDING)
{
CancelThreadpoolIo(recvReq->client->io);
}
pReq->client->isConnect = true;
}
InterlockedAdd(&CurrentAcceptRequcest, -1);
pReq->isUsing = false;
pReq->client = nullptr;
if (WSA_OPERATION_ABORTED != IoResult) //Request被取消,系统正在退出
{
AddAcceptRequest();
}
}
break;
case IOOption::Receive:
{
if (NO_ERROR != IoResult)
{
bool isFind = false;
EnterCriticalSection(&cs);
isFind = 0 < clients.erase(pReq->client);
LeaveCriticalSection(&cs);
if (isFind)
{
cout << "Port: " << ntohs(pReq->client->ClientAddr.sin_port) << " Disconnect." << endl;
closesocket(pReq->client->socket);
CloseThreadpoolIo(pReq->client->io);
delete pReq->client;
}
delete pReq;
break;
}
if (NumberOfBytesTransferred > 0)
{
pReq->buffer[NumberOfBytesTransferred] = '\0';
cout << "Port: "<< ntohs(pReq->client->ClientAddr.sin_port)<<", Receive: " << pReq->buffer << endl;
}
int iResult = 0;
StartThreadpoolIo(pReq->client->io);
iResult = WSARecv(pReq->client->socket, &pReq->wsabuf, 1, NULL, &flag, pReq, nullptr);
if (iResult != 0 && WSAGetLastError() != WSA_IO_PENDING)
{
CancelThreadpoolIo(pReq->client->io);
}
}
break;
case IOOption::Send:
{
if (NO_ERROR != IoResult)
{
bool isFind = false;
EnterCriticalSection(&cs);
isFind = 0 < clients.erase(pReq->client);
LeaveCriticalSection(&cs);
if (isFind)
{
cout << "Port: " << ntohs(pReq->client->ClientAddr.sin_port) << " Disconnect." << endl;
closesocket(pReq->client->socket);
CloseThreadpoolIo(pReq->client->io);
delete pReq->client;
}
}
else
{
cout << "Port: "<< ntohs(pReq->client->ClientAddr.sin_port) << " Send Successfully." << endl;//异步发送成功
}
delete pReq;
}
break;
case IOOption::Start:
{
}
break;
case IOOption::Exit:
break;
default:
break;
}
}