完成端口补全

前言

妈的最近忙疯了,公司现在有一个项目简单来说就是使用完成端口服务端去接受下位机发送的数据,同时对下位机的连接生命周期进行管理。当然这只是项目的一个小部分。本来都是很简单的事情但是由于我们硬件供应商出了一大堆幺蛾子搞得非常伤,主要是采用的硬件方案不是一个成品是临时设计开发的,各方面测试都没有过,抗干扰能力非常差。但是就是因为这种方式的问题反而暴露了一下我写的完成端口程序一个巨大的BUG,也是我水平不够高没有彻底理解AcceptEX()的工作方式。

首先给看过我帖子的朋友道个歉,如果你完全按照我之前帖子的方式编写自己的服务端程序那必然也会有这个问题原贴链接是MFC高性能网络编程:完成端口

问题描述

我们都知道socket编程之中对监听的socket有一个函数是accept,这个函数的作用是阻塞方式,当有链接来的时候便会返回。它的性能其实不高,原因在于当我们accept的时候对于客户端的连接回去创建一个新的socket,这个时候便会去申请内容完成构造。我们使用AcceptEX方式是预先创建完成一个socket当连接来的时候便将这个socket给客户端,所以他的性能很好。同时AcceptEX不会阻塞而是直接放回,所以需要将接受到的socket投递给完成端口好让完成端口进行响应。之前我写的创建方式是这样的代码:

GuidAcceptEx = WSAID_ACCEPTEX;
    DWORD dwBytes = 0;
    SOCKET Accept = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, WSA_FLAG_OVERLAPPED);
    if (SOCKET_ERROR  == WSAIoctl(Accept, SIO_GET_EXTENSION_FUNCTION_POINTER, &GuidAcceptEx, sizeof(GuidAcceptEx), &m_lpfnAcceptEx, sizeof(m_lpfnAcceptEx), &dwBytes, NULL, NULL))
    {
        CString E;
        E.Format(_T("%d"), WSAGetLastError());
        Function->WriteLog(_T("查找接受函数指针错误:") + E);
    }
    PPER_IO_CONTEXT mainPerIoData = (PPER_IO_CONTEXT)GlobalAlloc(GPTR, sizeof(PER_IO_CONTEXT));
    mainPerIoData->m_sockAccept = Accept;
    mainPerIoData->dataLength = DATA_BUFSIZE;
    RtlZeroMemory(&(mainPerIoData->m_Overlapped), sizeof(OVERLAPPED));
    mainPerIoData->m_OpType = ACCEPT;
    mainPerIoData->dataLength = DATA_BUFSIZE;
    mainPerIoData->m_wsaBuf.buf = mainPerIoData->m_szBuffer;
    mainPerIoData->m_wsaBuf.len = mainPerIoData->dataLength;
    if (FALSE == m_lpfnAcceptEx(mainPerHandleData->m_Socket, mainPerIoData->m_sockAccept, &mainPerIoData->m_szBuffer, mainPerIoData->dataLength - ((sizeof(SOCKADDR_IN) + 16) * 2), sizeof(SOCKADDR_IN) + 16, sizeof(SOCKADDR_IN) + 16, &dwBytes, &(mainPerIoData->m_Overlapped)))
    {
        if (WSAGetLastError() != WSA_IO_PENDING)
        {
            CString E;
            E.Format(_T("%d"), WSAGetLastError());
            Function->WriteLog(_T("投递接收请求错误:") + E);
        }
    }

大家可以看到m_lpfnAcceptEx是AcceptEX的函数指针,我们先来分析一下AcceptEX的参数到底是一些什么东西在将问题讲出来:

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
); 

第一个参数是监听的socket,第二是接受的socket这两个没有什么好说的,第三个参数连接数据的缓冲区包含服务器的本地地址,客户机的远程地址,以及在新建连接上发送的第一个数据块三种数据,第四个参数是接收数据的长度,也就是缓冲区的长度,这个就是一个问题了,接收数据长度如果设置的值大于零的话那么完成端口是不会响应连接的。意思是当一个连接上来之后没有发送数据那么不仅仅IOCP没有能力识别到这个事件同时这个连接会将接受给占用,直到它发送第一条数据,如果将这个值设置为0则IOCP会立刻响应新连接,这就是问题。后面几个参数上一篇文件有我也就不多说了。

简单来讲问题就是:当客户端建立了连接但是没有向服务端发送任何数据的时候会将我们准备的accept给占用了,这时新的连接来了之后就没有东西来接它了。虽然在队里里面等待这但是没有入口IOCP也不会响应。

解决方案

经过上面的分析解决方案有两个,第一是改变AcceptEX的缓冲区大小使完成端口立刻响应连接并进入处理。问题是我现在写好的程序解析与管理部分都已经经过多重测试,如果这样修改的话后续的结构基本上都会发送变动。第二是找一个办法能够监测accept的连接时长如果当时间超过一个具体的值之后把它干掉重新投递就可以解决了。我使用的就是这种方式,简单来说就是创建一个新的线程进行监管超时之后处理一下就行了。这里便需要使用一个NB的函数getsockopt:

#include <sys/types.h>
#include <sys/socket.h>
int getsockopt(int sockfd, int level, int optname, void *optval, socklen_t*optlen);
//sockfd:一个标识套接口的描述字。
//level:选项定义的层次。例如,支持的层次有SOL_SOCKET、IPPROTO_TCP。
//optname:需获取的套接口选项。
//optval:指针,指向存放所获得选项值的缓冲区。
//optlen:指针,指向optval缓冲区的长度值。

这些参数里面最重要的就是套接字的选项,这个参数指明你需要获取socket的什么东西。在众多的选项之中有一个就是SO_CONNECT_TIME,他表示的是socket的连接时间,如果没有连接的话optval将等于-1,如果已经连接的话这个值便是连接的时间单位为秒。所以我的处理方案便是:

void IO_CompletionPort::CheckAccept() {
  while (true)
  {
    Sleep(1000);
    int iSecs;
    int iBytes = sizeof(int);
    EnterCriticalSection(&CheckCS);//TODO进入临界区
    getsockopt(Accept, SOL_SOCKET, SO_CONNECT_TIME, (char *)&iSecs, &iBytes);
    LeaveCriticalSection(&CheckCS);//TODO离开临界区
    if (iSecs > 10) {//大于10S后断开
      closesocket(Accept);
    }
  }
}

这里的临界区是为了保证多线程情况下函数操作的线程安全,但是理论上来讲socket就是线程安全,我这样写无非就是为了以防万一。当closesocket执行的时候完成端口可以捕捉到对应的响应然后处理一下重新投递就可以了。其中的Accept对象就是创建的待接收socket,这个东西我是放在全局更新的,虽然这样做基本上毁了AcceptEX的高性能,但是无所谓了。

总结

这次总结就比较简单了:吃一堑长一智!!!

留下回复