消息同步调用-- ESFramework 4.0 进阶(07)
分布式系统的构建一般有两种模式,一是基于消息(如Tcp,http等),一是基于方法调用(如RPC、WebService、Remoting)。深入想一想,它们其实是一回事。如果你了解过.NET的Proxy,那么你会发现,方法调用和消息请求/回复实际上是可以相互转换的,.NET的Proxy的实现,就是在方法调用的堆栈帧和消息之间相互转换的过程。
在ESFramework 4.0 进阶(06)-- 正规消息发送器一文中,我们已经知道了如何发送消息,下面我们来关注一下客户端与服务端进行交互时最常见的一种情况:客户端发送一个请求给服务端,服务端处理后,返回回复消息。比如像这样,服务端提供一个加法运算的服务,客户端请求加法服务的消息类型为1001,消息体中包含加法运算所需的两个参数;服务端计算完成后给出的回复消息的类型为1002,回复消息的消息体中包含加法运算的结果。
一.类比方法调用
这个例子如果用C#方法来表达将非常直观明了:
{
......
}
加法方法接收两个参数,然后返回运算结果。
接下来,我们可以调用该方法:
太easy了,不是吗。如果ESFramework提供一种机制,在客户端通过类似的方法调用直接返回应答消息,该是多么地方便啊。就像这样:
就如最开始所描述的:客户端使用消息类型为1001、且消息体中包含两个参数的requestMsg来调用CommitRequest方法,该方法就返回消息类型为1002、且消息体中包含加法运算结果的应答Message。这就和Add方法所表达的含义一模一样了。
但是,要完全达到类似上面的效果,并不是非常的容易,困难在哪里了?
我们刚才使用了习以为常的方式来调用Add方法,实际上,我们进行的是同步调用。所谓同步调用,就是调用线程与方法执行的线程是同一个线程,调用方必须等到方法执行完毕后才会继续进行后面的动作,否则调用方将一直阻塞;还有一种方式叫异步调用,即调用线程与方法执行的线程是不同的线程,调用发生后,调用方不会阻塞以等待被调用的方法执行完毕,而是立刻执行后续的动作。比如,我们可以以异步的方式来调用Add方法:
Cb cb = new Cb(Add);
cb.BeginInvoke(1, 2, null, null);
现在回过头来,再看我们的刚才示例中的请求消息与应答消息:客户端在调用线程中向服务器发送请求消息,而在接收线程中收到服务器的回复消息,通常,调用线程与接收线程肯定不是同一个线程,所以,从最原始来说,请求消息与回复消息位于不同的线程中。这种模式更像是方法异步调用。
异步调用的好处是当前调用线程不会阻塞,而同步调用的好处是编程模型非常直观简单。所以,最好两种模型都支持,使用者需要用哪个就用哪个。毫无疑问,在通信框架中,原始的模型就是异步调用模型,而ESFramework也增加了同步调用的机制,使得编程模型更加丰富。接下来,我们看ESFramework是如何实现同步调用的。
二.回复消息管理器IResponseManager
ESFramework要将异步消息模型转换为同步消息模型,需要做的一件最重要的事情,就是将回复消息与请求消息匹配起来。但是,由于请求消息与回复消息位于不同的线程,所以需要先找个地方将回复消息保存起来,然后等待调用线程来把回复消息取走。这个保存回复消息的地方就是IResponseManager。其定义如下:
{
void Initialize() ;
/// <summary>
/// 将需要被提取的回复消息压入到管理器中,等待调用线程的提取。
/// </summary>
void PushResponse(IMessage response) ;
/// <summary>
/// 在TimeoutSec时间内不断搜索符合条件的消息,找到后立即返回,如果超时则抛出Timeout异常。
/// </summary>
IMessage PickupResponse(int messageType ,int messageID) ;
/// <summary>
/// 如果一个回复在管理器中存在的时间超过ResponseTTL,则会被删除。单位为s。
/// 如果ResponseTTL为0,则表示不进行生存期管理
/// </summary>
int ResponseTTL{get ;set ;}
/// <summary>
/// 如果在TimeoutSec内,PickupResponse仍然接收不到期望的回复,则抛出异常。取0时,表示不设置超时
/// </summary>
int TimeoutSec{get ;set ; }
}
(1)当我们发送完请求消息后,就可以立即调用PickupResponse来获取回复消息。
(2)当接收线程接收到一个消息后,如果确认其为回复消息(可以通过消息类型MessageType来判断),则调用PushResponse将其放入管理器中。
(3)当期望的消息出现在管理器中,PickupResponse会立即返回。
(4)如果超过TimeoutSec时间,PickupResponse还未找到期望的回复消息,则抛出Timeout异常。
(5)如果一个回复消息在管理器中存在的时间超过了ResponseTTL,则就会从管理器中移除。
ESFramework规定,如果两个消息为匹配的一对“请求/回复”组合,则它们的MessageID必须是一样的。在这个规则的帮助下,我们就知道所期望的回复消息的特征:首先,如果我的请求消息的MessageID 为1920,则回复消息的MessageID也必须为1920;另外,回复消息的类型也作为标志之一,比如上面的示例中,我们的回复消息的类型就是1002。根据MessageID和MessageType来作为完整的匹配依据,就不会出现匹配错乱的情况了。
所以,服务端在实现消息处理器时,处理完请求给出的回复消息的MessageID要设为与请求消息的MessageID相同的值。这点很重要。
三.服务器代理IServerAgent
ESFramework提供了ESFramework.Passive.IServerAgent来支持消息同步调用。其接口定义如下:
{
/// <summary>
/// 向服务器提交请求消息,并返回回复消息。如果超时仍然没有回复,则抛出Timeout异常。
/// </summary>
IMessage CommitRequest(IMessage requestMsg ,DataPriority dataPriority , bool checkRespond);
/// <summary>
/// 查找MessageType为resMessageType的回复,如果超时仍然没有回复,则抛出超时异常。
/// </summary>
IMessage CommitRequest(IMessage requestMsg ,DataPriority dataPriority , int? resMessageType);
IRegularSender RegularSender { set; }
IResponseManager ResponseManager { set; }
}
(1)第一个CommitRequest方法用于请求消息的类型与回复消息的类型相同时的同步调用,也就是说,请求消息的类型与回复消息的类型是可以相同的,比如,消息类型1000,如果是客户端发给服务器的,则一定是请求消息,如果是服务器发给客户端的一定是回复消息,所以此时可以公用一个消息类型。但是,并不是所有的情况下都可以公用,比如,“P2P类型的请求/回复”(即客户端提交的请求不是由服务器处理的,而是由另一个在线客户端处理的)就必须使用两个消息类型将请求和回复区分开来,否则,客户端就无法区分接收到的消息究竟是请求消息还是回复消息了。checkRespond参数表示是否需要搜索回复,如果不需要或根本就没有回复,则方法立即返回null。
(2)第二个CommitRequest方法用于请求消息的类型与回复消息的类型不同时的同步调用,明确指定期望的回复消息的类型。如果resMessageType为null,则表示不需要或根本就没有回复,方法将立即返回null。
(3)IServerAgent依赖于正规消息发送器IRegularSender,表示其要借助于IRegularSender来向服务器提交请求消息。
(4)IServerAgent依赖于刚刚介绍的回复管理器IResponseManager ,表示其会从IResponseManager中提取回复消息。
到此为止,还剩下最后一个问题,那就是回复消息如何被放入回复管理器中,ESFramework可以自动完成这一工作吗?当然可以的。
四.客户端容器类型的消息处理器ContainerStylePassiveProcesser
ESFramework提供了ESFramework.Passive.ContainerStylePassiveProcesser类作为默认的客户端消息处理器,客户端使用它来处理接收到的所有消息。其类图如下:
(1)ContainerStylePassiveProcesser处理器是ContainerStyle,即它不仅实现了IMessageProcesser接口,而且通过ProcesserList属性可以挂接很多个IMessageProcesser实例,这些实例才是真正处理业务逻辑的处理器。
(2)ContainerStylePassiveProcesser可以通过PushKeyDispersiveKeyScope属性配置一组MessageType的集合来定义所有回复消息的类型,然后它就会把对应类型的消息放入回复管理器中,以支持消息同步调用机制。当然,我们也可以通过AddPushKey方法来添加回复消息的类型。
(3)ContainerStylePassiveProcesser支持接收消息的队列,通过AsynMessageQueueEnabled属性以控制是否开启异步消息处理模型(即,不在接收线程中处理消息,而是在一个单独的后台线程中处理所有消息)。注意,如果将AsynMessageQueueEnabled设为true,就没必要再将客户端引擎的HandleMessageAsynchronismly属性设为true了。
(4)ContainerStylePassiveProcesser还依赖于IMessageTransceiver,主要是因为,如果真正的业务处理器有返回消息,则ContainerStylePassiveProcesser会通过IMessageTransceiver将消息发送出去。下篇文章中,我们将会详细介绍IMessageTransceiver。
五.示范代码
下面我们就构造一个典型的支持消息同步调用的客户端骨架流程,最主要是要将ContainerStylePassiveProcesser处理器挂接到骨架上。
IMessageTransceiver messageTransceiver = ......;
IMessageProcesser messageProcesser1 = ......;
IMessageProcesser messageProcesser2 = ......;
IMessageProcesser[] messageProcessers = new IMessageProcesser[] { messageProcesser1, messageProcesser2 };
ResponseManager responseManager = new ResponseManager(5,30);//设定消息在管理器中的生存期为5秒,超时为30秒。
ContainerStylePassiveProcesser containerStylePassiveProcesser = new ContainerStylePassiveProcesser(messageProcessers, responseManager);
containerStylePassiveProcesser.AsynMessageQueueEnabled = true;
containerStylePassiveProcesser.MessageTransceiver = messageTransceiver;
containerStylePassiveProcesser.AddPushKey(1002); //添加回复消息类型1002
containerStylePassiveProcesser.Initialize();
IProcesserFactory processerFactory = new ProcesserFactory(containerStylePassiveProcesser);
IServerAgent serverAgent = new ServerAgent(responseManager, regularSender);
//IMessage response = serverAgent.CommitRequest(requestMessage,DataPriority.Common ,true);
如此,便可以通过serverAgent进行消息同步调用了。 关于如何实例化一个IRegularSender,可以参见ESFramework 4.0 进阶(06)-- 正规消息发送器一文中的代码示例。而IMessageTransceiver实例的构造,我们将在下篇文章中介绍。
六.Rapid引擎对消息同步调用的简化
直接使用ESFramework.Passive.IServerAgent进行消息同步调用,我们还需要构造请求的IMessage并解析返回的回复消息,这个工作还可以简化,ESPlus提供的客户端Rapid引擎让我们可以直接提交数据请求和返回回复数据。大家一定还记得ESPlus.Application.CustomizeInfo.Passive.ICustomizeInfoOutter接口的CommitRequest方法,这个方法用于向服务器提交同步调用:
/// 向服务器提交请求信息,并返回应答信息。(类似于方法的同步调用)
/// </summary>
/// <param name="requestInfoType">自定义请求信息的类型</param>
/// <param name="requestInfo">请求信息</param>
/// <returns>服务器的应答信息</returns>
byte[] CommitRequest(int requestInfoType, byte[] requestInfo);
ICustomizeInfoOutter接口还有一个CommitP2PRequest方法,用于向其它的在线用户提交同步调用(即请求由其它在线用户处理):
/// 向在线目标用户提交请求信息,并返回应答信息。如果目标用户不在线,或超时没有应答则将抛出Timeout异常。
/// </summary>
/// <param name="targetUserID">接收并处理请求消息的目标用户ID</param>
/// <param name="informationType">自定义请求信息的类型</param>
/// <param name="info">请求信息</param>
/// <returns>应答信息</returns>
byte[] CommitP2PRequest(string targetUserID, int informationType, byte[] info);
上述的两个同步调用都是由客户端发出的,而在服务端,我们还可以通过ICustomizeInfoController接口的QueryClient方法来向在线客户端提交同步调用:
/// 询问在线客户端,并返回应答信息。如果目标用户不在线,或超时没有应答则将抛出Timeout异常。
/// </summary>
/// <param name="userID">接收并处理服务器询问的目标用户ID</param>
/// <param name="informationType">自定义请求信息的类型</param>
/// <param name="info">请求信息</param>
/// <returns>客户端给出的应答信息</returns>
byte[] QueryClient(string userID, int informationType, byte[] info);
Rapid引擎内部已经为我们打理好了一切,我们只要调用方法得到结果就OK了。