前面几章已经分别介绍了window下socket 网络编程的几种模式,今天简单的介绍一下最后一个模型:完成端口(completionPort)模型。
关于他的一些优点网上有一堆,这边我也不再一一介绍,点而言之就是他充分利用内核对象的调度,只使用少量的几个线程来梳理和客户端所有通信,最大限度的提高了网络通信的性能。下面简单介绍一下主要涉及主要函数。
最优线程数
根据实际应用中发现,Cup核数*2+2这个数量是最优线程数(网上查询到的,我也不知道原因),获取系统内核数可调用
VOID GetSystemInfo(LPSYSTEM_INFO lpSystemInfo)
通过LPSYSTEM_INFO 结构体获取内核数
SYSTEM_INFO sys_info;
GetSystemInfo(&sys_info);
//获取CPU核数
int n = sys_info.dwNumberOfProcessors;
CreateIoCompletionPort函数
创建一个完成端口,函数原型为
HANDLE WINAPI CreateIoCompletionPort(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE ExistingCompletionPort,
_In_ ULONG_PTR CompletionKey,
_In_ DWORD NumberOfConcurrentThreads
);
参数:
FileHandle
打开的文件句柄或INVALID_HANDLE_VALUE。
该句柄必须指向支持重叠I / O的对象。
如果提供了句柄,则必须打开它才能完成I / O重叠。例如,在使用CreateFile函数获取句柄时,必须指定FILE_FLAG_OVERLAPPED标志 。
如果指定了INVALID_HANDLE_VALUE,那ExistingCompletionPort参数必须为NULL,并且CompletionKey参数将被忽略
ExistingCompletionPort 现有I / O完成端口或NULL的句柄。
如果此参数指定了现有的I / O完成端口,则该函数将其与FileHandle参数指定的句柄相关联。如果成功,该函数将返回现有I / O完成端口的句柄;它不会创建新的I / O完成端口。
如果此参数为NULL,则该函数将创建一个新的I / O完成端口,并且如果FileHandle参数有效,则将其与新的I / O完成端口关联。否则,不会发生文件句柄关联。如果成功,该函数将句柄返回到新的I / O完成端口。
CompletionKey
每个句柄用户定义的完成密钥,包含在指定文件句柄的每个I / O完成数据包中。
NumberOfConcurrentThreads
操作系统可以允许同时处理I / O完成端口的I / O完成数据包的最大线程数。如果ExistingCompletionPort参数不为NULL,则忽略此参数。
如果此参数为零,则系统允许的并发运行线程数与系统中的处理器数量一样。
综上:如果你要创建一个新的完成端口,第一个参数必须设置为INVALID_HANDLE_VALUE,第二个参数为NULL,第三个忽略,第四个参数是运行的最大线程数量,例如:
//创建完成端口
HANDLE g_completionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
如果要在创建的端口单口上关联一个I/O操作,如sokcet 监听套接字,具体操作如下:
SOCKET listenSock=socket(AF_INET, SOCK_STREAM, 0);
//绑定监听套接字和端口
CreateIoCompletionPort((HANDLE)listenSock, g_completionPort, (DWORD)listenSock, 0);
返回值:
如果函数成功,则返回值是I / O完成端口的句柄:
如果ExistingCompletionPort参数为NULL,则返回值为新句柄。
如果ExistingCompletionPort参数是有效的I / O完成端口句柄,则返回值就是该句柄。
如果FileHandle参数是有效的句柄,则该文件句柄现在与返回的I / O完成端口关联。
如果函数失败,则返回值为NULL。若要获取扩展的错误信息,请调用GetLastError函数。
ACCEPTEX 函数
此函数是一个Microsoft特定的扩展,原型如下:
BOOL AcceptEx(
_In_ SOCKET sListenSocket,
_In_ SOCKET sAcceptSocket,
_In_ PVOID lpOutputBuffer,
_In_ DWORD dwReceiveDataLength,
_In_ DWORD dwLocalAddressLength,
_In_ DWORD dwRemoteAddressLength,
_Out_ LPDWORD lpdwBytesReceived,
_In_ LPOVERLAPPED lpOverlapped
);
但是平不是所有的系统都支持使用这个API,并且获取的开销很大,不建议直接使用(好多资料上都是这么说的,具体未研究)
微软给了俩个特定指针来解析这个动作,固定的格式:
LPFN_ACCEPTEX lpfnAcceptEx;
LPFN_GETACCEPTEXSOCKADDRS lpfnGetAcceptExSockaddrs;
GUID guidAcceptEx = WSAID_ACCEPTEX;
GUID guidGetAddr = WSAID_GETACCEPTEXSOCKADDRS;
//两个扩展函数,成功都是返回0
DWORD bytes;
//加载AccpetEx函数指针
int rc = WSAIoctl(
listenSock,
SIO_GET_EXTENSION_FUNCTION_POINTER,
&guidAcceptEx,
sizeof(guidAcceptEx),
&lpfnAcceptEx,
sizeof(lpfnAcceptEx),
&bytes,
NULL,
NULL
);
//加载GetAcceptExSockaddrs函数指针
rc = WSAIoctl(
listenSock,
SIO_GET_EXTENSION_FUNCTION_POINTER,
&guidGetAddr,
sizeof(guidGetAddr),
&lpfnGetAcceptExSockaddrs,
sizeof(lpfnGetAcceptExSockaddrs),
&bytes,
NULL,
NULL
);
上面的写法是固定的,不明白的可以查询关资料。
GetQueuedCompletionStatus
获取完成端口的状态,当有重叠任务完成时,在多个调用该函数的线程中挑选一个线程返回,并返回相应的结构用于Accept,Recv,Send等操作。
函数原型:
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,
LPDWORD lpNumberOfBytes,
PULONG_PTR lpCompletionKey,
LPOVERLAPPED *lpOverlapped,
DWORD dwMilliseconds
);
参数:
CompletionPort:指定的IOCP,该值由CreateIoCompletionPort函数创建的完成端口句柄
lpnumberofbytes:一次完成后的I/O操作所传送数据的字节数。
lpcompletionkey:当文件I/O操作完成后,用于存放与之关联的CK,如socket 等
lpoverlapped:为调用IOCP机制所引用的OVERLAPPED结构。
dwmilliseconds:用于指定调用者等待CP的时间,如果设置为 INFINITE,表示会一直等待下去
返回值:
调用成功,则返回非零数值,相关数据存于lpNumberOfBytes、lpCompletionKey、lpCompletionKey变量中。失败则返回零值。
DWORD dwBytesTransfered = 0;
LPPER_IO_OPERATION_DATA pIO = nullptr;
SOCKET sock = INVALID_SOCKET;
/*
四个参数分别是:
创建的完成端口句柄 g_completionPort
接收的字节数
完成重叠动作的关联套接字,socket
等待时间,单位毫秒
*/
bool bRet = GetQueuedCompletionStatus(g_completionPort,
&dwBytesTransfered,
(PULONG_PTR)&sock,
(LPOVERLAPPED*)&pIO,
INFINITE
自定义的重叠结构
在实际的操作中,为了方便我们使用,一般我们需要自定义一个重叠结构体,基本都是大同小异,一般定义如下:
#define MSGSIZE 1024
//定义枚举类型
enum OPERATION_TYPE
{
Io_Send,
Io_Recv,
Io_Accept
};
//定义具体重叠结构体
typedef struct
{
WSAOVERLAPPED overlap;
SOCKET socket;
WSABUF buffer;
char szMessage[MSGSIZE];
DWORD NumberOfBytesRecvd;
DWORD Flags;
OPERATION_TYPE Type;
} PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA;
用到的知识点基本上就这么多,下面是具体的服务端代码.
编译环境: vs2019
// completionPort.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <thread>
#include <WinSock2.h>
#include <mswsock.h>
#define MSGSIZE 1024
using namespace std;
#pragma comment(lib,"ws2_32.lib")
LPFN_ACCEPTEX lpfnAcceptEx;
LPFN_GETACCEPTEXSOCKADDRS lpfnGetAcceptExSockaddrs;
HANDLE g_completionPort;
SOCKET listenSock;
int ntotal = 0;
enum OPERATION_TYPE
{
Io_Send,
Io_Recv,
Io_Accept
};
typedef struct
{
WSAOVERLAPPED overlap;
SOCKET socket;
WSABUF buffer;
char szMessage[MSGSIZE];
DWORD NumberOfBytesRecvd;
DWORD Flags;
OPERATION_TYPE Type;
} PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA;
bool initSocket()
{
WSADATA wsdata;
WORD sVer = MAKEWORD(2, 2);
int ret = WSAStartup(sVer, &wsdata);
if (ret != 0)
{
return false;
}
return true;
}
bool PostAccept()
{
DWORD dwbytes;
LPPER_IO_OPERATION_DATA pIO = nullptr;
pIO = (LPPER_IO_OPERATION_DATA)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(PER_IO_OPERATION_DATA));
pIO->buffer.len = MSGSIZE;
pIO->buffer.buf = pIO->szMessage;
SOCKET psock = socket(AF_INET, SOCK_STREAM, 0);
if (psock==INVALID_SOCKET)
{
return false;
}
pIO->socket = psock;
pIO->Type = Io_Accept;
bool rc = lpfnAcceptEx(listenSock, psock, &pIO->buffer, 0,
sizeof(sockaddr_in) + 16,
sizeof(sockaddr_in) + 16,
&dwbytes,
&pIO->overlap);
if (ERROR_IO_PENDING!=WSAGetLastError())
{
return false;
}
return true;
}
bool postSend(LPPER_IO_OPERATION_DATA& lap)
{
ZeroMemory(&lap->overlap, sizeof(WSAOVERLAPPED));
ZeroMemory(lap->szMessage, MSGSIZE);
lap->buffer.buf = lap->szMessage;
lap->Type = Io_Send;
//继续投递重叠结构
WSASend(lap->socket,
&lap->buffer,
1,
&lap->NumberOfBytesRecvd,
lap->Flags,
&lap->overlap,
NULL);
if (ERROR_IO_PENDING != WSAGetLastError())
{
return false;
}
return true;
}
bool postRecv(LPPER_IO_OPERATION_DATA &lap)
{
//清空重叠结构和缓冲区
ZeroMemory(&lap->overlap, sizeof(WSAOVERLAPPED));
ZeroMemory(lap->szMessage, MSGSIZE);
//重叠结构体的长度设置不可省略,省略之后调试发现长度是0,无法接收数据
lap->buffer.len = MSGSIZE;
lap->buffer.buf = lap->szMessage;
lap->Type = Io_Recv;
//继续投递重叠结构
WSARecv(lap->socket,
&lap->buffer,
1,
&lap->NumberOfBytesRecvd,
&lap->Flags,
&lap->overlap,
NULL);
if (ERROR_IO_PENDING != WSAGetLastError())
{
return false;
}
return true;
}
void workThread()
{
DWORD dwBytesTransfered = 0;
LPPER_IO_OPERATION_DATA pIO = nullptr;
SOCKET sock = INVALID_SOCKET;
while (true)
{
bool bRet = GetQueuedCompletionStatus(g_completionPort,
&dwBytesTransfered,
(PULONG_PTR)&sock,
(LPOVERLAPPED*)&pIO,
INFINITE
);
if (dwBytesTransfered == 0 && ((pIO->Type == Io_Recv) || (pIO->Type == Io_Send)))
{
CancelIo((HANDLE)pIO->socket);
closesocket(pIO->socket);
HeapFree(GetProcessHeap(), 0, pIO);
}
else
{
if (pIO->Type == Io_Accept)
{
SOCKADDR_IN* addrClient = NULL, * addrLocal = NULL;
int nClientLen = sizeof(SOCKADDR_IN), nLocalLen = sizeof(SOCKADDR_IN);
//调用lpfnGetAcceptExSockaddrs 获取客户端连接信息
lpfnGetAcceptExSockaddrs(&pIO->buffer, 0,
sizeof(SOCKADDR_IN) + 16, sizeof(SOCKADDR_IN) + 16,
(LPSOCKADDR*)&addrLocal, &nLocalLen,
(LPSOCKADDR*)&addrClient, &nClientLen);
//关键点:要把完成端口操作和socket绑定起来
CreateIoCompletionPort((HANDLE)pIO->socket, g_completionPort, (DWORD)pIO->socket, 0);
/*收到连接重叠结构,说明有客户端连接上来
1>要投递一个接收数据重叠结构,来接收数据
2>申请的等待连接的socket 已经被占用,再重新投递重叠结构,等待客户端的连接
*/
postRecv(pIO);
PostAccept();
}
else if (pIO->Type == Io_Recv)
{
cout << pIO->szMessage << endl;
postRecv(pIO);
}
else if (pIO->Type == Io_Send)
{
postSend(pIO);
}
}
}
}
int main()
{
lpfnAcceptEx = NULL;
lpfnGetAcceptExSockaddrs = NULL;
SYSTEM_INFO sys_info;
g_completionPort = INVALID_HANDLE_VALUE;
//初始化com
if (!initSocket())
{
return -1;
}
sockaddr_in Sa;
int len = sizeof(Sa);
Sa.sin_port = htons(8888);
Sa.sin_addr.S_un.S_addr = INADDR_ANY;
Sa.sin_family = AF_INET;
listenSock = socket(AF_INET, SOCK_STREAM, 0);
if (listenSock == INVALID_SOCKET)
{
cout << "Create socket fail" << endl;
return -1;
}
if (bind(listenSock, (sockaddr*)&Sa, len) == SOCKET_ERROR)
{
cout << "bind fail" << endl;
return -1;
}
if (listen(listenSock, 5) == SOCKET_ERROR)
{
cout << "listen fail" << endl;
return -1;
}
//创建完成端口
g_completionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
//绑定监听套接字和端口
CreateIoCompletionPort((HANDLE)listenSock, g_completionPort, (DWORD)listenSock, 0);
GetSystemInfo(&sys_info);
//获取CPU核数
int n = sys_info.dwNumberOfProcessors;
GUID guidAcceptEx = WSAID_ACCEPTEX;
GUID guidGetAddr = WSAID_GETACCEPTEXSOCKADDRS;
//两个扩展函数,成功都是返回0
DWORD bytes;
//加载AccpetEx函数指针
int rc = WSAIoctl(
listenSock,
SIO_GET_EXTENSION_FUNCTION_POINTER,
&guidAcceptEx,
sizeof(guidAcceptEx),
&lpfnAcceptEx,
sizeof(lpfnAcceptEx),
&bytes,
NULL,
NULL
);
//加载GetAcceptExSockaddrs函数指针
rc = WSAIoctl(
listenSock,
SIO_GET_EXTENSION_FUNCTION_POINTER,
&guidGetAddr,
sizeof(guidGetAddr),
&lpfnGetAcceptExSockaddrs,
sizeof(lpfnGetAcceptExSockaddrs),
&bytes,
NULL,
NULL
);
//关键点 首先要先投递出一个接收客户端连接的重叠操作,等待客户端连接上来,这一点不可缺少
PostAccept();
thread t(workThread);
t.detach();
// for (int i = 0; i < n * 2 + 2; i++)
// {
// thread t(workThread);
// t.detach();
// }
while (true)
{
Sleep(1000);
}
return 0;
}
注意点:
- 在主线程里需要先投递一个接收soceket,即PostAccept();
- 在postRecv()中要重新设定重叠结构的长度
lap->buffer.len = MSGSIZE;
lap->buffer.buf = lap->szMessage;
- 接收到完成动作之后,如果type=Io_Accept,需要关联新socket和完成端口动作,并且投递新的接收接收重叠结构和新的套接:
//关键点:要把完成端口操作和socket绑定起来
CreateIoCompletionPort((HANDLE)pIO->socket, g_completionPort, (DWORD)pIO->socket, 0);
/*收到连接重叠结构,说明有客户端连接上来
1>要投递一个接收数据重叠结构,来接收数据
2>申请的等待连接的socket 已经被占用,再重新投递重叠结构,等待客户端的连接*/
postRecv(pIO);
postAccept();
到此为止,socket 五种模式都已经介绍完了,由于能力有限,一些Demo中代码可能有所疏漏,欢迎给位指正。这几篇文章主要是自己记录一下自己所学知识,当然如果能够给需要的人提供微弱的帮助,我之所愿矣.