es通信框架

http://www.cnblogs.com/wenjie/archive/2008/04/11/1148577.html

 http://www.cnblogs.com/oraycn/archive/2011/10/11/2205662.htmlESFramework 开发手册

ESFramework介绍之(6)―― 基于C/S的4层架构概述

    ESFramework4层结构的4层分别是:客户端(Client)、应用服务器(AS)、功能服务器(FS)、数据库服务器。它们之间的联系图示意如下:
es通信框架

    FS FunctionServer),功能服务器,处理并且仅处理所有的功能性请求,不参与用户管理、状态保持等,提供最纯粹的功能服务。
    AS ApplicationServer),应用服务器,转发所有的功能请求给FS,并处理所有的非功能请求,并管理终端用户、进行状态保持、日志记录等。
    上图中的功能服务器FS的个数可能是0NN>0)个。在某种意义上可以认为,每个功能服务器FS是可以互换的。

    将服务器拆分为功能服务器和应用服务器有两个显而易见的好处:
(1)功能服务器FS的完全可复用。由于功能服务器采用“框架+插件”的结构,所以整个功能服务器是完全可复用的,从一个具体应用转换到另一个具体应用,只需要替换功能插件即可,FS不需重新编译。
(2)由于FS仅提供最纯粹的功能服务,不需要进行用户管理、状态保持,这种功能服务器在运行时的无状态性,使得功能服务器很容易实现负载均衡集群(后文中会讲到,这种动态负载均衡是如何实现的)。
 

    如果ESFramework仅仅做到这一步,就没有必要拿出来和大家分享了,ESFramework不仅对这种4层架构给予了充分完整的支持,ESFramework更进了一步,它可以支持“类似地域分布式”的体系结构。读者可能已经了解到,上面的4层架构已经是一种分布式架构了,那么这里说的“类似地域分布式”又是什么意思?

    可以这么说,4层架构是一种“纵向”的架构,“类似地域分布式”则侧重于“横向”,在“类似地域分布式”体系结构中,每一个具体的“4层架构的实现”只是其中的一个组成元素。我举个例子,现在我们的一个应用需要为全国范围内的所有大城市的手机用户提供某种基于C/S的手机增值服务。我们的经验是,为每个城市配置一个应用服务器AS,由于大量的计算在该AS对应的FS上,所以可能需要多个FS为这个AS服务。而每个城市的AS之间可能需要相互通信(比如处理漫游用户),这就需要将AS也管理起来,管理AS的服务器是IRAS(跨区域服务器)。如此一来,我可以画出下图作为例子:
es通信框架
     图中的FunAddin功能插件,这再前文已介绍过了。整个体系中,终端请求的服务主要分为两大类,一是向应用服务器AS请求功能服务,另一类是终端与终端之间的非功能通信。所有的功能服务由功能插件(FunAddin)进行处理,所有的非功能通信由应用服务器处理或中转。如果,终端请求的功能服务位于外部系统,则功能插件会自动定位外部系统的地址,然后通过WebService等方式向外部系统提交请求。
    
    好了,读者已经了解了
ESFramework中的4层结构和“类似地域分布式”结构是怎么回事了,下面我简单概述一下ESFramework4层结构和“类似地域分布式”结构提供了哪些强有力的特性支持:            

1.  基于构件
    除了所有的功能插件是构件外,整个ESF平台也是由构件组装而成。其好处是:
(1)快速搭建系统
(2)促进构件复用,如AS/IRAS/FS/IRFS可以使用同一个通信组件来完成通信层工作。
(3) 实现功能插件的“热插拔”,可以在运行时动态的添加/移除功能服务

2.  高度可扩展
    由于ESF服务平台体系需要随时随地的应付各种突如其来的变化,其一定要具备高度的可扩展性:
(1)功能插件的“热插拔”
(2)外部服务的动态接入(通常是通过WebService
(3)应用服务器AS的动态添加/移除,比如,新开通针对大连城市的服务。
(4)功能服务器FS的动态添加/移除,实现功能服务器的动态负载均衡集群。

3.  高度可伸缩
    随着我们提供的服务日渐深入人心,我们的用户的数量会急剧增加,所以ESF服务平台体系必须具备高度可伸缩性来提高系统的最大负载和吞吐量。
(1)由于功能服务器需要进行大量的功能运算,所以平台的瓶颈通常位于功能服务器,这可以通过功能服务器的动态集群来解决。集群中的各个功能服务器之间的负载均衡由对应的应用服务器AS来调度。
(2)当单个区域的常在线用户数量突破500010000时,我们需要添加AS进行区域级的负载均衡,这可以通过具有端口映射功能分硬件来解决。 

4.  高度可复用
    ESF服务平台体系并非只是适用于我们的LBS服务,其实,ESF服务平台体系是一个高度可复用的体系,也就是说ESF服务平台可以作为任何、任意的服务的基本平台,只要其所提供的服务是终端和服务器之间通过Tcp进行基于连接的通信。 

5.  分布式
    除了外部系统的接入通过分布式服务进行外,各应用服务器之间、功能服务器与应用服务器之间、应用服务器和跨区域的应用服务器之间都是采用分布式通信。跨区域的应用服务器通过类似于remoting的方式在各个应用服务器之间进行调度。

6.  简单部署、自动升级
    由于ESF服务平台体系服务的区域可能非常多,比如各个大城市可能都需要部署应用服务器和功能服务器,所以如果通过人工进行部署和升级是非常低效的,ESF服务平台提供了自动升级、加载、运行的功能。
(1)服务平台安装后,仅仅需要修改配置文件中的几个参数即可正常运行。
(2)当功能插件拥有新版本的时候,可以在不停止服务的情况下,自动升级到新版本。
(3)当各服务器系统(AS/IRAS/FS/IRFS)有新版本时,会在该系统重启的时候自动升级到新版本。为了在升级的时候不终止服务,服务器系统可以使用逐步升级的方式。 

7.  通信保证机制
    当遇到网络突然断开或某服务器重启的情况,在网络恢复或服务器重启完成后,需要一种能自动的立即恢复通信(比如AS和FS的通信,各AS与IRAS之间的通信)的机制,ESF服务平台提供了这种保证,其采用的策略主要基于:
(1)定时论询
(2)Tcp连接池自动重连
(3)连接动态反转

8.  漫游支持、跨区域功能请求支持
    在ESF服务平台体系中,漫游是指某一区域的用户登录到另外一区域的应用服务器AS上,对于此AS来说,该用户是漫游用户。如果用户登录到某AS却请求其它区域的功能服务,则是跨区域的功能请求。ESF服务平台对这两种情况都给予了充分的支持。

9.  终端与终端之间的通信支持
    有时,终端需要和终端(可能是同区域的、也可能是其它区域的)之间进行通信,并且这种通信可以基于连接和基于非连接。基于连接的通信像实时视频聊天、实时监控,基于非连接的像发送一张图片给不在线的用户。所有这些,ESF服务平台都提供了支持。

10.支持二次开发
   在基于ESF服务平台高度可复用和可扩展的基础上,ESF平台可以非常容易的支持二次开发,只要遵循相同的接口和通信协议,就可在ESF平台进行二次开发。

11.客户端框架
   如果应用的客户端也可以使用.NET开发,则ESFramework也提供了完善的支持,在ESFramework的支持下,开发客户端仅仅需要开发业务插件就可以了,而整个网络通信、多线程、升级部署等,都由框架完成了。后面的文章中我会介绍如何在AgileIM中开发自定义的业务插件。

     上面的所有特性将会在“基于C/S4层架构”部分分节介绍,感谢关注! 

     如果你的应用不需要这么复杂的结构,比如仅仅一个简单的3层架构,那么ESFramework仍然可以帮助你快速构建,ESFramework是个轻量级的应用框架,你不会为那些ESFramework提供了的而你不需要的功能/特性付出任何代价。
    (注意,ESFramework不太适合处理遗留系统(就像你很难使用MFC去处理基于VCL构建的UI应用一样),ESFramework虽然与应用无关,但是为了能将更多的任务从应用中抽象到框架中来,必须对应用做一些假设,幸运的是,ESFramework仅仅对应用的通信协议做了最少的假设,这个假设包含在NetMessage中。如果你不是处理遗留系统,而是构建一个全新的C/S应用,那么ESFramework可以为你节省大量的架构设计时间、软件开发时间、调试和维护时间。 

 

 

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

  我觉得IServerAgent是我在开发ESFramework期间非常满意的一个想法,相信大家也会对它感兴趣的。因为它使得使用基于消息请求/回复的交互就像方法调用一样简单
    客户端与服务器之间的所有通信都可经过IServerAgent,包括要转发的P2P消息。它的主要目的是:
(1)屏蔽客户端与服务端之间的通信协议(Tcp/Udp),ITcpServerAgent、IUdpServerAgent
(2)可将异步的消息请求/回复转化为同步的方法调用。 

  ESFramework主要支持基于TcpUdpC/S系统,所以客户端和服务端之间是通过消息进行交互的。如果仅仅是客户端发出请求、服务器给出服务这种情况很容易处理,但是如果服务端有主动发消息给客户端的情况,事情就会变得稍微复杂。通常,客户端会有一个专门的接收线程来负责从网络接收数据,然后把接收的消息交给对应的处理器处理,或者,这个接收到的消息是个服务端给出的回复,那么这个回复就应该交给发出请求的请求者,但是对应的请求者在哪里了?这种回复消息与请求消息的匹配是比较繁琐的,特别是在上述服务端可以主动给客户端发送消息的情况下。为了简化这个过程,IServerAgent出现了,它用于客户端,像它的名字一样,可以把它当作服务器。IServerAgent的主要目的就是将消息请求/回复转换成方法调用,就像该接口定义的一样:
es通信框架

 

 

public   interface  IServerAgent
    {
        
///   <summary>
        
///  如果超时仍然没有回复,则抛出超时异常
        
///  如果dataPriority != DataPriority.CanBeDiscarded ,则checkRespond只能为false
        
///   </summary>      
        NetMessage CommitRequest(NetMessage requestMsg ,DataPriority dataPriority ,  bool  checkRespond);        
    }

    
public   enum  DataPriority
    {
        High ,
// 紧急命令
        Common , // 如普通消息,如聊天消息
        Low , // 如文件传输
        CanBeDiscarded  // 如视频数据、音频数据
    }

 

     首先解释一下参数dataPriority的意义,dataPriority参数仅仅对Tcp协议起作用,当有多个请求要同时发送时,它决定了发送的优先级。CanBeDiscarded表明这个消息在网络繁忙时可以被抛弃,比如即时通讯的音频数据、视频数据等。关于这个数据发送的优先级机制的实现是ITcpAutoSender,这个组件会在后文中介绍。

    CommitRequest方法提交一个请求消息该给服务器,并返回一个回复消息给请求者。这就是一个方法调用!!!其间隐藏了通过网络将消息发送给服务器并从服务器获取结果的中间细节。这是怎么做到的?思路其实很简单,只是描述起来有些复杂。主要要解决两个问题,一是如何将请求消息与对应的回复匹配起来,二是CommitRequest从哪里找到匹配的回复。
    对于第一个问题,相信大家还记得IMessageHeader定义中有个CorrelationID属性,正如其名,这是一个随机数,每生成一个新的请求消息,就会产生一个随机数赋值给CorrelationID属性,由于随机数重复的可能性很小,所以可以把它当作是唯一的。这样一个随机数就唯一的标志了一个请求,当服务端收到这个请求后,就处理这个请求,并把回复消息的消息头中的CorrelationID属性设为与对应的请求消息的CorrelationID一样的值,这样,客户端收到回复消息后,就可以和对应的请求消息一一对应起来了。

  对于第二个问题的解释,就需要涉及到ESFramework中支持客户端开发的其它两个组件:EsbPassiveDataDealerIResponseManagerEsbPassiveDataDealer是客户端用户处理所有接收到的消息的处理器,而IResponseManager组件用于暂存所有的来自服务端的回复。对于每个接收到的消息,EsbPassiveDataDealer判断其是否为回复,如果是,则将其交给IResponseManager暂存。IResponseManager为暂存的每个回复都设置的生存期TTL,如果回复在IResponseManager中的时间超过了这个TTL,则会被删除。

  你也许已经想到第二个问题的解决方法了。是的,CommitRequest方法将请求发送到网络之后,就定时从IResponseManager中寻找CorrelationID为请求消息头的CorrelationID值的回复消息,如果找到,就返回它,否则就等待循环,直至超时抛出TimeoutException异常。下面给出IResponseManager的接口定义:
    es通信框架

 

    public   interface  IResponseManager
    {
        
void  Initialize() ;

        
void  PushResponse(NetMessage response) ;
        NetMessage PopRespose(
int  correlationID , int  serviceKey) ;   // 立即返回
        NetMessage PickupResponse( int  serviceKey , int  corelationID) ; // 在TimeoutSec时间内不断的PopRespose

        
///   <summary>
        
///  ResponseTTL 如果一个回复在管理器中存在的时间超过ResponseTTL,则会被删除。如果ResponseTTL为0,则表示不进行生存期管理
        
///   </summary>         
         int  ResponseTTL{ set  ;}  // s

        
///   <summary>
        
///  如果在TimeoutSec内,仍然接收不到期望的回复,则抛出异常。取0时,表示不设置超时
        
///   </summary>
         int  TimeoutSec{ set  ; }     
    }

 

      IServerAgent的具体实现包括TcpServerAgent和UdpServerAgent,分别支持Tcp协议和Udp协议的客户端开发。从它们的接口定义中可以看到它们都借助于IServerAgentHelper实现自己。

 

    public   interface  IServerAgentHelper
    {
        IEsbLogger          EsbLogger{
set  ;  get  ;}
        IContractHelper   ContractHelper{
set  ;  get  ;}
        INetMessageHook   NetMessageHook {
set  ;  get  ;}
        IPassiveHelper    PassiveHelper {
set  ;  get  ;}

        IResponseManager  ResponseManager{
set  ; get  ;}    

        ISingleMessageDealer SingleMessageDealer{
set  ;  get  ;}        

        IMessageDispatcher ConstructDispatcher() ;        
    }

 

    我们要特别注意其ConstructDispatcher方法,该方法构建了一个客户端比较常用的消息分配器实例。在介绍IMessageDispatcher时,我们讲过,客户端通常不需要对消息Spy,仅仅需要Hook就可以了,所以IServerAgentHelper正是通过对各组件的组装做到了这一点:

 

        public  IMessageDispatcher ConstructDispatcher()
        {
            
// NakeDispatcher
            EsbPassiveDataDealer dealer      =   new  EsbPassiveDataDealer( this .responseManager , this .passiveHelper , this .singleMessageDealer) ;
            EsbPassiveDealerFactory factory 
=   new  EsbPassiveDealerFactory(dealer) ;
            NakeDispatcher nakeDispatcher   
=   new  NakeDispatcher() ;
            nakeDispatcher.ContractHelper   
=   this .contractHelper ;
            nakeDispatcher.DataDealerFactory
=  factory ;

            
// MessageDispatcher
            IMessageDispatcher messageDispatcher  =   new  MessageDispatcher() ;
            messageDispatcher.ContractHelper     
=   this .contractHelper ;
            messageDispatcher.NetMessageHook     
=   this .netMessageHook ;
            messageDispatcher.NakeDispatcher     
=  nakeDispatcher ;

            
return  messageDispatcher ;
        }

 

    在IServerAgent的基础之上,我们就可以从一个新的角度来设计客户端的结构的,那就是采用和功能服务器一样的插件方式。在ESFramework的支持下,我们的应用开发变得非常简洁和简单,所要做的主要内容就是开发服务端的“业务功能插件”和对应的客户端的“PassiveAddin”(客户端插件)。如果我们的应用已经发布投入使用,而此时用户要求添加一项新的业务,那将是非常简单的事情,那就是开发一个实现了新业务的功能插件动态加载到功能服务器中、再开发一个对应的客户端插件动态加载到客户端中,这样就可以了。服务器不用重编译、甚至不用停止服务;客户端也不用重编译、甚至不用停止使用。一切都是在运行中动态完成的。

 

ESFramework介绍之(8)-- 客户端插件IPassiveAddin

    前文已经提到了,在IServerAgent的基础上,客户端也可以采用插件的结构形式,客户端插件需要实现IPassiveAddin接口。
    我的想法是,当客户端主程序加载一个新的PassiveAddin时,可以在某个菜单的子Items上添加一项,当双击这个子菜单项时,则弹出该客户端插件提供的“业务操作窗体”。这只是使用客户端插件的可行方式之一,你完全可以根据你的应用来决定使用形式。IPassiveAddin接口定义如下:

 1     ///   <summary>
 2       ///  IPassiveAddin 用于客户端的插件。通常一个PassiveAddin对应着一个服务端的功能插件FunAddin
 3       ///  zhuweisky 2006.03.13
 4       ///   </summary>
 5       public   interface  IPassiveAddin : IAddin
 6      {
 7          Type AddinFormType{ get  ;}  // AddinFormType必须实现IAddinForm接口
 8      }
 9 
10       public   interface  IPassiveAddinForm
11      {
12           // PassiveAddin通过IServerAgent发送请求并获取结果
13           void  Initialize(IServerAgent serverAgent , string  userID) ;
14      }

    IPassiveAddin直接从IAddin继承,仅仅增加了一个属性AddinFormType,这个属性就是前面说的客户端插件提供的“业务操作窗体”。“业务操作窗体”必须从IPassiveAddinForm接口继承。
    “业务操作窗体”只有通过暴露的Initialize方法获取IServerAgent引用后,才能发送请求获取结果。Initialize方法的第二个参数说明当前时哪个用户在操作,这样客户端插件在构建请求消息时,需要将userID填充到请求消息的消息头中去,这样服务器才会知道这个消息的来源。

    下面的代码说明了客户端主程序是如何加载IPassiveAddin的:

 1            private   void  LoadPassiveAddins()
 2          {
 3               this .lIToolStripMenuItem_addin.DropDownItems.Clear();
 4 
 5               string  directory  =  System.IO.Directory.GetParent(System.Windows.Forms.Application.ExecutablePath).FullName;
 6               this .addinManagement.LoadAllAddins(directory,  true );
 7 
 8               foreach  (IAddin addin  in   this .addinManagement.AddinList)
 9              {
10                  IPassiveAddin passiveAddin  =  addin  as  IPassiveAddin;
11                   if  (passiveAddin  !=   null )
12                  {
13                      ToolStripItem item  =   new  ToolStripMenuItem(passiveAddin.ServiceName,  null new  EventHandler( this .OnAddinMenuClicked));
14                      item.Tag  =  passiveAddin;
15                       this .lIToolStripMenuItem_addin.DropDownItems.Add(item);
16                  }
17              }
18          }
19 
20           private   void  OnAddinMenuClicked( object  sender, EventArgs e)
21          {
22               try
23              {
24                  ToolStripItem item  =  (ToolStripItem)sender;
25                  IPassiveAddin passiveAddin  =  (IPassiveAddin)item.Tag;
26                  Form addinForm  =  (Form)Activator.CreateInstance(passiveAddin.AddinFormType);
27                  ((IPassiveAddinForm)addinForm).Initialize( this .tcpServerAgent , this .currentUserID);
28                  addinForm.Show();
29              }
30               catch  (Exception ee)
31              {
32                  ee  =  ee;
33              }
34          }

    上述的介绍没有什么难点,仔细体会一下都能明白,就不多说了。这里我给出一个测试用的功能插件和对应的客户端插件示例。示例的功能插件用于从http://www.webservicex.net/globalweather.asmx 通过WebService获取城市的天气信息,而客户端插件则用于为用户提供这项服务。

先看服务端功能插件实现:

WeatherPreAddin

主要是DealRequestMessage方法的实现,代码非常简单,通过WebService获取指定城市的天气情况,将返回的XML解析封装成IContract,然后返回给客户端。

接下来看客户端插件的实现,分为两步:首先是“业务操作窗体”界面设计
es通信框架

 该窗体要从IPassiveAddinForm接口继承。当点击按钮时,处理代码为:

button1_Click
        private void button1_Click(object sender, System.EventArgs e)
        {
            string cityName = ESFramework.Common.ChsToSpellConverter.Convert(this.comboBox1.SelectedItem.ToString()) ;
            WeatherReqContract body = new WeatherReqContract(this.contractHelper) ;
            body.cityName = cityName ;
            MessageHeader header = new MessageHeader(this.contractHelper) ;
            header.TypeKey = int.Parse(this.textBox_asCityCode.Text.Trim()) ;
            header.ServiceKey = 987 ;
            header.UserID     = this.curUserID ;
            header.UserIDLen  = this.contractHelper.GetBytesFromStr(this.curUserID).Length ;
            header.MessageBodyLength = body.GetStreamLength() ;

            ESFramework.Network.Message msg = new ESFramework.Network.Message(header ,body) ;
             NetMessage resMsg = this.theAgent.CommitRequest(msg ,DataPriority.Common ,true) ;
            if(resMsg.Header.Result != ServiceResultType.ServiceSucceed)
            {
                MessageBox.Show("没有发现对应的服务~!") ;
                return ;
            }

            WeatherPredictionContract resContract = new WeatherPredictionContract(this.contractHelper) ;
            resContract.FillMyself(resMsg.Body ,resMsg.BodyOffset) ;

            this.groupBox1.Text = "服务结果--" + this.comboBox1.SelectedItem.ToString() ;

            this.label_pressure.Text = resContract.Pressure ;
            this.label_temp.Text     = resContract.Temprature ;
            this.label_vis.Text      = resContract.Visbility ;
            this.label_wind.Text     = resContract.Wind ;
            this.label_time.Text     = resContract.PreTime ;
        }

    注意,theAgent成员即是通过Initialize传入的IServerAgent引用!

接着是
IPassiveAddin实现:

WeatherPassiveAddin

    需要格外注意要实现AddinFormType属性,就是前面实现的“业务窗体”类型。 

下图是功能服务器加载功能插件的截图:

es通信框架 

下图是客户端加载客户插件后的截图:

es通信框架 

下图是客户端插件提供服务的截图:

es通信框架 

     经过上述的介绍,读者应该对开发服务端的功能插件和客户端插件有些了解了。快结束的时候,再为下篇blog开个头。当我们开发了客户端插件和服务端插件后,做调试是一项非常麻烦的工作,因为不仅要启动应用服务器,还要启动客户端主程序、功能服务器才行。为了简化这个过程,我实现了一个Bridge应用程序,只需要加载一pair插件(服务端插件和对应的客户插件),即可进行两个插件的调试,而不用在启动客户端、AS、FS了。


 

你可能感兴趣的:(框架)