本文介绍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通道向在线用户targetUserID发送信息。 /// </summary> /// <param name="targetUserID">接收消息的目标用户ID,不能为null。</param> /// <param name="informationType">自定义信息类型</param> /// <param name="info">信息</param> /// <param name="actionType">当P2P通道不存在时,采取的操作</param> /// <param name="post">是否采用Post模式发送消息</param> /// <param name="action">当通道繁忙时所采取的动作</param> void SendByP2PChannel(string targetUserID, int informationType, byte[] info, ActionTypeOnNoP2PChannel actionType, 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); /// <summary> /// 向在线目标用户或服务器提交请求信息,并返回应答信息。如果目标用户不在线,或超时没有应答则将抛出TimeoutException。如果对方在处理请求时出现未捕获的异常,则该调用会抛出HandingException。 /// </summary> /// <param name="targetUserID">接收并处理请求消息的目标用户ID。如果为null,表示信息接收者为服务端。</param> /// <param name="informationType">自定义请求信息的类型</param> /// <param name="info">请求信息</param> /// <returns>应答信息</returns> byte[] Query(string targetUserID, int informationType, byte[] info); /// <summary> /// 回复异步调用。向在线目标用户或服务器提交请求信息,当收到应答信息或超时时,将回调CallbackHandler函数。 /// </summary> /// <param name="targetUserID">接收并处理请求消息的目标用户ID。如果为null,表示信息接收者为服务端。</param> /// <param name="informationType">自定义请求信息的类型</param> /// <param name="info">请求信息</param> /// <param name="handler">用于处理回复信息的处理器</param> /// <param name="tag">携带的状态数据,将被传递给回调函数handler</param> void Query(string targetUserID, int informationType, byte[] info, CallbackHandler handler, object tag);
} /// <summary> /// 用于处理回复信息的方法委托。 /// </summary> /// <param name="ee">如果为TimeoutException,表示超时没有回;如果为HandingException,表示处理方在处理时抛出异常。只有为null时,response参数才有效。</param> /// <param name="response">回复消息</param> /// <param name="tag">状态数据</param> public delegate void CallbackHandler(Exception ee, byte[] response, object tag);
发送信息有几种方式:
调用Send方法进行普通发送,即将信息写入网络流后就立即返回。
Send方法的重载有个ActionTypeOnChannelIsBusy参数,用于指示当通道繁忙时所采取的动作:继续发送、或丢弃数据。在某些系统中,对于一些非重要非紧急信息的发送,可以为ActionTypeOnChannelIsBusy参数传入枚举值Discard(丢弃)。
调用SendCertainly方法发送信息时会启用ACK机制,即将信息发送出去后,调用并不返回,而是要等到接收方的ACK后,才返回。ACK机制是由ESPlus底层实现的,我们直接使用,不需要做任何额外的其它工作。关于带ACK机制的信息发送的更多内容可以参见ACK机制。
调用Query方法可以发送请求信息,并返回接收方处理请求后的应答信息。就像方法调用一样 - - 使用参数调用方法并返回结果。从Query方法的重载看到,信息同步调用的对象既可以是服务端、也可以是另外一个在线客户端。关于信息同步调用的更多内容可以参见消息同步调用。
重载的Query方法(带有CallbackHandler参数的)在发送请求信息后,不会阻塞而继续向下执行,而框架在收到对应的回复信息时,会调用CallbackHandler委托指向的方法。由于调用线程与回复回调的线程不是同一个线程,所以称这种机制为回复异步调用。
调用SendByP2PChannel方法可以明确指定使用P2P通道进行发送,这是一种普通发送。如果与接收者之间的P2P通道不存在,则由ActionTypeOnNoP2PChannel参数指示如何动作:使用服务器转发、或者丢弃信息。关于P2P通道的更多内容可参考ESFramework 开发手册(04) -- 可靠的P2P。
对于某些类型的P2P信息,我们可能想在服务端监控它,如果这些信息还是经过P2P通道发送的话,那么服务端将捕获不到这些信息。TransferByServer方法用于解决这一问题。如果调用TransferByServer方法发送信息,那么即使有可靠的P2P通道存在,信息仍然会经过服务器中转。
调用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> void Send(string userID, int informationType, byte[] info); /// <summary> /// 向当前AS上的在线用户发送信息,并等待其ACK。当前调用线程会一直阻塞,直到收到ACK;如果超时都没有收到ACK,则将抛出Timeout异常。 /// </summary> /// <param name="userID">接收消息的用户ID</param> /// <param name="informationType">自定义信息类型</param> /// <param name="info">信息</param> void SendCertainlyToLocalClient(string userID, int informationType, byte[] info); /// <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> void Send(string userID, int informationType, byte[] info ,bool post ,ActionTypeOnChannelIsBusy action); /// <summary> /// 向ID为userID的在线用户发送大数据块信息。直到数据发送完毕,该方法才会返回。如果担心长时间阻塞调用线程,可考虑异步调用本方法。 /// </summary> /// <param name="userID">接收消息的用户ID</param> /// <param name="informationType">自定义信息类型</param> /// <param name="blobInfo">大数据块信息</param> /// <param name="fragmentSize">分片传递时,片段的大小</param> void SendBlob(string userID, int informationType, byte[] blobInfo, int fragmentSize); /// <summary> /// 询问当前AS的在线用户,并返回应答信息。如果超时没有应答则将抛出Timeout异常。如果客户端在处理请求时出现未捕获的异常,则该调用会抛出HandingException。 /// </summary> /// <param name="userID">接收并处理服务器询问的目标用户ID</param> /// <param name="informationType">自定义请求信息的类型</param> /// <param name="info">请求信息</param> /// <returns>客户端给出的应答信息</returns> byte[] QueryLocalClient(string userID, int informationType, byte[] info); }
各个方法含义几乎与客户端是一致的。ICustomizeController接口还暴露了TransmitFailed事件,我们可以通过预定该事件来监控那些转发失败的信息。
SendCertainlyToLocalClient方法和QueryLocalClient方法的名称都以LocalClient结尾,其在ESPlatform群集平台中就会显示其特别的含义 - - 这两个方法只能针对连接到当前服务端的客户端进行调用。而其它的几个方法,则是可以将信息发送给任何在线用户的,即使该用户位于群集中的其它服务器上。
客户端可以收到来自其它客户端或服务端的信息、大数据块、以及同步调用。服务端也可以收到来自客户端的信息(转发的信息除外)及同步调用。那么,我们如何处理这些接收到的信息了?
无论是服务端,还是客户端,都只要实现ESPlus.Application.CustomizeInfo.ICustomizeHandler接口即可。
public interface ICustomizeHandler { /// <summary> /// 处理接收到的信息(包括大数据块信息)。 /// </summary> /// <param name="sourceUserID">发出信息的用户ID。如果为null,表示信息来自服务端。</param> /// <param name="informationType">自定义信息类型</param> /// <param name="info">信息</param> void HandleInformation(string sourceUserID, int informationType, byte[] info); /// <summary> /// 处理接收到的请求并返回应答信息。 /// </summary> /// <param name="sourceUserID">发送请求信息的用户ID。如果为null,表示信息来自服务端。</param> /// <param name="informationType">自定义请求信息的类型</param> /// <param name="info">请求信息</param> /// <returns>应答信息</returns> byte[] HandleQuery(string sourceUserID, 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方法以挂接到框架。
信息发送可以使用同步模型或异步模型,在方法中通过bool型post参数体现出来。如果其值为true,表示使用异步模型(即发送方法的调用立即返回,不用等到信息发送完毕);否则使用同步模型(阻塞调用线程,直到信息发送完毕)。
客户端和服务端的ICustomizeHandler,我们称之为自定义信息处理器,或者业务处理器,表示其用于处理我们应用系统的具体业务逻辑。
(1)业务处理器将在后台线程中被调用,所以,实现业务处理器的方法中如果涉及到了UI操作,则必须将调用转发到UI线程。
(2)业务处理器的方法必须尽可能快地返回,否则,将不能及时地处理后续的消息。如果某个业务处理方法非常耗时,可以考虑使用异步方式。
当发送大数据块时,发送方会将其拆分为许多连续的片段逐个发送,而在接收方会自动将接收到的片段重组起来构成一个完整的信息。而且无论是发送大数据块,还是普通信息,在接收方都是调用相同的方法(ICustomizeHandler的HandleInformation方法)来处理的。
最后,大家可以查看demo的源码,并运行demo,来了解信息发送和处理的流程。谢谢。
关于ESFramework的任何问题,欢迎联系我们:
电话:027-87638960
Q Q:372841921