ESFramework 4.0 进阶(07)-- 消息同步调用

  分布式系统的构建一般有两种模式,一是基于消息(如Tcphttp等),一是基于方法调用(如RPCWebServiceRemoting)。深入想一想,它们其实是一回事。如果你了解过.NETProxy,那么你会发现,方法调用和消息请求/回复实际上是可以相互转换的,.NETProxy的实现,就是在方法调用的堆栈帧和消息之间相互转换的过程。

     在ESFramework 4.0 进阶(06)-- 正规消息发送器一文中,我们已经知道了如何发送消息,下面我们来关注一下客户端与服务端进行交互时最常见的一种情况:客户端发送一个请求给服务端,服务端处理后,返回回复消息。比如像这样,服务端提供一个加法运算的服务,客户端请求加法服务的消息类型为1001,消息体中包含加法运算所需的两个参数;服务端计算完成后给出的回复消息的类型为1002,回复消息的消息体中包含加法运算的结果。

 

一.类比方法调用

   这个例子如果用C#方法来表达将非常直观明了:

    public   static   int  Add( int  para1,  int  para2)
    {
        ......
    }

      加法方法接收两个参数,然后返回运算结果。

      接下来,我们可以调用该方法:

   int  result  =  Add( 1 2 );

      太easy了,不是吗。如果ESFramework提供一种机制,在客户端通过类似的方法调用直接返回应答消息,该是多么地方便啊。就像这样:

   IMessage  CommitRequest( IMessage  requestMsg);

      就如最开始所描述的:客户端使用消息类型为1001、且消息体中包含两个参数的requestMsg来调用CommitRequest方法,该方法就返回消息类型为1002、且消息体中包含加法运算结果的应答Message。这就和Add方法所表达的含义一模一样了。

      但是,要完全达到类似上面的效果,并不是非常的容易,困难在哪里了?

      我们刚才使用了习以为常的方式来调用Add方法,实际上,我们进行的是同步调用。所谓同步调用,就是调用线程与方法执行的线程是同一个线程,调用方必须等到方法执行完毕后才会继续进行后面的动作,否则调用方将一直阻塞;还有一种方式叫异步调用,即调用线程与方法执行的线程是不同的线程,调用发生后,调用方不会阻塞以等待被调用的方法执行完毕,而是立刻执行后续的动作。比如,我们可以以异步的方式来调用Add方法:

public   delegate   int   Cb( int  a,  int  b);
 
  
Cb  cb  =   new   Cb (Add);
   cb.BeginInvoke(
1 2 null null );

  现在回过头来,再看我们的刚才示例中的请求消息与应答消息:客户端在调用线程中向服务器发送请求消息,而在接收线程中收到服务器的回复消息,通常,调用线程与接收线程肯定不是同一个线程,所以,从最原始来说,请求消息与回复消息位于不同的线程中。这种模式更像是方法异步调用。

     异步调用的好处是当前调用线程不会阻塞,而同步调用的好处是编程模型非常直观简单。所以,最好两种模型都支持,使用者需要用哪个就用哪个。毫无疑问,在通信框架中,原始的模型就是异步调用模型,而ESFramework也增加了同步调用的机制,使得编程模型更加丰富。接下来,我们看ESFramework是如何实现同步调用的。

 

二.回复消息管理器IResponseManager

     ESFramework要将异步消息模型转换为同步消息模型,需要做的一件最重要的事情,就是将回复消息与请求消息匹配起来。但是,由于请求消息与回复消息位于不同的线程,所以需要先找个地方将回复消息保存起来,然后等待调用线程来把回复消息取走。这个保存回复消息的地方就是IResponseManager。其定义如下:

    public   interface   IResponseManager  IDisposable
    {
        
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来支持消息同步调用。其接口定义如下:

    public   interface   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类作为默认的客户端消息处理器,客户端使用它来处理接收到的所有消息。其类图如下:

      ESFramework 4.0 进阶(07)-- 消息同步调用

(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处理器挂接到骨架上。

     IRegularSender  regularSender  =   ...... ;
    
IMessageTransceiver  messageTransceiver  =   ...... ;

    
IMessageProcesser  messageProcesser1  =   ...... ;
    
IMessageProcesser  messageProcesser2  =   ...... ;
    
IMessageProcesser [] messageProcessers  =   new   IMessageProcesser [] { messageProcesser1, messageProcesser2 };
    
    
ResponseManager  responseManager  =   new   ResponseManager( 5 , 30 );//设定消息在管理器中的生存期为5秒,超时为30秒。
    responseManager.Initialize();
    
ContainerStylePassiveProcesser  containerStylePassiveProcesser  =   new   ContainerStylePassiveProcesser (messageProcessers, responseManager);
    containerStylePassiveProcesser.AsynMessageQueueEnabled 
=   true ;
    containerStylePassiveProcesser.MessageTransceiver 
=  messageTransceiver;
    containerStylePassiveProcesser.AddPushKey(
1002 ); //添加回复消息类型1002
    containerStylePassiveProcesser.Initialize();
    
IProcesserFactory  processerFactory  =   new   ProcesserFactory ( containerStylePassiveProcesser );               
    I ServerAgent  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>
   
///  向服务器提交请求信息,并返回应答信息。(类似于方法的同步调用)
   
///   </summary>       
   
///   <param name="requestInfoType"> 自定义请求信息的类型 </param>
   
///   <param name="requestInfo"> 请求信息 </param>
   
///   <returns> 服务器的应答信息 </returns>
    byte [] CommitRequest( int  requestInfoType,  byte [] requestInfo);

      ICustomizeInfoOutter接口还有一个CommitP2PRequest方法,用于向其它的在线用户提交同步调用(即请求由其它在线用户处理):

    ///   <summary>
    
///  向在线目标用户提交请求信息,并返回应答信息。如果目标用户不在线,或超时没有应答则将抛出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方法来向在线客户端提交同步调用:

    ///   <summary>
    
///  询问在线客户端,并返回应答信息。如果目标用户不在线,或超时没有应答则将抛出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了。

  

ESFramework 4.0 概述

ESFramework 4.0 有哪些优点?

ESFramework 4.0 版本升级说明(持续更新)

《ESFramework 4.0 快速上手》系列所有文章

《ESFramework 4.0 高级进阶》系列所有文章 

 

你可能感兴趣的:(framework)