高性能、高可用性Socket通讯库介绍 - 采用完成端口、历时多年调优!(附文件传输程序)

前言 本人从事编程开发十余年,因为工作关系,很早就接触socket通讯编程。常言道:人在压力下,才可能出非凡的成果。我从事的几个项目都涉及到通讯,为我研究通讯提供了平台,也带来了动力。处理socket通讯对初学者而言,具有很大的挑战性。我有个梦想:能不能开发一套系统,能很好的实现性能和易用性的统一。高性能socket采用iocp(完成端口)是唯一选择。iocp像一匹烈马,虽然性能优良,但不宜驯服。本套系统为这匹烈马套上了枷锁,让他变得温顺;但是,当你需要他时,又能迸发出强劲的动力。本文就介绍该系统如何实现易用性和高性能的统一。

  此库的特点:高性能与易用性完美统一;全部自主编码,反复测试,尽最大程度做到了bug free。 

  基于本文介绍的网络模块,开发的文件快速传输系统:

  如果你的系统需要高性能网络通信,可以联系我。根据你的系统特点定制开发。

  点击下载执行程序   

系统简介

1 系统采用c#,可以在.net core平台编译通过。所以可运行在windows、linux平台。
2 系统有两个模块组成IocpCore,EasyNetMessage。IocpCore对完成端口进行了封装,EasyNetMessage在IocpCore基础上进一步封装,实现了易用性。可在EasyNetMessage基础上,进一步扩展,实现分布式系统(类似WCF)。
3 系统只实现了TCP通讯,秉承simple is best的理念,不为过于冗余的功能干扰。
4 系统突出专业性(professional)。为了测试稳定性,开发了专门的测试程序,反复对系统蹂躏,检验系统的稳定性。为了测试性能,做了精确计时,检验每个功能点的效率。
网上也有很多第三方网络库,好像没有必要再另起炉灶。但这些库大部分无法满足专业性、易用性要求。通过对系统API封装,可以完全了解底层特性,由于所有代码都是自己亲自编写,做到了心中有数,对所有代码了然于心。即使系统出现bug,也可以很快解决。

性能指标
Iocp是可扩展性通讯模型,就是不随着连接数增加而导致性能下降。所支持的连接数只与平台硬件有关。本系统保守估计可以支持10万个连接。普通平台下,可以满足千兆网传输需求。

设计思路
  如果网络库可以用到各种场景,所处理的逻辑必须与业务无关。系统采用分层处理,底层处理字节流的收发,完全与业务无关。底层的目标就是收发速度足够快。再上一层,就是对完整的数据包处理,处理的关键是如何将数据流分割成完整的数据包。再向上就是应用层,将收到的数据包转换成类,上层只需对c#类处理,不用关心底层细节。

IocpCore 模块介绍
本模块对iocp封装,充分挖掘iocp的潜质;可以处理字节流也可以处理一个完整的包。
对外接口:

 public class SocketEventParam
    {
        public EN_SocketEvent SocketEvent;
        public SocketClientInfo ClientInfo;

        public Socket Socket;
        public byte[] Data { get; set; }

        public SocketEventParam(EN_SocketEvent socketEvent, Socket socket)
        {
            SocketEvent = socketEvent;
            Socket = socket;
        }
    }
 public enum EN_SocketEvent
    {
        connect,
        accept,
        close,
        read,
        send,
        packetLenError
    }

程序接口非常简单就只有一个类。这个类对socket事件做了封装,就是告诉你socket 连接、关闭、读取这些事件。不需要关心任何底层的细节,所以使用起来非常简单。

使用举例

  NetServer _netServer;
  _netServer = new NetServer(this, 100);
  _netServer.OnSocketPacketEvent += SocketPacketEvent;
  _netServer.AddListenPort(5668, 1);
  
  private void DealPacket(SocketEventParam socketParam)
    {
        if (socketParam.SocketEvent == EN_SocketEvent.read)
        {        
        }
        else if (socketParam.SocketEvent == EN_SocketEvent.accept)
        {         
        }
        else if (socketParam.SocketEvent == EN_SocketEvent.close)
        {   
        }
    }
    

内部处理及优化说明

1 可以应对突发大数据量连接
    每秒可以起送应对几千个客户端连接。接收对方监听采用AcceptAsync,也是异步操作。有单独的线程负责处理Accept。

     int MaxAcceptInPool = 20;
        private void DealNewAccept()
        {
            try
            {
                if (_acceptAsyncCount <= MaxAcceptInPool)
                {
                    StartAccept();
                }

                while (true)
                {
                    AsyncSocketClient client = _newSocketClientList.GetObj();
                    if (client == null)
                        break;

                    DealNewAccept(client);
                }
            }
            catch (Exception ex)
            {
                _log.LogException(0, "DealNewAccept 异常", ex);
            }
        }

线程会同时投递多个AcceptAsync,就是已经建立好多个socket,等待客户端连接。当客户端到达时,可以迅速生成可用socket。

2 接收优化

当收到接收完成消息后,立即投递下一次接收操作,再处理接收的数据。这样可以提高数据处理的实时性。

     private void ReceiveEventArgs_Completed(object sender, SocketAsyncEventArgs readArgs)
        {
            try
            {
                bool readError = false;
                lock (_readLock)
                {
                    _inReadPending = false;

                    if (readArgs.BytesTransferred > 0
                            && readArgs.SocketError == SocketError.Success)
                    {
                      //加入到缓冲中
                        AddToReadList(readArgs.BufferList, readArgs.BytesTransferred);
                        readArgs.BufferList = null;
                    }
                    else
                    {
                        readError = true;
                    }
                }

                if (IsSocketError || readError)
                {
                    OnReadError();
                }
                else
                {
                    TryReadData();
                }
            }
            catch (Exception ex)
            {
                _log.LogException(0, "ReceiveEventArgs_Completed", ex);
            }
        }
        
         internal int TryReadData()
        {
            int readCount = 0;
            while (true)
            {
                EN_SocketReadResult result = ReadNextData();
                if (result != EN_SocketReadResult.ReadError)
                    readCount++;

                if (result == EN_SocketReadResult.HaveRead)
                    continue;
                else
                {
                    break;
                }
            }
            ProcessReadData();
            return readCount;
        }

 3 发送优化

 发送时,将数据先放到发送缓冲。在对多个可发送数据,一次性发送。SocketAsyncEventArgs类中有属性public IList<ArraySegment<byte>> BufferList { get; set; },可以将多个发送buffer放入该列表,一次性发送走。

 EasyNetMessage模块介绍

1 对外接口

 public enum EasyNetEvent
    {
        connect,
        accept,
        close,
        read,
        send,
        connectError = 100,
    }

    public class EasyNetParam
    {
        public EasyNetEvent NetEvent { get; set; }
        public SocketClientInfo ClientInfo { get; set; }
        public Socket Socket { get; set; }
        public NetPacket Packet { get; set; }
    }

  这个接口和IocpCore有些类似。主要的区别是 public NetPacket Packet { get; set; }。NetPacket包含的不再是字节流,而是封装好的类。用户不必再处理容易出错的字节流。当然,客户端和服务器都必须使用EasyNetMessage才可以。

NetPacket使用说明

 客户端和服务器之间传输的是NetPacket类,完全忽略底层细节。客户端构造一个NetPacket,在服务端会收到一个完全一样的NetPacket。以发送文件为例:

   --->发送端
    NetPacket  netPacket = new NetPacket();
      netPacket.AddInt("packetType", 1);        //包类型
      netPacket.AddString("fileName", fileName);//文件名字
      netPacket.AddInt("sendIndex", fileIndex); //文件块序列号
      netPacket.AddInt("sendOver", 0);          //是否发送完标志

      netPacket.AddBuffer("fileData", readData);//文件数据
      <---接收端
       private void DealFileRcv(NetPacket netPacket)
        {
            FileRcvInfo info = new FileRcvInfo();
            info.IsSendOver = netPacket.GetInt("sendOver") == 1;
            info.FileName = netPacket.GetString("fileName");
            info.SendIndex = netPacket.GetInt("sendIndex").Value;
            info.FileData = netPacket.GetBuffer("fileData");
           }
        }

 以上只是使用NetPacket一个简单的例子。使用EasyNetMessage,短时间内可以开发出一个高性能的文件传输系统。

 NetPacket详细定义

  public class NetPacket
    {
        public NetPacket();

        public List<NetValuePair> Items { get; set; }
        public int Param1 { get; set; }
        public int PacketType { get; set; }
        public int Param2 { get; set; }

        public void AddBuffer(string key, byte[] value);
        public void AddByte(string key, byte value);
        public void AddInt(string key, int value);
        public void AddListInt(string key, List<int> value);
        public void AddListLong(string key, List<long> value);
        public void AddListString(string key, List<string> listValue);
        public void AddLong(string key, long value);
        public void AddString(string key, string name);
        
        public List<KeyBuffer> GetAllBuffer();
        public List<KeyString> GetAllString();
        public byte[] GetBuffer(string key);
        public List<byte[]> GetBufferOfSameKey(string key);
        public byte? GetByte(string key);
        public List<byte> GetByteOfSameKey(string key);
        public int? GetInt(string key);
        public List<int> GetIntOfSameKey(string key);
        public List<int> GetListInt(string key);
        public List<long> GetListLong(string key);
        public List<string> GetListString(string key, int startIndex = 0);
        public long? GetLong(string key);
        public List<long> GetLongOfSameKey(string key);
        public string GetString(string key);
        public List<string> GetStringOfSameKey(string key);
    }

NetPacket中数据采用key、value的方式存储。可以存储int,string,List<int>,List<string>,byte[]等类型,可以满足多种应用场景。
处理逻辑说明
  处理的重点是NetPacket的序列化和反序列化。将NetPacket序列化为多个内存块,而不是序列化为一个单独的内存块。这样做意义就是:当NetPacket包含大量的数据(比如几百兆),如果只序列化为一个内存块,则需要系统分配连续的几百兆内存,这样很可能导致分配失败;序列化为多个小内存块就可以防止这种问题,所以NetPacket一次可以传输大量数据,而不用担心系统是否可以分配连续的大内存块。

性能验证测试

  前文剖析了系统内部处理逻辑,系统的性能还需要现实检验。任何成功都不是一蹴而就,为了追求性能的极致,对系统做了多次优化,才达到了满意的效果。
  系统的性能有两个指标:传输量、响应时间。响应时间指的是:数据发送到对端,再从对端返回的时长。传输量、响应时间这两个指标有关联,而又不完全一样。很多系统传输量大非常大,但是响应不够及时。响应及时是开发远程过程调用的基础,是更高一个层次的要求。这里主要测试响应时间。

 主要测试数据发送到对方,再从对方返回数据所用时长。因为条件所限,客户端与服务器都在同一台机器上。

 测试平台: i5第4代cpu;

1) 50个字节数据发送

  

平均时间小于1毫秒,也就是说每秒可以执行1000次函数调用。

2)1K字节数据收发

 

 和50字节调用差别不大。

3)100K 字节数据收发

响应时间大概为12毫秒,每秒可以执行80次调用。

4)10000K字节数据收发 (接近10M数据)

时间刚超过1秒。这是10M数据发送,再接收的时间。相当于占用200M带宽。

响应时间测试总结小数据量,基本可以达到每秒1000次函数调用。10M的数据收发刚刚超过1秒。注意这还不能完全反应网络层处理的能力。因为这是单个线程调用,如果多个线程同时调用,可以达到更高的调用次数。

传输量测试

我使用c++写的模拟程序,对该系统测试。收发数据总计超过50M,暨占用500M带宽,cpu占用率23%。测试平台为笔记本,硬件配置比较低。如果采用高性能服务器,达到千兆带宽传输,cpu占用也不会很高。

总结笔者从事软件开发多年,对于socket通讯编程非常有经验。 一个好的通讯模块有很多指标,比如:复用性高、耦合性低、性能高、易用性好;本系统在设计时就综合考虑了这些要求。对于如何设计好通讯层,我进行了很多思考,将其付诸于代码;公司的多款产品通讯层就是采用该系统,该系统经过了实践的检验,完全满足了多个产品的要求。

  当然,一款产品在任何条件都是最优的,这很难做到。网络层亦是如此。根据上层数据收发的特点,来调整网络层的一些配置参数,这样才能达到最优。如果你公司的产品需要高性能网络层做支持,可以联系我。我会根据产品的特点做优化。

posted @ 2019-03-03 09:33  源之缘-OFD先行者  阅读(6285)  评论(0编辑  收藏  举报
关注我