ESFramework 开发手册(01) -- 发送和处理信息
本文介绍ESFramework 开发手册(00) -- 概述一文中提到的四大武器的第一个:发送和处理自定义信息。
使用通信框架最基础的需求就是收发信息,ESFramework底层已经为我们封装好了所有与信息收发相关的操作,我们只要调用ESPlus.Application.CustomizeInfo命名空间下的相关组件的API来发送信息,以及实现对应的处理器接口来处理收到的信息就可以了。
一.客户端发送信息
客户端可以发送信息给服务端,也可以发送信息给其他在线用户。
客户端通过ESPlus.Application.CustomizeInfo.Passive.ICustomizeOutter接口提供的方法来发送信息。
我们可以从ESPlus.Rapid.IRapidPassiveEngine暴露的CustomizeOutter属性来获取ICustomizeOutter引用。
public interface ICustomizeOutter { /// <summary> /// 向服务器发送信息。 /// </summary> /// <param name="informationType">自定义信息类型</param> /// <param name="info">信息</param> void Send(int informationType, byte[] info); /// <summary> /// 向在线用户targetUserID发送信息。 /// </summary> /// <param name="targetUserID">接收消息的目标用户ID</param> /// <param name="informationType">自定义信息类型</param> /// <param name="info">信息</param> void Send(string targetUserID, int informationType, byte[] info); /// <summary> /// 向服务器提交请求信息,并返回服务器的应答信息。如果超时没有应答则将抛出Timeout异常。 /// </summary> /// <param name="informationType">自定义请求信息的类型</param> /// <param name="info">请求信息</param> /// <returns>服务器的应答信息</returns> byte[] Query(int informationType, byte[] info); /// <summary> /// 向在线用户或服务器发送信息。 /// </summary> /// <param name="targetUserID">接收消息的目标用户ID。如果为null,表示接收者为服务器。</param> /// <param name="informationType">自定义信息类型</param> /// <param name="post">是否采用Post模式发送消息</param> /// <param name="action">当通道繁忙时所采取的动作</param> void Send(string targetUserID, int informationType, byte[] info, bool post, ActionTypeOnChannelIsBusy action); /// <summary> /// 即使与目标用户之间有可靠的P2P通道存在,也要通过服务器转发信息。 /// </summary> /// <param name="targetUserID">接收消息的目标用户ID,不能为null。</param> /// <param name="informationType">自定义信息类型</param> /// <param name="info">信息</param> void TransferByServer(string targetUserID, int informationType, byte[] info ); /// <summary> /// 向在线用户或服务器发送信息,并等待其ACK。当前调用线程会一直阻塞,直到收到ACK;如果超时都没有收到ACK,则将抛出TimeoutException。 /// </summary> /// <param name="targetUserID">接收消息的目标用户ID。如果为null,表示信息接收者为服务端。</param> /// <param name="informationType">自定义信息类型</param> /// <param name="info">信息</param> void SendCertainly(string targetUserID, int informationType, byte[] info); /// <summary> /// 向在线用户或服务器发送大的数据块信息。直到数据发送完毕,该方法才会返回。如果担心长时间阻塞调用线程,可考虑异步调用本方法。 /// </summary> /// <param name="targetUserID">接收消息的目标用户ID。如果为null,表示接收者为服务器。</param> /// <param name="informationType">自定义信息类型</param> /// <param name="blobInfo">大的数据块信息</param> /// <param name="fragmentSize">分片传递时,片段的大小</param> void SendBlob(string targetUserID, int informationType, byte[] blobInfo ,int fragmentSize); }
发送信息有几种方式:
1. 普通发送:
调用Send方法进行普通发送,即将信息写入网络流后就立即返回。
Send方法的重载有个ActionTypeOnChannelIsBusy参数,用于指示当通道繁忙时所采取的动作:继续发送、或丢弃数据。在某些系统中,对于一些非重要非紧急信息的发送,可以为ActionTypeOnChannelIsBusy参数传入枚举值Discard(丢弃)。
2. 带ACK机制的发送:
调用SendCertainly方法发送信息时会启用ACK机制,即将信息发送出去后,调用并不返回,而是要等到接收方的ACK后,才返回。如果接收方不在线,SendCertainly调用会在阻塞一段时间后(默认为30秒),抛出超时异常。
ACK机制是由ESFramework底层实现的,我们直接使用,不需要做任何额外的其它工作。关于带ACK机制的信息发送的更多内容可以参见ACK机制。
3. 信息同步调用:
调用Query方法可以发送请求信息,并返回接收方处理请求后的应答信息。就像方法调用一样 - - 使用参数调用方法并返回结果。从Query方法的重载看到,信息同步调用的对象既可以是服务端、也可以是另外一个在线客户端。关于信息同步调用的更多内容可以参见消息同步调用。
4. 回复异步调用:
重载的Query方法(带有CallbackHandler参数的)在发送请求信息后,不会阻塞而继续向下执行,而框架在收到对应的回复信息时,会回调CallbackHandler委托指向的方法。由于调用线程与回复回调的线程不是同一个线程,所以称这种机制为回复异步调用。
5. 使用P2P通道发送:
调用SendByP2PChannel方法可以明确指定使用P2P通道进行发送,这是一种普通发送。如果与接收者之间的P2P通道不存在,则由ActionTypeOnNoP2PChannel参数指示如何动作:使用服务器转发、或者丢弃信息。关于P2P通道的更多内容可参考ESFramework 开发手册(04) -- 可靠的P2P。
6. 强制经过服务器转发:
对于某些类型的P2P信息,我们可能想在服务端监控它,如果这些信息还是经过P2P通道发送的话,那么服务端将捕获不到这些信息。TransferByServer方法用于解决这一问题。如果调用TransferByServer方法发送信息,那么即使有可靠的P2P通道存在,信息仍然会经过服务器中转。
7. 发送大数据块:
调用SendBlob方法可以将大数据块信息(比如一张大图片)发送给服务端或任何其他的在线用户。 关于大数据块的更多内容,可以参考ESFramework 使用技巧 -- 大数据块信息。
二.服务端发送信息
服务端可以通过ESPlus.Application.CustomizeInfo.Server.ICustomizeController接口向客户端发送信息和广播、以及同步调用客户端。
我们可以从ESPlus.Rapid.IRapidServerEngine暴露的CustomizeController属性来获取ICustomizeController引用。
public interface ICustomizeController { /// <summary> /// 当因为目标用户不在线而导致服务端转发自定义信息失败时,将触发该事件。参数为转发失败的信息。 /// </summary> event CbGeneric<Information> TransmitFailed; /// <summary> /// 向ID为userID的在线用户发送信息。如果用户不在线,则直接返回。 /// </summary> /// <param name="userID">接收消息的用户ID</param> /// <param name="informationType">自定义信息类型</param> /// <param name="info">信息</param> /// <param name="clientType">要发给哪个终端设备。如果传入null,表示发送给所有在线终端设备</param> void Send(string userID, int informationType, byte[] info,ClientType? clientType = null); /// <summary> /// 向当前AS上的在线用户发送信息,并等待其ACK。当前调用线程会一直阻塞,直到收到ACK;如果超时都没有收到ACK,则将抛出Timeout异常。 /// </summary> /// <param name="userID">接收消息的用户ID</param> /// <param name="informationType">自定义信息类型</param> /// <param name="info">信息</param> /// <param name="ClientType">要发给哪个终端设备</param> void SendCertainlyToLocalClient(string userID, int informationType, byte[] info, ClientType clientType); /// <summary> /// 向ID为userID的在线用户发送信息。如果用户不在线,则直接返回。 /// </summary> /// <param name="userID">接收消息的用户ID</param> /// <param name="informationType">自定义信息类型</param> /// <param name="info">信息</param> /// <param name="post">是否采用Post模式发送消息</param> /// <param name="action">当通道繁忙时所采取的动作</param> /// <param name="clientType">要发给哪个终端设备。如果传入null,表示发送给所有在线终端设备</param> void Send(string userID, int informationType, byte[] info ,bool post,ActionTypeOnChannelIsBusy action,ClientType? clientType = null); /// <summary> /// 向ID为userID的在线用户发送大数据块信息。直到数据发送完毕,该方法才会返回。如果担心长时间阻塞 用线程,可考虑异步调用本方法。 /// </summary> /// <param name="userID">接收消息的用户ID</param> /// <param name="informationType">自定义信息类型</param> /// <param name="blobInfo">大数据块信息</param> /// <param name="fragmentSize">分片传递时,片段的大小</param> /// <param name="clientType">要发给哪个终端设备。如果传入null,表示发送给所有在线终端设备</param> void SendBlob(string userID, int informationType, byte[] blobInfo, int fragmentSize,ClientType? clientType = null); /// </summary> /// 询问当前AS的在线用户,并返回应答信息。如果超时没有应答则将抛出Timeout异常。如果客户端在处理请求时出现未捕获的异常,则该调用会抛出HandingException。 /// </summary> /// <param name="userID">接收并处理服务器询问的目标用户ID</param> /// <param name="informationType">自定义请求信息的类型</param> /// <param name="info">请求信息</param> /// <<param name="clientType">要发给哪个终端设备</param> /// <returns>客户端给出的应答信息</returns> byte[] QueryLocalClient(string userID, int informationType, byte[] info, ClientType clientType); }
各个方法含义几乎与客户端是一致的。ICustomizeController接口还暴露了TransmitFailed事件,我们可以通过预定该事件来监控那些转发失败的信息。
SendCertainlyToLocalClient方法和QueryLocalClient方法的名称都以LocalClient结尾,其在ESPlatform群集平台中就会显示其特别的含义 - - 这两个方法只能针对连接到当前服务端的客户端进行调用。而其它的几个方法,则是可以将信息发送给任何在线用户的,即使该用户位于群集中的其它应用服务器上。
三.处理信息
客户端可以收到来自其它客户端或服务端的信息、大数据块、以及同步调用。服务端也可以收到来自客户端的信息(转发的信息除外)及同步调用。那么,我们如何处理这些接收到的信息了?
无论是服务端,还是客户端,都只要实现ESPlus.Application.CustomizeInfo.ICustomizeHandler接口即可。
public interface ICustomizeHandler { /// <summary> /// 处理接收到的自定义信息。 /// </summary> /// <param name="sourceUserID">发送该信息的用户ID。如果为null,表示信息发送者为服务端。</param> /// <param name="clientType">客户端设备类型</param> /// <param name="informationType">自定义信息类型</param> /// <param name="info">信息</param> void HandleInformation(string sourceUserID, ClientType clientType , int informationType, byte[] info); /// <summary> /// 处理接收到的请求并返回应答信息。 /// </summary> /// <param name="sourceUserID">发送该请求信息的用户ID。如果为null,表示信息发送者为服务端。</param> /// <param name="clientType">客户端设备类型</param> /// <param name="informationType">自定义请求信息的类型</param> /// <param name="info">请求信息</param> /// <returns>应答信息</returns> byte[] HandleQuery(string sourceUserID, ClientType clientType, int informationType, byte[] info); }
(1)凡是sourceUserID参数为null的,都表示被处理的信息是来自服务端的;否则,表示被处理的信息是由其它在线客户端发出的。
(2)ICustomizeHandler 即用于客户端、也用于服务端。如果是用于服务端,则方法的第一个参数sourceUserID是绝对不会为null的。
(3)ICustomizeHandler接口的所有方法都是在后台线程中被调用的,所以如果这些方法的实现中涉及到了操作UI,一定要将调用转发到UI线程。
(4)在客户端,将ICustomizeHandler的实现类的实例传递给ESPlus.Rapid.IRapidPassiveEngine的Initialize方法以挂接到框架;
在服务端,则将ICustomizeHandler的实现类的实例传递给ESPlus.Rapid.IRapidServerEngine的Initialize方法以挂接到框架。
四.更多说明
1. 信息发送模型
信息发送可以使用同步模型或异步模型,在方法中通过bool型post参数体现出来。如果其值为true,表示使用异步模型(即发送方法的调用立即返回,不用等到信息发送完毕);否则使用同步模型(阻塞调用线程,直到信息发送完毕)。
2. 信息处理
客户端和服务端的ICustomizeHandler,我们称之为自定义信息处理器,或者业务处理器,表示其用于处理我们应用系统的具体业务逻辑。
(1)业务处理器将在后台线程中被调用,所以,实现业务处理器的方法中如果涉及到了UI操作,则必须将调用转发到UI线程。
(2)业务处理器的方法必须尽可能快地返回,否则,将不能及时地处理后续的消息。如果某个业务处理方法非常耗时,可以考虑使用异步方式。
3. 大数据块
当发送大数据块时,发送方会将其拆分为许多连续的片段逐个发送,而在接收方会自动将接收到的片段重组起来构成一个完整的信息。而且无论是发送大数据块,还是普通信息,在接收方都是调用相同的方法(ICustomizeHandler的HandleInformation方法)来处理的。
五.另一套消息收发机制
除了通过Customize空间发送消息和处理消息外,ESFramework的最新版本增加了另一套收发消息的机制 -- 直接通过引擎接口收发消息。
与Customize机制的区别在于,通过引擎接口发送的消息,接收方将触发 MessageReceived 事件,而不是回调Handler接口。这两套机制是相互独立的,不会相互影响。下面我们把引擎接口中与消息收发相关的API摘出来:
public interface IRapidPassiveEngine { /// <summary> /// 当接收到来自服务器或其它用户的消息时,触发此事件。 /// 事件参数:sourceUserID - clientType - informationType - message - tag 。 /// 如果消息来自服务器,则sourceUserID为null。 /// </summary> event CbGeneric<string,ClientType,int, byte[], string> MessageReceived;
/// <summary> /// 当(当前用户在其它客户端设备上发送了消息)时,触发此事件。 /// 事件参数:clientType - destUserID - informationType - message - tag 。 /// 如果原消息的接收者为服务器,则destUserID为null。 /// </summary> event CbGeneric<ClientType, string, int, byte[], string> EchoMessageReceived; /// <summary> /// 向服务器或其它在线用户发送消息。如果其它用户不在线,消息将被丢弃。 /// </summary> /// <param name="targetUserID">接收者的UserID,如果为服务器,则传入null</param> /// <param name="informationType">消息类型</param> /// <param name="message">消息内容</param> /// <param name="tag">附加内容</param> /// <param name="toOtherClientOfMine">是否发送给我的其它在线设备</param> /// <param name="observer">消息发送进度的观察者</param> void SendMessage(string targetUserID, int informationType, byte[] message, string tag, bool toOtherClientOfMine = false, IProgressObserver observer = null); /// <summary> /// 向服务器或其它在线用户发送消息。如果其它用户不在线,消息将被丢弃。 /// </summary> /// <param name="targetUserID">接收者的UserID,如果为服务器,则传入null</param> /// <param name="informationType">消息类型</param> /// <param name="message">消息内容</param> /// <param name="tag">附加内容</param> /// <param name="fragmentSize">消息将被分块发送,分块的大小</param> /// <param name="toOtherClientOfMine">是否发送给我的其它在线设备</param> /// <param name="observer">消息发送进度的观察者</param> void SendMessage(string targetUserID, int informationType, byte[] message, string tag , int fragmentSize,bool toOtherClientOfMine = false, IProgressObserver observer = null); /// <summary> /// 向服务器或其它在线用户异步发送消息(当前调用线程立即返回)。如果其它用户不在线,消息将被丢弃。 /// </summary> /// <param name="targetUserID">接收者的UserID,如果为服务器,则传入null</param> /// <param name="informationType">消息类型</param> /// <param name="message">消息内容</param> /// <param name="tag">附加内容</param> /// <param name="fragmentSize">消息将被分块发送,分块的大小</param> /// <param name="handler">当发送任务结束时,将回调该处理器</param> /// <param name="handlerTag">将回传给ResultHandler的参数</param> /// <param name="toOtherClientOfMine">是否发送给我的其它在线设备</param> void SendMessage(string targetUserID, int informationType, byte[] message, string tag, int fragmentSize, ResultHandler handler, object handlerTag,bool toOtherClientOfMine = false);}
public interface IRapidServerEngine { /// <summary> /// 当接收到来自客户端的消息时,触发此事件。 /// 事件参数:sourceUserID - sourceType - informationType - message - tag 。 /// </summary> event CbGeneric<string, ClientType,int, byte[], string> MessageReceived; /// <summary> /// 向在线用户发送消息。如果目标用户不在线,消息将被丢弃。 /// </summary> /// <param name="targetUserID">接收者的UserID</param> /// <param name="informationType">消息类型</param> /// <param name="message">消息内容</param> /// <param name="tag">附加内容</param> /// <param name="clientType">要发送给哪个客户端设备,如果为null,则表示发给所有设备</param> void SendMessage(string targetUserID, int informationType, byte[] message, string tag, ClientType? clientType = null); /// <summary> /// 向在线用户发送消息。如果目标用户不在线,消息将被丢弃。 /// </summary> /// <param name="targetUserID">接收者的UserID</param> /// <param name="informationType">消息类型</param> /// <param name="message">消息内容</param> /// <param name="tag">附加内容</param> /// <param name="fragmentSize">消息将被分块发送,分块的大小</param> /// <param name="clientType">要发送给哪个客户端设备,如果为null,则表示发给所有设备</param> void SendMessage(string targetUserID, int informationType, byte[] message, string tag, int fragmentSize,ClientType? clientType = null); }
比如,客户端通过IRapidPassiveEngine的SendMessage方法发送消息给服务端,则服务端的IRapidServerEngine将触发MessageReceived事件。预定MessageReceived事件,我们就可以处理接收到的消息。
假设A登录了PC和安卓,然后A在安卓上通过SendMessage 发消息给B,并将 toOtherClientOfMine参数传入true时,则A的PC端会触发EchoMessageReceived事件。
在实际使用时,究竟该使用哪种机制比较好了?
我们的建议是,以Customize机制为主,以引擎直接收发消息机制为辅助。在Customize机制中,通过实现处理器接口,我们可以将所有的消息处理集中到一个Handler类中;而如果使用引擎直接收发消息的机制,则在很多地方都可以预定MessageReceived事件,使得消息处理的代码分散在不同的地方。
下一篇:ESFramework 开发手册(02) -- 在线用户管理、基础功能及状态通知
上一篇:ESFramework 开发手册(00) -- 概述
-----------------------------------------------------------------------------------------------------------------------------------------------
Q Q:168757008