zoukankan      html  css  js  c++  java
  • 单线程实现同时监听多个端口(windows平台c++代码)

    前言

      多年前开发了一套网络库,底层实现采用IOCP(完成端口)。该库已在公司多个程序中应用;经过多次修改,长时间检验,已经非常稳定高效。

    最近把以前的代码梳理了一下,又加进了一些新的思路。代码结构更加合理,性能也有所提升。打算将该库一些的知识点写出来,以供参考。

    服务端要在多个端口监听,这种场合并不多见。但作为一个完善的网络库,似乎有必要支持此功能的。

    传统实现方法

      如果监听端口个数很少,也可以采用传统的方法。因为accept函数是阻塞的,所以要实现在n个端口监听,就需要n个线程。如果监听端口个数不多,这也不是多大问题。如果监听端口多达几十个,这种方法就有些不妥。线程也是一种资源,线程过多占用资源会增加;也会导致系统负担加重。

    更可行的实现方法

      实现方法有些曲折,需要一步一步分析;基本的原理就是将socket句柄与事件(event)相关联。Windows有相关的函数可以对多个事件监听,当某个事件被触发,就知道相应的socket有事件到达。可以对该socket做accept,因为已经确定该socket有事件了,所以accept函数会立即返回。这样就达到对多个端口同时监听的目的。

    1)生成socket,并与某个端口绑定

    struct LISTEN_SOCKET_INFO
    {
        UINT16   listenPort;  //监听端口
        SOCKET   listenSocket;//句柄
        WSAEVENT netEvent;    //socket对应事件
    };
    
    int IocpAccept::CreateListenInfo()
    {
        //m_listListenPort存储要监听的端口;总个数不超过64个
        std::vector<UINT16>::iterator pos = m_listListenPort.begin();
        for (;pos != m_listListenPort.end();++pos)
        {
            //生成socket
            UINT16 listenPort = *pos;
            LISTEN_SOCKET_INFO socketInfo;
            socketInfo.listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
            socketInfo.listenPort = listenPort;
    
            //绑定端口
            sockaddr_in InetAddr;
            InetAddr.sin_family = AF_INET;
            InetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
            InetAddr.sin_port = htons(listenPort);
    
            int ret = bind(socketInfo.listenSocket, (SOCKADDR *)&InetAddr, sizeof(InetAddr));
            if (SOCKET_ERROR == ret)
            {
                ::closesocket(socketInfo.listenSocket);
                //绑定失败
                continue;
            }
    
            //生成事件
            socketInfo.netEvent = WSACreateEvent();
    
            //将socket句柄与事件关联起来。只监视socket的accept和close消息
            ret = WSAEventSelect(socketInfo.listenSocket, socketInfo.netEvent, FD_ACCEPT | FD_CLOSE);
            if (SOCKET_ERROR == ret)
            {
                ::closesocket(socketInfo.listenSocket);
                continue;
            }
    
            // 启动监听
            ret = listen(socketInfo.listenSocket, 1000);
            if (SOCKET_ERROR == ret)
            {
                ::closesocket(socketInfo.listenSocket);
                continue;
            }
    
            m_listListenInfo.push_back(socketInfo);
        }
        return 0;
    }

    该函数已将需要的数据存储在列表m_listListenInfo中。

    2)启动监听线程,对多个事件监听

      对多个事件监听用到如下函数:

    DWORD WSAAPI WSAWaitForMultipleEvents( DWORD cEvents, const WSAEVENT *lphEvents, BOOL fWaitAll, DWORD dwTimeout, BOOL fAlertable );该函数最多可以对64个事件做跟踪,所以一个线程最多可以对64个端口做监听。(同时对超过64个端口监听的场合非常少见。本文不考虑。)

    //生成事件地址指针
        int nEventTotal;
        WSAEVENT* pEventArray = CreateNetEventArray(&nEventTotal);
        if (nEventTotal == 0)
            return 0;
        assert(nEventTotal <= WSA_MAXIMUM_WAIT_EVENTS);
    
        MSG msg;
        while (m_bServerStart)
        {
            // 同时对多个事件做监听
            DWORD index = WSAWaitForMultipleEvents(nEventTotal,
                pEventArray,
                FALSE,
                10000,
                FALSE);
            if (!m_bServerStart)
                return 0;
    
            //查看是哪个事件触发函数返回
            index = index - WSA_WAIT_EVENT_0;
            //客户端连接事件
            if ((index != WSA_WAIT_FAILED) && (index != WSA_WAIT_TIMEOUT))
            {
                //pEventArray排序与m_listListenInfo一样,所以可以根据index找到对应的socket。
                //就是该socket导致函数返回
                LISTEN_SOCKET_INFO socketInfo = m_listListenInfo[index];
    
                //查看具体是什么事件导致函数返回
                WSANETWORKEVENTS NetworkEvents;
                WSAEnumNetworkEvents(socketInfo.listenSocket, pEventArray[index], &NetworkEvents);
    
                //如果是accept事件,说明有客户端连接此端口
                if (NetworkEvents.lNetworkEvents == FD_ACCEPT
                    && NetworkEvents.iErrorCode[FD_ACCEPT_BIT] == 0)
                {
                    //这时调用accept函数,会立即返回
                    AcceptListenPort(socketInfo.listenSocket, socketInfo.listenPort);
                }
                if (NetworkEvents.lNetworkEvents == FD_CLOSE
                    && NetworkEvents.iErrorCode[FD_CLOSE_BIT] == 0)
                {
                    assert(false);
                }
    
            }
            else
            {
                //因为超时等其他原因引起函数返回
            }
        }

    下文accept函数调用,并不会阻塞。

    UINT IocpAccept::AcceptListenPort(SOCKET hListenSocket, UINT16 nListenPort)
    {
        SOCKET hClient = 0;
        SOCKADDR_IN localAddr;
        int iaddrSize = sizeof(SOCKADDR_IN);
    
        hClient = accept(hListenSocket, (struct sockaddr *)&localAddr, &iaddrSize);
        if (INVALID_SOCKET == hClient)
        {
            int nAccepetError = WSAGetLastError();
            if (nAccepetError == WSAECONNRESET)
            {
                return 1;
            }
            else
            {
                return 0;
            }
        }
        else
        {
            //获取了一个客户端连接
            OnAcceptClient(hClient, nListenPort);
        }
        return 0;
    }

    后记:同时对多个端口做监听,可能还有更好的方法。如果对几百个以上端口做监听,此方法可能就不太合适。通常情况下,对多个端口监听的场景比较少见,所以对更优化的处理方法也没深究。

    代码下载地址: https://download.csdn.net/download/qq_29939347/10691921

  • 相关阅读:
    个人作业——软件工程实践总结&个人技术博客
    个人作业——软件评测
    结对第二次作业——某次疫情统计可视化的实现
    结对第一次—疫情统计可视化(原型设计)
    软工实践寒假作业(2/2)
    软工实践寒假作业(1/2)
    个人技术总结 (vex-table)
    个人作业——软件工程实践总结&个人技术博客
    个人作业——软件评测
    结对第二次作业——疫情统计可视化的实现
  • 原文地址:https://www.cnblogs.com/yuanchenhui/p/icop_accept.html
Copyright © 2011-2022 走看看