• AcceptEx与完成端口(IOCP)结合实例


    前言 在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的使用,可以做到触类旁通的效果。
  • 相关阅读:
    Javascript 对象(object)合并 转
    数据库连接池设置
    约瑟夫问题
    链表中环入口节点
    Spring整合Mybatis
    Spring中事务管理
    Spring中对象和属性的注入方式
    把数组排成最小的数
    Spring之IOC
    Spring之AOP
  • 原文地址:https://www.cnblogs.com/yuanchenhui/p/acceptex_socket.html
Copyright © 2020-2023  润新知