高性能、高可用性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通讯编程非常有经验。 一个好的通讯模块有很多指标,比如:复用性高、耦合性低、性能高、易用性好;本系统在设计时就综合考虑了这些要求。对于如何设计好通讯层,我进行了很多思考,将其付诸于代码;公司的多款产品通讯层就是采用该系统,该系统经过了实践的检验,完全满足了多个产品的要求。
当然,一款产品在任何条件都是最优的,这很难做到。网络层亦是如此。根据上层数据收发的特点,来调整网络层的一些配置参数,这样才能达到最优。如果你公司的产品需要高性能网络层做支持,可以联系我。我会根据产品的特点做优化。