如果ESFramework对您的项目来说,太庞大、太重量级;如果您的项目不需要P2P、不需要传文件、不需要群集等功能,只需要简单的TCP/UDP通信。那么,可以考虑使用轻量级的通信引擎StriveEngine。相比较而言,StriveEngine更单纯、更容易上手,也更容易与已存在的遗留系统进行协作。
一.StriveEngine 主要特性
1.底层采用高效的IOCP(完成端口)模型。
2.内部自动管理可复用的线程池、以及内存池。
3.内置多种通信引擎类型:TCP/UDP、文本协议/二进制协议、服务端/客户端。而且,所有这些引擎的使用方式一致。
4.解决了TCP通信中的粘包以及消息重组问题。
5.发送消息支持同步、异步两种方式。
6.服务端引擎支持异步消息队列模式。
7.客户端TCP引擎支持断线自动重连。
8.支持Sock5代理。
9.兼容IPv6。
二.StriveEngine与ESFramework/ESPlus/ESPlatform的区别
1.ESFramework 是一个功能强大的通信框架/平台,而StriveEngine是一个单纯高效的通信引擎类库。
2.ESFramework 更贴近应用层,而StriveEngine更贴近Socket层。
3.ESFramework 使用UserID标志每一个在线的客户端,而StriveEngine则使用低阶的IPEndPoint来标志每一个在线的客户端。
4.可以认为StriveEngine就是ESFramework底层使用的内核。
5.ESFramework 定义了底层通信消息的格式(对ESFramework使用者是透明的),而StriveEngine对通信消息的格式未作任何定义。
6.ESFramework和StriveEngine都提供了服务端引擎和客户端引擎。当涉及到要开发新的其它平台引擎来与ESFramework或StriveEngine协作时,比如:
(1)如果要开发其它平台的客户端引擎与ESFramework的服务端引擎协作,则必需实现ESPlus协议格式;
(2)如果要开发其它平台的客户端引擎与StriveEngine的服务端引擎协作,由于StriveEngine未定义任何消息格式,所以不存在要必需实现既有协议格式的问题。
(3)与(2)同理,使用 StriveEngine的客户端引擎,可以与任何其它现有的服务端协作。
7.对于那些涉及到在线用户管理、以UserID为中心的应用(比如即时通信应用),或者需要P2P通信、传文件等功能的应用来说,ESFramework更适合;
对于那些仅仅需要简单而高效的通信功能的应用(如数据采集、消息转发等)来说,StriveEngine更合适。
三.StriveEngine 整体结构
StriveEngine内置了多种通信引擎,这些通信引擎都以接口的方式暴露了其功能,而不同的引擎却都依据某些共通的部分,继承了相同的基接口。 引擎接口的继承关系如下所示:
INetworkEngine 是所有引擎的根接口,只要是StriveEngine中的内置引擎,都实现了这个接口。
IServerEngine 是服务端引擎接口。IPassiveEngine 是客户端引擎接口。
ITcpServerEngine 是基于TCP的服务端引擎接口。ITcpPassiveEngine是基于TCP的客户端引擎接口。
IUdpEngine 是基于UDP的通信引擎接口。由于UDP是非连接协议,可以认为UDP通信的双方是对等的,所以,服务端和客户端都使用IUdpEngine即可。
ITextEngine 是基于文本协议的通信引擎的基接口。
下面,我们来详细看看每个接口。
1.INetworkEngine
/// <summary> /// 所有网络引擎(包括服务端引擎、客户端引擎、TCP引擎、UDP引擎、二进制引擎、文本引擎)的基础接口。 /// </summary> public interface INetworkEngine : IDisposable { /// <summary> /// 传输层协议类型,TCP或UDP /// </summary> ProtocolType ProtocolType { get; } /// <summary> /// 引擎实例的创建时间。 /// </summary> DateTime CreateTime { get; } /// <summary> /// 表示要监听本地的哪个端口。如果设定其值小于等于0,则表示由系统自动分配。 /// 注意,引擎初始化完成后,不能再修改该属性。 /// </summary> int Port { get; set; } /// <summary> /// 对于服务端引擎,表示要绑定本地的哪个IP,如果设为null,则表示绑定本地所有IP。 /// 对于客户端引擎,表示要绑定本地的哪个IP与服务器进行通信。如果设为null(其值会在初始化完成后被修改),则自动选择地址列表中的某一个。 /// 注意,引擎初始化完成后,不能再修改该属性。 /// </summary> string IPAddress { get; set; } /// <summary> /// 设置日志记录的文件路径。如果设为null,表示不记录日志。默认值为null。 /// </summary> string LogFilePath { set; } /// <summary> /// Socket(网卡)发送缓冲区的大小。默认为8k。 /// </summary> int SocketSendBuffSize { get; set; } /// <summary> /// Socket(网卡)接收缓冲区的大小。默认为8k。 /// </summary> int SocketReceiveBuffSize { get; set; } /// <summary> /// 网络引擎能够接收的最大的消息尺寸。据此网络引擎可以为每个Session/Connection开辟适当大小的接收缓冲区。 /// 默认为1k。当接收到的消息尺寸超过MaxMessageSize时,将会关闭对应的连接(对TCP)或丢弃数据(对UDP)。 /// </summary> int MaxMessageSize { get; set; } /// <summary> /// 引擎实例是否已经被释放。 /// </summary> bool Disposed { get; } /// <summary> /// 初始化并启动网络引擎。如果修改了引擎配置参数,在应用新参数之前必须先重新调用该方法初始化引擎。 /// </summary> void Initialize(); /// <summary> /// 当不再使用当前引擎实例时,释放它。(异步方式) /// 注意:对于UDP或客户端引擎,如果消息是同步处理的(HandleMessageAsynchronismly为false),请不要在消息处理器中调用Dispose方法,否则,会导致死锁。因为停止引擎要等到最后一条消息处理完毕才会返回。 /// 可以转向调用异步的DisposeAsyn方法。 /// </summary> void DisposeAsyn(); /// <summary> /// 当引擎实例被释放时,触发此事件。 /// </summary> event CbDelegate<INetworkEngine> EngineDisposed; /// <summary> /// 接收到一个完整的消息时触发该事件。 /// </summary> event CbDelegate<IPEndPoint,byte[]> MessageReceived; /// <summary> /// 将消息成功发送之后触发该事件 /// </summary> event CbDelegate<IPEndPoint, byte[]> MessageSent; } /// <summary> /// 传输层协议类型。 /// </summary> public enum ProtocolType { TCP = 0, UDP } /// <summary> /// 协议的格式。 /// </summary> public enum ContractFormatStyle { /// <summary> /// 流协议或称二进制协议。 /// </summary> Stream = 0, /// <summary> /// 文本协议,如基于xml的。 /// </summary> Text }
(1)StriveEngine中的所有通信引擎在设置完必要的属性后,都必须调用Initialize方法进行初始化,初始化完成后,引擎实例才开始正常工作。
(2)INetworkEngine从IDisposable继承,表明通信引擎内部持有了重要的资源,当不再使用其实例时,要尽快调用IDisposable的Dispose方法释放资源。
(3)当通信引擎被释放后,会触发EngineDisposed事件,并且此后,Disposed属性将返回true。
(4)请根据应用的需要谨慎地设置MaxMessageSize,如果设置的过大,可能会造成内存空间的浪费(特别是对于基于文本协议的服务端引擎)。
(5)通过MessageReceived事件,可以得到通信引擎接收到的所有消息;通过MessageSent事件,可以监控通信引擎发送出去的所有消息。
2.IServerEngine
/// <summary> /// 服务端引擎接口。 /// </summary> public interface IServerEngine : INetworkEngine { /// <summary> /// 到目标客户的通道是否繁忙? /// </summary> bool ChannelIsBusy(IPEndPoint client); /// <summary> /// 给某个客户端同步发信息。注意:如果引擎已经停止或客户端不在线,则直接返回。 /// </summary> /// <param name="client">接收数据的客户端</param> /// <param name="msg">消息</param> void SendMessageToClient(IPEndPoint client, byte[] msg); /// <summary> /// 给某个客户端同步发信息。注意:如果引擎已经停止或客户端不在线,则直接返回。 /// </summary> /// <param name="client">接收数据的客户端</param> /// <param name="msg">消息</param> /// <param name="action">当通道繁忙时采取的动作:继续发送或丢弃。</param> void SendMessageToClient(IPEndPoint client, byte[] msg, ActionTypeOnChannelIsBusy action); /// <summary> /// 给某个客户端异步发信息。注意:如果引擎已经停止或客户端不在线,则直接返回。 /// </summary> /// <param name="client">接收数据的客户端</param> /// <param name="msg">消息</param> void PostMessageToClient(IPEndPoint client, byte[] msg); /// <summary> /// 给某个客户端异步发信息。注意:如果引擎已经停止或客户端不在线,则直接返回。 /// </summary> /// <param name="client">接收数据的客户端</param> /// <param name="msg">消息</param> /// <param name="action">当通道繁忙时采取的动作:继续发送或丢弃。</param> void PostMessageToClient(IPEndPoint client, byte[] msg, ActionTypeOnChannelIsBusy action); }
(1)SendMessageToClient和PostMessageToClient 分别表示同步和异步发送消息给客户端。
(2)ChannelIsBusy 指的是在与目标客户端的TCP连接上,是否有数据正在发送(服务端至客户端)。
(3)ActionTypeOnChannelIsBusy 参数允许我们在通道繁忙时,丢弃不重要的消息。
3.IPassiveEngine
/// <summary> /// 客户端引擎接口。 /// </summary> public interface IPassiveEngine :INetworkEngine { /// <summary> /// 服务器地址。 /// </summary> AgileIPE ServerIPEndPoint { get; set; } /// <summary> /// 发送消息的通道是否正忙。使用者可以根据该属性决定是否要丢弃后续某些消息的发送。 /// </summary> bool ChannelIsBusy { get; } /// <summary> /// 将消息异步发送给服务器,不经任何处理,直接发送。注意:如果引擎已经停止,则直接返回。 /// </summary> /// <param name="msg">要发送的消息</param> /// <param name="action">当通道繁忙时采取的动作:继续发送或丢弃。</param> void SendMessageToServer(byte[] msg, ActionTypeOnChannelIsBusy action); /// <summary> /// 将消息异步发送给服务器,不经任何处理,直接发送。注意:如果引擎已经停止,则直接返回。 /// </summary> /// <param name="msg">要发送的消息</param> void SendMessageToServer(byte[] msg); /// <summary> /// 将消息同步发送给服务器,不经任何处理,直接发送。注意:如果引擎已经停止,则直接返回。 /// </summary> /// <param name="msg">要发送的消息</param> /// <param name="action">当通道繁忙时采取的动作:继续发送或丢弃。</param> void PostMessageToServer(byte[] msg, ActionTypeOnChannelIsBusy action); /// <summary> /// 将消息同步发送给服务器,不经任何处理,直接发送。注意:如果引擎已经停止,则直接返回。 /// </summary> /// <param name="msg">要发送的消息</param> void PostMessageToServer(byte[] msg); }
(1)SendMessageToServer和PostMessageToServer 分别表示同步和异步发送消息给服务端。
(2)ChannelIsBusy 指的是当前与服务器的TCP连接上,是否有数据正在发送(客户端至服务端)。
(3)ActionTypeOnChannelIsBusy 参数允许我们在通道繁忙时,丢弃不重要的消息。
4.ITcpServerEngine
/// <summary> /// TCP服务端引擎接口。 /// </summary> public interface ITcpServerEngine : IServerEngine { /// <summary> /// 引擎所采用的协议类型(二进制、文本)。 /// </summary> ContractFormatStyle ContractFormatStyle { get; } /// <summary> /// 当前在线客户端的数量。 /// </summary> int ClientCount { get; } /// <summary> /// 服务器允许最大的同时在线客户端数。 /// </summary> int MaxClientCount { get;} /// <summary> /// 给每个连接发送数据超时时间(单位:毫秒。默认为-1,表示无限)。如果在指定的时间内未将数据发送完,则关闭对应的连接。 /// </summary> int WriteTimeoutInMSecs { get; set; }/// <summary> /// 每个通道连接上允许最大的等待发送【包括投递】以及正在发送的消息个数。 /// 当等待发送以及正在发送的消息个数超过该值时,将关闭对应的连接。如果设置为0,则表示不作限制。默认值为0。 /// </summary> int MaxChannelCacheSize { get; set; } /// <summary> /// 监听器是否开启。 /// </summary> bool IsListening { get; } /// <summary> /// 关闭到某个客户端的连接,将触发SomeOneDisconnected事件。 /// </summary> void CloseClient(IPEndPoint client); /// <summary> /// 关闭所有客户端的连接。 /// </summary> void CloseAllClient(); /// <summary> /// 关闭或开启监听器。该方法调用不影响网络引擎的消息接收和处理。 /// </summary> void ChangeListenerState(bool enabled); /// <summary> /// 获取所有在线连接的客户端的地址。 /// </summary> List<IPEndPoint> GetClientList(); /// <summary> /// 客户端是否在线? /// </summary> bool IsClientOnline(IPEndPoint client); /// <summary> /// 当某TCP连接断开时,触发该事件 /// </summary> event CbDelegate<IPEndPoint> ClientDisconnected; /// <summary> /// 当某TCP连接建立成功时,触发此事件。 /// </summary> event CbDelegate<IPEndPoint> ClientConnected; /// <summary> /// 当tcp连接数量发生变化时,触发此事件。 /// </summary> event CbDelegate<int> ClientCountChanged; /// <summary> /// 当连接监听器的状态发生变化时,触发此事件。事件参数为true,表明连接监听器启动;事件参数为false,表明连接监听器已停止。 /// </summary> event CbDelegate<bool> ListenerStateChanged; }
(1)WriteTimeoutInMSecs 用于设置发送数据的超时。 最好给该属性赋一个适当的值,因为在某些情况下,发送数据可能会导致很长时间的阻塞。该属性只对同步发送有效。
(2)MaxChannelCacheSize 是服务端的一个安全设置。该设置用于防止服务器为速度慢的通道缓存太多的消息,而导致服务器内存无限制增长。
(3)ChangeListenerState 用于改变服务器的监听状态,其将触发ListenerStateChanged事件,并改变IsListening属性的值。
如果IsListening为false,表示当前不接受新的TCP连接请求。
(4)当有连接建立或断开时,将分别触发ClientConnected和ClientDisconnected事件。
5.ITcpPassiveEngine
/// <summary> /// 客户端TCP引擎接口。 /// </summary> public interface ITcpPassiveEngine :IPassiveEngine { /// <summary> /// 引擎所采用的协议类型(二进制、文本)。 /// </summary> ContractFormatStyle ContractFormatStyle { get; } /// <summary> /// 当客户端与服务器的TCP连接断开时,将触发此事件。 /// </summary> event CbDelegate ConnectionInterrupted; /// <summary> /// 自动重连开始时,触发此事件。 /// </summary> event CbDelegate ConnectionRebuildStart; /// <summary> /// 自动重连成功后,触发此事件。 /// </summary> event CbDelegate ConnectionRebuildSucceed; /// <summary> /// 自动重连超过最大重试次数时,表明重连失败,将触发此事件。 /// </summary> event CbDelegate ConnectionRebuildFailure; /// <summary> /// Sock5代理服务器信息。如果不需要代理,则设置为null。 /// </summary> Sock5ProxyInfo Sock5ProxyInfo { get; set; } /// <summary> /// 当前是否处于连接状态。 /// </summary> bool Connected { get; } /// <summary> /// 当与服务器断开连接时,是否自动重连。 /// </summary> bool AutoReconnect { get; set; } /// <summary> /// 当连接断开时,自动重连尝试的最大次数。默认值为int.MaxValue。 /// </summary> int MaxRetryCount4AutoReconnect { get; set; } /// <summary> /// 主动关闭与服务器的连接。如果AutoReconnect为true,将引发自动重连。 /// </summary> void CloseConnection(); /// <summary> /// 手动重连。如果当前处于连接状态,则直接返回。 /// </summary> /// <param name="retryCount">重试次数</param> /// <param name="retrySpanInMSecs">重试间隔时间,毫秒</param> void Reconnect(int retryCount, int retrySpanInMSecs); }
(1)如果AutoReconnect设置为true,表示启用自动重连,那么,当连接断开时,会按以下顺序触发相关事件: ConnectionInterrupted 、ConnectionRebuildStart 、 ConnectionRebuildSucceed/ConnectionRebuildFailure。
(2)注意,如果AutoReconnect设置为true,CloseConnection将会先关闭当前连接,然后再启动自动重连。
6.IUdpEngine
/// <summary> /// Udp引擎基础接口。 /// </summary> public interface IUdpEngine :INetworkEngine { /// <summary> /// 向指定的端点发送UDP消息。注意:如果引擎已经停止,则直接返回。 /// </summary> /// <param name="message">要发送的消息</param> /// <param name="address">目标端点</param> void SendMessage(IPEndPoint address, byte[] message); /// <summary> /// 向指定的端点投递UDP消息。注意:如果引擎已经停止,则直接返回。 /// </summary> /// <param name="message">要投递的消息</param> /// <param name="address">目标端点</param> void PostMessage(IPEndPoint address, byte[] message); }
UDP是非连接的协议,所以,UDP引擎不用区分客户端和服务端,或者说,无论是客户端还是服务端,都可以使用IUdpEngine。注意:IUdpEngine也从INetworkEngine继承,所以,它具备了StriveEngine中基础引擎所有的功能。
四.如何使用StriveEngine
在StriveEngine中,我们不能直接new某个通信引擎的class来获得其实例。StriveEngine提供了NetworkEngineFactory,我们可以通过工厂的静态方法来得到通信引擎实例的引用。
1.通信引擎工厂
/// <summary> /// 通信引擎工厂。 /// </summary> public static class NetworkEngineFactory { /// <summary> /// 创建使用二进制协议的TCP服务端引擎。对于返回的引擎实例,可以设置其更多属性,然后调用其Initialize方法启动引擎。 /// </summary> /// <param name="port">服务端引擎监听的端口号</param> /// <param name="helper">二进制协议助手接口</param> public static ITcpServerEngine CreateStreamTcpServerEngine(int port, IStreamContractHelper helper); /// <summary> /// 创建使用文本协议的TCP服务端引擎。对于返回的引擎实例,可以设置其更多属性,然后调用其Initialize方法启动引擎。 /// 注意:返回的引擎实例,可以强转为ITextEngine接口。 /// </summary> /// <param name="port">服务端引擎监听的端口号</param> /// <param name="helper">文本协议助手接口</param> public static ITcpServerEngine CreateTextTcpServerEngine(int port, ITextContractHelper helper); /// <summary> /// 创建使用二进制协议的TCP客户端引擎。对于返回的引擎实例,可以设置其更多属性,然后调用其Initialize方法启动引擎。 /// </summary> /// <param name="serverIP">要连接的服务器的IP</param> /// <param name="serverPort">要连接的服务器的端口</param> /// <param name="helper">二进制协议助手接口</param> public static ITcpPassiveEngine CreateStreamTcpPassivEngine(string serverIP, int serverPort, IStreamContractHelper helper); /// <summary> /// 创建使用文本协议的TCP客户端引擎。对于返回的引擎实例,可以设置其更多属性,然后调用其Initialize方法启动引擎。 /// 注意:返回的引擎实例,可以强转为ITextEngine接口。 /// </summary> /// <param name="serverIP">要连接的服务器的IP</param> /// <param name="serverPort">要连接的服务器的端口</param> /// <param name="helper">文本协议助手接口</param> public static ITcpPassiveEngine CreateTextTcpPassiveEngine(string serverIP, int serverPort, ITextContractHelper helper); /// <summary> /// 创建UDP引擎。对于返回的引擎实例,可以设置其更多属性,然后调用其Initialize方法启动引擎。 /// </summary> public static IUdpEngine CreateUdpEngine(); }
我们可以根据项目的需要(是TCP还是UDP、是文本协议还是二进制协议、是用于客户端还是用于服务端),来调用NetworkEngineFactory的对应方法获得正确的通信引擎实例。
注意:NetworkEngineFactory创建的所有通信引擎实例,必须要调用其Initialize方法后,引擎才算正常启动。当然,在调用Initialize之前,可以根据需要设置其相关的属性。
2.ContractHelper
StriveEngine内部通过ContractHelper来从接收的网络流中识别完整的消息,针对消息格式为文本和二进制,ContractHelper就划分为对应的ITextContractHelper 和IStreamContractHelper。我们看到,在通过NetworkEngineFactory创建通信引擎实例时,其有个参数是必须传入ContractHelper引用的。所以,在项目中,我们必须实现ITextContractHelper或者是IStreamContractHelper。
(1) ITextContractHelper
/// <summary> /// 文本协议助手接口。 /// </summary> public interface ITextContractHelper { /// <summary> /// 消息的结束标识符(经过编码后得到的字节数组)。 /// </summary> byte[] EndToken { get; } }
当使用文本协议时,通常,使用某一个或多个特殊的字符作为消息的结束符(EndToken)。ITextContractHelper的EndToken是一个byte[],其指的是消息的结束符经过编码(比如UTF-8)后得到的字节数组。通信引擎即通过ITextContractHelper提供的EndToken来从网络流中识别完整的消息。
如果是使用UTF-8对文本消息(当然,也包括消息结束符)进行编解码,则可以使用StriveEngine内置的DefaultTextContractHelper,其实,它的实现非常简单:
/// <summary> /// StriveEngine内置的ITextContractHelper实现。使用UTF-8对EndToken进行编码。 /// </summary> public class DefaultTextContractHelper : ITextContractHelper { public DefaultTextContractHelper(string endTokenStr) { this.endToken = System.Text.Encoding.UTF8.GetBytes(endTokenStr); } private byte[] endToken; public byte[] EndToken { get { return this.endToken; } } }
(2)IStreamContractHelper
/// <summary> /// 二进制协议助手接口。 /// </summary> public interface IStreamContractHelper { /// <summary> /// 从消息头中解析出消息体的长度。 /// </summary> /// <param name="head">完整的消息头,长度固定为MessageHeaderLength</param> /// <returns>消息体的长度</returns> int ParseMessageBodyLength(byte[] head); /// <summary> /// 消息头的长度。 /// </summary> int MessageHeaderLength { get; } }
当使用二进制协议时,通常,消息分为消息头(Header)和消息体(Body)两部分,消息头是必须的,而消息体可以为null。消息头的长度是固定的(比如8个字节),且其至少包含了一个字段--消息体的长度(或根据消息头的内容可以间接结算出消息体的长度)。
3.使用StriveEngine的步骤
(1)实现ITextContractHelper或者是IStreamContractHelper接口。
(2)调用NetworkEngineFactory的创建引擎的方法,得到正确的通信引擎实例。
(3)根据需要,设置引擎实例的某些属性(如MaxMessageSize属性)。
(4)根据需要,预定引擎实例的某些事件(如MessageReceived事件)。
(5)调用引擎实例的Initialize方法启动通信引擎。
五.Demo下载
通过上述的内容大致了解了StriveEngine后,接下来我们通过一个简单的demo来看看在实际项目中是如何使用StriveEngine的。
该Demo是一个简单的客户端与服务端进行消息通信的demo,消息使用文本协议。该Demo总共包括三个项目:
(1)StriveEngine.SimpleDemoServer:基于StriveEngine开发的服务端。
(2)StriveEngine.SimpleDemoClient:基于StriveEngine开发的客户端。
(3)StriveEngine.SimpleDemo:直接基于.NET的Socket开发的客户端,其目的是为了演示:在客户端不使用StriveEngine的情况下,如何与基于StriveEngine的服务端进行通信。
Demo运行起来后的截图如下所示:
更多信息请参见:www.oraycn.com