zoukankan      html  css  js  c++  java
  • 采用完成端口(IOCP)实现高性能网络服务器(Windows c++版)

    前言

     TCPIP已成为业界通讯标准。现在越来越多的程序需要联网。网络系统分为服务端和客户端,也就是cs模式(client server)。client一般有一个或少数几个连接;server则需要处理大量连接。大部分情况下,只有服务端才特别考虑性能问题。本文主要介绍服务端处理方法,当然也可以用于客户端。

      我也发表过c#版网络库。其实,我最早是从事c++开发,多年前就实现了对完成端口的封装。最近又把以前的代码整理一下,做了测试,也和c#版网络库做了粗略对比。总体上,还是c++性能要好一些。c#网络库见文章《一个高性能异步socket封装库的实现思路》。

          Windows平台下处理socket通讯有多种方式;大体可以分为阻塞模式和非阻塞模式。阻塞模式下send和recv都是阻塞的。简单讲一下这两种模式处理思路。

      阻塞模式:比如调用send时,把要发送的数据放到网络发送缓冲区才返回。如果这时,网络发送缓冲区满了,则需要等待更久的时间。socket的收发其实也是一种IO,和读写硬盘数据有些类似。一般来讲,IO处理速度总是慢的,不要和内存处理并列。对于调用recv,至少读取一个字节数据,函数才会返回。所以对于recv,一般用一个单独的线程处理。

          非阻塞模式:send和recv都是非阻塞的;比如调用send,函数会立马返回。真正的发送结果,需要等待操作系统的再次通知。阻塞模式下一步可以完成的处理,在非阻塞模式下需要两步。就是多出的这一步,导致开发难度大大增加。高性能大并发网络服务器必须采用非阻塞模式。完成端口(IOCP)是非阻塞模式中性能最好的一种。

       作者多年以前,就开始从事winsocket开发,最开始是采用c++、后来采用c#。对高性能服务器设计的体会逐步加深。人要在一定的压力下才能有所成就。最开始的一个项目是移动信令分析,所处理的消息量非常大;高峰期,每秒要处理30万条信令,占用带宽500M。无论是socket通讯还是后面的数据处理,都必须非常优化。所以从项目的开始,我就谨小慎微,对性能特别在意。项目实施后,程序的处理性能出乎意料。一台服务器可以轻松处理一个省的信令数据(项目是08年开始部署,现在的硬件性能远超当时)。程序界面如下:

     

    题外话 通过这个项目我也有些体会:1)不要怀疑Windows的性能,不要怀疑微软的实力。有些人遇到性能问题,或是遇到奇怪的bug,总是把责任推给操作系统;这是不负责任的表现。应该反思自己的开发水平、设计思路。2)开发过程中,需要把业务吃透;业务是开发的基石。不了解业务,不可能开发出高性能的程序。所有的处理都有取舍,每个函数都有他的适应场合。有时候需要拿来主义,有时候需要从头开发一个函数。

    目标

      开发出一个完善的IOCP程序是非常困难的。怎么才能化繁为简?需要把IOCP封装;同时这个封装库要有很好的适应性,能满足各种应用场景。一个好的思路就能事半功倍。我就是围绕这两个目标展开设计。

      1 程序开发接口

      socket处理本质上可以分为:读、写、accept、socket关闭等事件。把这些事件分为两类:a)读、accept、socket关闭 b)写;a类是从库中获取消息,b类是程序主动调用函数。对于a类消息可以调用如下函数:

    //消息事件
    enum Enum_MessageType :char
    {
        EN_Accept = 0,
        EN_Read,
        EN_Close,
        EN_Connect
    };
    
    //返回的数据结构
    class SocketMessage
    {
    public:
        SOCKET         Socket;
        Enum_MessageType MessageType;
        //当MessageType为EN_Connect时,BufferLen为EasyIocpLib_Connect函数的tag参数
        INT32 BufferLen;
        char *Buffer;
    };
    
    //不停的调用此函数,返回数据
    SocketMessage* EasyIocpLib_GetMessage(UINT64 handle);

    对于b类,就是发送数据。当调用发送时,数据被放到库的发送缓冲中,函数里面返回。接口如下:

    enum EN_SEND_BUFFER_RESULT
    {
        en_send_buffer_ok = 0, //放入到发送缓冲
        en_not_validate_socket, //无效的socket句柄
        en_send_buffer_full     //发送缓冲区满
    };
    
    EN_SEND_BUFFER_RESULT EasyIocpLib_SendMessage(UINT64 handle, SOCKET socket,
            char* buffer, int offset, int len, BOOL mustSend = FALSE);

      总的思路是接收时,放到接收缓冲;发送时,放到发送缓冲。外部接口只对内存中数据操作,没有任何阻塞。

          2)具有广泛的适应性

           如果网络库可以用到各种场景,所处理的逻辑必须与业务无关。所以本库接收和发送的都是字节流。包协议一般有长度指示或有开始结束符。需要把字节流分成一个个完整的数据包。这就与业务逻辑有关了。所以要有分层处理思想:

    库性能测试

      首先对库的性能做测试,使大家对库的性能有初步印象。这些测试都不是很严格,大体能反映程序的性能。IOCP是可扩展的,就是同时处理10个连接与同时处理1000个连接,性能上没有差别。

          我的机器配置不高,cup为酷睿2 双核 E7500,相当于i3低端。

      1)两台机器测试,一个发送,一个接收:带宽占用40M,整体cpu占用10%,程序占用cpu不超过3%。

       2)单台机器,两个程序互发:收发数据达到30M字节,相当于300M带宽,cpu占用大概25%。

      

      3)采用更高性能机器测试,两个程序对发数据:cpu为:i5-7500 CPU @ 3.40GHz

      

       收发数据总和80M字节每秒,接近1G带宽。cpu占用25%。

       测试程序下载地址 :《完成端口(IOCP)性能测试程序(c++版本 64位程序)。只有exe程序,不包括代码。

    网络库设计思路

      服务器要启动监听,当有客户端连接时,生成新的socket句柄;该socket句柄与完成端口关联,后续读写都通过完成端口完成。

    1 socket监听(Accept处理)

      关于监听处理,参考我另一篇文章《单线程实现同时监听多个端口》

    2 数据接收

      收发数据要用到类型OVERLAPPED。需要对该类型进一步扩充,这样当从完成端口返回时,可以获取具体的数据和操作类型。这是处理完成端口一个非常重要的技巧。

    //完成端口操作类型
    typedef enum
    {
        POST_READ_PKG, //
        POST_SEND_PKG, //
        POST_CONNECT_PKG,
        POST_CONNECT_RESULT
    }OPERATION_TYPE;
    
    
    struct PER_IO_OPERATION_DATA
    {
        WSAOVERLAPPED overlap; //第一个变量,必须是操作系统定义的结构
        OPERATION_TYPE opType;
        SOCKET         socket;
        WSABUF         buf;    //要读取或发送的数据
    };

       发送处理:overlap包含要发送的数据。调用此函数会立马返回;当有数据到达时,会有通知。

    BOOL NetServer::PostRcvBuffer(SOCKET socket, PER_IO_OPERATION_DATA *overlap)
    {
        DWORD flags = MSG_PARTIAL;
        DWORD numToRecvd = 0;
    
        overlap->opType = OPERATION_TYPE::POST_READ_PKG;
        overlap->socket = socket;
    
        int ret = WSARecv(socket,
            &overlap->buf,
            1,
            &numToRecvd,
            &flags,
            &(overlap->overlap),
            NULL);
    
        if (ret != 0)
        {
            if (WSAGetLastError() == WSA_IO_PENDING)
            {
                ret = NO_ERROR;
            }
            else
            {
                ret = SOCKET_ERROR;
            }
        }
    
        return (ret == NO_ERROR);
    }

     从完成端口获取读数据事件通知:

    DWORD NetServer::Deal_CompletionRoutine()
    {
        DWORD dwBytesTransferred;
        PER_IO_OPERATION_DATA *lpPerIOData = NULL;
    
        ULONG_PTR    Key;
        BOOL rc;
        int error;
    
        while (m_bServerStart)
        {
            error = NO_ERROR;
            //从完成端口获取事件
            rc = GetQueuedCompletionStatus(
                m_hIocp,
                &dwBytesTransferred,
                &Key,
                (LPOVERLAPPED *)&lpPerIOData,
                INFINITE);
    
            if (rc == FALSE)
            {
                error = 123;
                if (lpPerIOData == NULL)
                {
                    DWORD lastError = GetLastError();
                    if (lastError == WAIT_TIMEOUT)
                    {
                        continue;
                    }
                    else
                    {
                        //continue;
                        //程序结束
                        assert(false);
                        return lastError;
                    }
                }
                else
                {
                    if (GetNetResult(lpPerIOData, dwBytesTransferred) == FALSE)
                    {
                        error = WSAGetLastError();
                    }
                }
            }
            if (lpPerIOData != NULL)
            {
                switch (lpPerIOData->opType)
                {
                    case POST_READ_PKG: //读函数返回
                    {
                        OnIocpReadOver(*lpPerIOData, dwBytesTransferred, error);
                    }
                    break;
    
                    case POST_SEND_PKG:
                    {
                        OnIocpWriteOver(*lpPerIOData, dwBytesTransferred, error);
                    }
                    break;
                }
            }
            
        }
        return 0;
    }
    
    void NetServer::OnIocpReadOver(PER_IO_OPERATION_DATA& opData,
        DWORD nBytesTransfered, DWORD error)
    {
        if (error != NO_ERROR || nBytesTransfered == 0)//socket出错 
        {
            Net_CloseSocket(opData.socket);
            NetPool::PutIocpData(&opData);//数据缓冲处理
        }
        else
        {
            OnRcvBuffer(opData, nBytesTransfered);//处理接收到的数据
            BOOL post = PostRcvBuffer(opData.socket, &opData); //再次读数据
            if (!post)
            {
                Net_CloseSocket(opData.socket);
                NetPool::PutIocpData(&opData);
            }
        }
    }

     3 数据发送

      数据发送时,先放到发送缓冲,再发送。向完成端口投递时,每个连接同时只能有一个正在投递的操作。

    BOOL NetServer::PostSendBuffer(SOCKET socket)
    {
        if (m_clientManage.IsPostSendBuffer(socket)) //如果有正在执行的投递,不能再次投递
            return FALSE;
    
        //获取要发送的数据
        PER_IO_OPERATION_DATA *overlap = NetPool::GetIocpData(FALSE);
        int sendCount = m_clientManage.GetSendBuf(socket, overlap->buf);
        if (sendCount == 0)
        {
            NetPool::PutIocpData(overlap);
            return FALSE;
        }
    
        overlap->socket = socket;
        overlap->opType = POST_SEND_PKG;
        BOOL post = PostSendBuffer(socket, overlap);
        if (!post)
        {
            Net_CloseSocket(socket);
            NetPool::PutIocpData(overlap);
            return FALSE;
        }
        else
        {
            m_clientManage.SetPostSendBuffer(socket, TRUE);
            return TRUE;
        }
    }

     总结:开发一个好的封装库必须有的好的思路。对复杂问题要学会分解,每个模块功能合理,适应性要强;要有模块化、层次化处理思路。如果网络库也处理业务逻辑,处理具体包协议,它就无法做到通用性。一个通用性好的库,才值得我们花费大气力去做好。我设计的这个库,用在了公司多个系统上;以后无论遇到任何网络协议,这个库都可以用得上,一劳永逸的解决网络库封装问题。

  • 相关阅读:
    Delphi XE4 FireMonkey 开发 IOS APP 发布到 AppStore 最后一步.
    Native iOS Control Delphi XE4
    Delphi XE4 iAD Framework 支持.
    using IOS API with Delphi XE4
    GoF23种设计模式之行为型模式之命令模式
    Android青翼蝠王之ContentProvider
    Android白眉鹰王之BroadcastReceiver
    Android倚天剑之Notification之亮剑IOS
    Android紫衫龙王之Activity
    GoF23种设计模式之行为型模式之访问者模式
  • 原文地址:https://www.cnblogs.com/yuanchenhui/p/iocp_windows.html
Copyright © 2011-2022 走看看