评论

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

原标题:一款.NET中高性能、高可用性Socket通讯库

转自:源之缘

cnblogs.com/yuanchenhui/p/iocpcore.html

前言

本人从事编程开发十余年,因为工作关系,很早就接触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的潜质;可以处理字节流也可以处理一个完整的包。

对外接口:

publicclassSocketEventParam

{

publicEN_SocketEvent SocketEvent;

publicSocketClientInfo ClientInfo;

publicSocket Socket;

publicbyte[] Data { get; set; }

publicSocketEventParam(EN_SocketEvent socketEvent, Socket socket)

{

SocketEvent = socketEvent;

Socket = socket;

}

}

publicenumEN_SocketEvent

{

connect,

accept,

close,

read,

send,

packetLenError

}

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

使用举例

NetServer _netServer;

_netServer = newNetServer( this, 100);

_netServer.OnSocketPacketEvent += SocketPacketEvent;

_netServer.AddListenPort( 5668, 1);

privatevoidDealPacket(SocketEventParam socketParam)

{

if(socketParam.SocketEvent == EN_SocketEvent.read)

{

}

elseif(socketParam.SocketEvent == EN_SocketEvent.accept)

{

}

elseif(socketParam.SocketEvent == EN_SocketEvent.close)

{

}

}

内部处理及优化说明

1、可以应对突发大数据量连接

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

intMaxAcceptInPool = 20;

privatevoidDealNewAccept()

{

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、接收优化

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

privatevoidReceiveEventArgs_Completed(objectsender, SocketAsyncEventArgs readArgs)

{

try

{

boolreadError = 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);

}

}

internalintTryReadData()

{

intreadCount = 0;

while( true)

{

EN_SocketReadResult result = ReadNextData;

if(result != EN_SocketReadResult.ReadError)

readCount++;

if(result == EN_SocketReadResult.HaveRead)

continue;

else

{

break;

}

}

ProcessReadData;

returnreadCount;

}

3、发送优化

发送时,将数据先放到发送缓冲。在对多个可发送数据,一次性发送。

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

EasyNetMessage模块介绍

1、对外接口

publicenumEasyNetEvent

{

connect,

accept,

close,

read,

send,

connectError = 100,

}

publicclassEasyNetParam

{

publicEasyNetEvent NetEvent { get; set; }

publicSocketClientInfo ClientInfo { get; set; }

publicSocket Socket { get; set; }

publicNetPacket Packet { get; set; }

}

这个接口和IocpCore有些类似。主要的区别是 public NetPacket Packet { get; set;}。

NetPacket包含的不再是字节流,而是封装好的类。用户不必再处理容易出错的字节流。当然,客户端和服务器都必须使用EasyNetMessage才可以。

NetPacket使用说明

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

--->发送端

NetPacket netPacket = newNetPacket;

netPacket.AddInt( "packetType", 1); //包类型

netPacket.AddString( "fileName", fileName); //文件名字

netPacket.AddInt( "sendIndex", fileIndex); //文件块序列号

netPacket.AddInt( "sendOver", 0); //是否发送完标志

netPacket.AddBuffer( "fileData", readData); //文件数据

<---接收端

privatevoidDealFileRcv(NetPacket netPacket)

{

FileRcvInfo info = newFileRcvInfo;

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详细定义

publicclassNetPacket

{

publicNetPacket();

publicList<NetValuePair> Items { get; set; }

publicintParam1 { get; set; }

publicintPacketType { get; set; }

publicintParam2 { get; set; }

publicvoidAddBuffer(stringkey, byte[] value);

publicvoidAddByte(stringkey, bytevalue);

publicvoidAddInt(stringkey, intvalue);

publicvoidAddListInt(stringkey, List<int> value);

publicvoidAddListLong(stringkey, List<long> value);

publicvoidAddListString(stringkey, List<string> listValue);

publicvoidAddLong(stringkey, longvalue);

publicvoidAddString(stringkey, stringname);

publicList<KeyBuffer> GetAllBuffer();

publicList<KeyString> GetAllString();

publicbyte[] GetBuffer(stringkey);

publicList< byte[]> GetBufferOfSameKey( stringkey);

publicbyte? GetByte( stringkey);

publicList<byte> GetByteOfSameKey(stringkey);

publicint? GetInt( stringkey);

publicList<int> GetIntOfSameKey(stringkey);

publicList<int> GetListInt(stringkey);

publicList<long> GetListLong(stringkey);

publicList<string> GetListString(stringkey, intstartIndex = 0);

publiclong? GetLong( stringkey);

publicList<long> GetLongOfSameKey(stringkey);

publicstringGetString(stringkey);

publicList<string> GetStringOfSameKey(stringkey);

}

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

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

版权声明:本文来源于网友收集或网友供稿,仅供学习交流之用,如果有侵权,请转告小编或者留言,本公众号立即删除。

关注公众号DotNet开发跳槽

点在看返回搜狐,查看更多

责任编辑:

平台声明:该文观点仅代表作者本人,搜狐号系信息发布平台,搜狐仅提供信息存储空间服务。
阅读 ()
大家都在看
推荐阅读