前言在 Windows 平台下实现高性能网络服务器, iocp(完成端口)是唯一选择. 编写网络服务器面临的问题有: 1 快速接收客户端的连接. 2 快速收发数据. 3 快速处理数据. 本文主要解决第一个问题.
AcceptEx 函数定义
- BOOL AcceptEx(
- SOCKET sListenSocket,
- SOCKET sAcceptSocket,
- PVOID lpOutputBuffer,
- DWORD dwReceiveDataLength,
- DWORD dwLocalAddressLength,
- DWORD dwRemoteAddressLength,
- LPDWORD lpdwBytesReceived,
- LPOVERLAPPED lpOverlapped
- );
为什么要用 AcceptEx
传统的 accept 函数能满足大部分场景的需要; 但在某些极端条件下, 必须使用 acceptEx 来实现. 两个函数的区别如下:
1)accept 是阻塞的; 在一个端口监听, 必须启动一个专用线程调用 accept. 当然也可以用迂回的方式, 绕过这个限制, 处理起来会很麻烦, 见文章单线程实现同时监听多个端口. acceptEx 是异步的, 可以同时对很多端口监听(监听端口的数量没有上限的限制). 采用迂回的方式, 使用 accept 监听, 一个线程最多监听 64 个端口. 这一点可能不是 AcceptEx 最大优点, 毕竟同时对多个端口监听的情况非常少见.
2)AcceptEx 可以返回更多的数据. a)AcceptEx 可以返回本地和对方 ip 地址和端口; 而不需要调用函数 getsockname 和 getpeername 获取网络地址了. b)AcceptEx 可以再接收到一段数据后, 再返回. 这种做法有利有弊, 一般不建议这样做.
3)AcceptEx 是先准备套接字 (socket) 后接收. 为了应对突发的连接高峰, 可以多次投放 AcceptEx.accept 是事后建立 SOCKET, 就是 tcp 三次握手完成后, accept 调用才返回, 再生成 socket. 生成套接字是相对比较耗时的操作, accept 的方式无法及时处理突发连接. 对于 AcceptEx 的处理方式为建议做如下处理: 一个线程负责创建 socket, 一个线程负责处理 AcceptEx 返回.
以上仅仅通过文字说明了 AcceptEx 的特点. 下面通过具体代码, 逐一剖析. 我将 AcceptEx 的处理封装到类 IocpAcceptEx 中. 编写该类时, 尽量做到高内聚低耦合, 使该类可以方便的被其他模块使用.
IocpAcceptEx 外部功能说明
- class IocpAcceptEx
- {
- public:
- IocpAcceptEx();
- ~IocpAcceptEx();
- // 设置回调接口. 当 accept 成功, 调用回调接口.
- void SetCallback(IAcceptCallback* callback);
- // 增加监听端口
- void AddListenPort(UINT16 port);
- // 启动服务
- BOOL Start();
- void Stop();
... 以下代码省略
- }
- #define POST_ACCEPT 1
- // 使用 IocpAcceptEx 类, 必须实现该接口. 接收客户端的连接
- class IAcceptCallback
- {
- public:
- virtual void OnAcceptClient(SOCKET hSocketClient, UINT16 nListenPort) = 0;
- };
该类的调用函数很简单, 对外接口也很明确. 说明该类的职责很清楚, 这也符合单一职责原则.
实现步骤说明
AcceptEx 不但需要与监听端口绑定, 还需要与完成端口绑定. 所以程序的第一步是创建完成端口:
a)创建完成端口
- m_hIocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, NULL, 0);
- if (m_hIocp == NULL)
- return FALSE;
b)监听端口创建与绑定
- // 生成套接字
- SOCKET serverSocket = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
- if (serverSocket == INVALID_SOCKET)
- {
- return false;
- }
- // 绑定
- SOCKADDR_IN addr;
- memset(&addr, 0, sizeof(addr));
- addr.sin_family = AF_INET;
- addr.sin_addr.s_addr = INADDR_ANY ;
- addr.sin_port = htons(port);
- if (bind(serverSocket, (sockaddr *)&addr, sizeof(addr)) != 0)
- {
- closesocket(serverSocket);
- serverSocket = INVALID_SOCKET;
- return false;
- }
- // 启动监听
- if (listen(serverSocket, SOMAXCONN) != 0)
- {
- closesocket(serverSocket);
- serverSocket = INVALID_SOCKET;
- return false;
- }
- // 监听端口与完成端口绑定
- if (CreateIoCompletionPort((HANDLE)serverSocket, m_hIocp, (ULONG_PTR)this, 0) == NULL)
- {
- closesocket(serverSocket);
- serverSocket = INVALID_SOCKET;
- return false;
- }
c)投递 AcceptEx
- struct AcceptOverlapped
- {
- OVERLAPPED overlap;
- INT32 opType;
- SOCKET serverSocket;
- SOCKET clientSocket;
- char lpOutputBuf[128];
- DWORD dwBytes;
- };
- int IocpAcceptEx::NewAccept(SOCKET serverSocket)
- {
- // 创建 socket
- SOCKET _socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
- AcceptOverlapped *ov = new AcceptOverlapped();
- ZeroMemory(ov,sizeof(AcceptOverlapped));
- ov->opType = POST_ACCEPT;
- ov->clientSocket = _socket;
- ov->serverSocket = serverSocket;
- // 存放网络地址的长度
- int addrLen = sizeof(sockaddr_in) + 16;
- int bRetVal = AcceptEx(serverSocket, _socket, ov->lpOutputBuf,
- 0,addrLen, addrLen,
- &ov->dwBytes, (LPOVERLAPPED)ov);
- if (bRetVal == FALSE)
- {
- int error = WSAGetLastError();
- if (error != WSA_IO_PENDING)
- {
- closesocket(_socket);
- return 0;
- }
- }
- return 1;
- }
AcceptEx 是非阻塞操作, 调用会立即返回. 当有客户端连接时, 怎么得到通知. 答案是通过完成端口返回. 注意有一个步骤: 监听端口与完成端口绑定, 就是 serverSocket 与 m_hIocp 绑定, 所以当有客户端连接 serverSocket 时, m_hIocp 会得到通知. 需要生成线程, 等待完成端口的通知.
d)通过完成端口, 获取通知
- DWORD dwBytesTransferred;
- ULONG_PTR Key;
- BOOL rc;
- int error;
- AcceptOverlapped *lpPerIOData = NULL;
- while (m_bServerStart)
- {
- error = NO_ERROR;
- rc = GetQueuedCompletionStatus(
- m_hIocp,
- &dwBytesTransferred,
- &Key,
- (LPOVERLAPPED *)&lpPerIOData,
- INFINITE);
- if (rc == FALSE)
- {
- error = 0;
- if (lpPerIOData == NULL)
- {
- DWORD lastError = GetLastError();
- if (lastError == WAIT_TIMEOUT)
- {
- continue;
- }
- else
- {
- assert(false);
- return lastError;
- }
- }
- }
- if (lpPerIOData != NULL)
- {
- switch (lpPerIOData->opType)
- {
- case POST_ACCEPT:
- {
- OnIocpAccept(lpPerIOData, dwBytesTransferred, error);
- }
- break;
- }
- }
- else
- {
- }
- }
- return 0;
- DWORD WINAPI IocpAcceptEx::AcceptExThreadPool(PVOID pContext)
- {
- ThreadPoolParam *param = (ThreadPoolParam*)pContext;
- param->pIocpAcceptEx->NewAccept(param->ServeSocket);
- delete param;
- return 0;
- }
- int IocpAcceptEx::OnIocpAccept(AcceptOverlapped *acceptData, int transLen, int error)
- {
- m_IAcceptCallback->OnAcceptClient(acceptData->clientSocket, acceptData->serverSocket);
- // 当一个 AcceptEx 返回, 需要投递一个新的 AcceptEx.
- // 使用线程池好像有点小题大做. 前文已说过, 套接字的创建相对是比较耗时的操作.
- // 如果不在线程池投递 AcceptEx,AcceptEx 的优点就被抹杀了.
- ThreadPoolParam *param = new ThreadPoolParam();
- param->pIocpAcceptEx = this;
- param->ServeSocket = acceptData->serverSocket;
- QueueUserWorkItem(AcceptExThreadPool, this, 0);
- delete acceptData;
- return 0;
- }
后记 采用完成端口是提高 IO 处理能力的一个途径(广义上讲, 通讯操作也是 IO). 为了提高 IO 处理能力, Windows 提供很多异步操作函数, 这些函数都与完成端口关联, 所以这一类处理的思路基本一致. 学会了 AcceptEx 的使用, 可以做到触类旁通的效果.
来源: https://www.cnblogs.com/yuanchenhui/p/acceptex_socket.html