一款.NET中高性能、高可用性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#类处理,不用关心底层细节。

一款.NET中高性能、高可用性Socket通讯库

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一次可以传输大量数据,而不用担心系统是否可以分配连续的大内存块。

一款.NET中高性能、高可用性Socket通讯库

性能验证测试

前文剖析了系统内部处理逻辑,系统的性能还需要现实检验。任何成功都不是一蹴而就,为了追求性能的极致,对系统做了多次优化,才达到了满意的效果。

系统的性能有两个指标:传输量、响应时间。响应时间指的是:数据发送到对端,再从对端返回的时长。传输量、响应时间这两个指标有关联,而又不完全一样。很多系统传输量大非常大,但是响应不够及时。响应及时是开发远程过程调用的基础,是更高一个层次的要求。这里主要测试响应时间。

一款.NET中高性能、高可用性Socket通讯库

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

测试平台:i5第4代cpu;

一款.NET中高性能、高可用性Socket通讯库

1)50个字节数据发送

一款.NET中高性能、高可用性Socket通讯库

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

2)1K字节数据收发

一款.NET中高性能、高可用性Socket通讯库

和50字节调用差别不大。

3)100K 字节数据收发

一款.NET中高性能、高可用性Socket通讯库

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

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

一款.NET中高性能、高可用性Socket通讯库

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

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

传输量测试

一款.NET中高性能、高可用性Socket通讯库

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

总结

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

当然,一款产品在任何条件都是最优的,这很难做到。网络层亦是如此。根据上层数据收发的特点,来调整网络层的一些配置参数,这样才能达到最优。