ESFramework 4.0 进阶(01)-- 消息

需要交互的分布式系统之间通过消息来传递有意义的信息。消息是通信框架的核心。离开了消息,再谈通信框架就没有任何意义,所以,消息是ESFramework中一个最核心的概念。

一. 消息的类别

在具体的应用中,我们需要对消息的类别进行定义,这有助于我们分析和讨论问题。消息大致可以分为4个类别:请求消息、回复消息、报告、通知、P2P消息。

在Client/Server模式中,出现最多的便是请求消息和回复消息。这两种类别的消息非常容易理解。

报告指的是Client/Server模式中客户端发送给服务端的消息。但这种消息不需要服务端的回复。比如,客户端可能需要将自己的当前状态上报给服务端,这就可以通过“报告”消息来传递。

通知指的是Client/Server模式中服务端主动发送给客户端的消息。通常,这种消息用于告诉客户端它所关心的服务器上的某个状态发生了变化。比如,即时通讯应用中,某个好友上线了,服务端就可以发一个通知来提醒当前用户。

P2P消息指的是Client/Server模式中客户端与客户端之间通信的消息。这种消息可能通过服务器中转,也可能直接经过P2P通道发送。

对于P2P消息,可以进行更进一步的分类,因为P2P消息中也可能存在请求、回复、通知、报告等不同的更具体的类型。通信框架并不关心P2P消息的更详细分类,但是我们的应用程序中应该认真地区分它们。

二. 消息的两种形态

消息的两种形态指的是消息在网络上进行传输的形态和消息在通信框架内部(或应用程序中)的形态。

在网络上,消息表现为一串字节流;而在基于面向对象的通信框架内部,消息通常表示为一个对象。为了使消息能在分布式系统之间进行传递和处理,则要求消息能在这两种形态之间进行转换,而且这种转换必须是无损的和可逆的。所谓“无损”,指的是在转换的过程中不会遗失任何信息;所谓“可逆”,指的是消息从一种形态转换到另一种形态,然后再从另一种形态转换到原来的形态,所得到的结果应该跟初始状态完全相同。

我们将消息对象转换为字节流的过程称为“消息序列化”,将字节流转换为消息对象的过程称为“消息解析”。

在通信框架内部识别一个消息非常容易,因为它们就是一个个的对象实例;而在网络上识别一个消息就要复杂很多,你需要判断出哪个字节是一个消息的开始,又到哪个字节是一个消息的结束。

三. 消息协议

通过网络相互通信的系统之间要想正常交互,它们必须有“共同的语言”,这种语言就是消息协议。遵守消息协议的消息才能被我们的系统所理解。

由于系统的底层通信会被ESFramework通信框架所解决,所以框架会给出一个抽象的消息协议(通过接口来展现),其最主要的目的是使通信框架能非常清晰的识别一个单独、完整的网络上的消息。系统中的消息必须遵循这个消息协议。

消息协议主要可以分为两类:文本协议(如基于xml的协议)和流协议(又称为二进制协议)。

1. 文本协议

“文本协议”采用起始标志符和结束标志符的方法来标志一个完整的消息。比如,规定所有的消息都以“#”开始,并且以“#”结束。

如你所想,“文本协议”方案有着天生的缺陷,无论你采用哪个字符(或多个字符)作为起始结束标志,在消息的中间都可能出现同样的字符,如果出现这样的消息,就会导致该消息的识别发生错误,而且如果是在同一个Tcp连接上传输,就很可能导致后续所有消息的识别都出错。所以,如果使用文本协议,一定要保证消息的内容中不要包含标识符,一般可以使用复杂的标识符(连续地多个字符)或者转义符来解决这个问题。

采用标志符来识别消息还有一个缺陷,那就是效率低下。因为我们需要逐个扫描每一个字符来判断它是否是指定的标志符。

幸运的是,我们有更好的办法来解决这个问题,那就是使用“流协议”。

2. 流协议

流协议规定网络上传递的任何一个消息必须符合以下规则:

(1) 消息由“消息头”(Message Header)和“消息体”(Message Body)构成,消息体可以为空。

(2) 如果有消息体,则消息体必须紧接在消息头的尾部。

(3) 消息头中至少包含了一个信息,那就是消息体的长度。

(4) 同一个应用中所有的消息的消息头的长度都是固定的。

我们来分析一下,流协议是如何解决字符协议的缺陷的。当通信框架从一个网络连接上接收数据时,首先读取消息头的固定长度的字节数,接着解析消息头,并从消息头中获取消息体的长度,然后再读取如此长度的消息体,然后就可以对消息体进行解析了。

在采用基于非连接的通信协议(如Udp)时,我们可以直接丢弃不完整的消息;而在采用基于连接的通信协议(如Tcp)时,我们则可能需要对过长的消息进行分裂和对不完整的消息进行重组。 

四.ESFramework中的消息定义 

ESFramework即支持文本协议,也支持流协议,并且ESFramework提供了这两种类型的消息的消息头的实现、以及消息的序列化及消息的解析。使用ESFramework提供的实现,你就再不用为消息的封装、字节数组到消息的相互转换、消息粘包、消息断裂、消息重组等等这些事情而伤脑筋了。

我们先来看看消息接口的抽象定义IMessage:

/// <summary>
    /// 在网络上传递的消息的基础接口。    
    /// </summary>    
    public interface IMessage
    {
        /// <summary>
        /// 发送消息的源地址。任何一个NetEngine都会保证,在接收完一条消息的时候,会为该字段正确地赋值。
        /// </summary>
        IUserAddress SourceAddress { get; }

/// <summary>
        /// 消息头。
        /// </summary>
        IMessageHeader Header { get; }

/// <summary>
        /// 获取整个消息的长度。如果为文本消息,则为整个文本的长度;如果为流消息,则为消息转化为流之后流的长度。
        /// </summary>       
        int GetMessageLength();

/// <summary>
        /// 将消息转化为字节流,以备发送或持久化。
        /// </summary>        
        byte[] ToStream();
    }

  GetMessageLength方法,用于获取将当前消息对象转换为字节流后的长度。通常,在将消息对象转换成字节流之前,我们先得分配内存空间来存储结果字节流,通过这个方法,我们可以知道需要分配的内存空间有多大。
  ToStream方法用于将当前消息对象序列化为字节流。
  ESFramework规定每个消息必须有消息头IMessageHeader,消息头不能为null,ESFramework框架中的很多组件都需要根据消息头中的相关信息对消息进行相应的处理,比如加解密、分派、转发等。IMessageHeader定义如下:

/// <summary>
    /// 消息头接口,规定了消息头中至少包含的信息:发送者、接收者、目录类型、消息类型、消息ID。
    /// zhuweisky 2005.12.07 
    /// </summary>
    public interface IMessageHeader :ICloneable
    {
        /// <summary>
        /// 发出本消息的用户编号。注意,普通用户编号只能由数字和字母组成,并且其最大长度会由IMessageHeader的实现类给出具体规定。
        /// </summary>
        string UserID { get;set;}

/// <summary>
        /// 接收消息的目标用户编号。注意,普通用户编号只能由数字和字母组成,并且其最大长度会由IMessageHeader的实现类给出具体规定。        
        /// </summary>
        string DestUserID { get;set;}

/// <summary>
        /// 消息的类型。
        /// </summary>
        int MessageType { get;set;}

/// <summary>
        /// 每个消息实例的唯一标志(也可用于将功能请求与其回复一一对应起来)
        /// </summary>
        int MessageID { get;set;}

IStringEncoder StringEncoder { get; }
    }

  UserID属性指示了该消息是由哪个用户发出的,通信框架通过UserID对在线用户进行管理或对目标用户的在线活动进行跟踪。通常,用户都通过客户端登录到服务器,如果消息是从服务器直接发出的,那么该消息的UserID的值取为ESFramework.NetServer.SystemUserID,这是一个常量值。
  DestUserID属性仅在P2P消息或通知类型的消息中有效,它表示这个P2P消息将被发送给哪个指定的用户。对于P2P消息,服务端会根据消息头中的DestUserID将其转发给目标用户。当然,并不是所有的P2P消息都需要经过服务器进行转发,如果P2P通道可用,P2P消息将直接通过用户之间的P2P通道进行传送。

  MessageType属性表名了这条消息的具体类型,不同的ServiceKey标志着不同的消息类型。比如这是一个请求当前天气温度的请求消息,还是一个发给在线好友的聊天文字消息。消息分派器会根据消息的MessageType来将其分派到合适的消息处理器上进行处理。

  特别要指出的是,在具体设计MessageType时,对于一些简单的情况,可以将一对请求/回复消息的MessageType取同一个值。而对于复杂的情况,比如,P2P消息中的一对请求/回复消息,则MessageType必须取不同的值,否则,通信框架将无法依据MessageType判断收到的P2P消息到底是一个请求、还是一个回复。因为在客户端与客户端之间进行P2P通信时,请求和回复消息是双向的,即每一方都可以发送同样类型的请求和给出回复。而在客户端与服务端进行通信时,通常是客户端发送请求,服务端给出回复,在这种情况下,服务端发送同样类型的请求给客户端的可能性非常小,如果出现这种情况,仍然需要为一对请求/回复消息的MessageType取不同的值。

  MessageID属性是每个消息的唯一标志,也就是说框架中处理的任何两个消息的MessageID是不一样的。除了唯一标志每个消息外,在ESFramework中MessageID还有两个额外的用途。第一个用途是在基于非连接的通信中,消息监控器可以根据MessageID过滤掉重复的消息。第二个用途是对于请求/回复消息,使用MessageID可以将请求消息和回复消息一一对应起来。

  

  ESFramework内置了文本消息和流消息的基本封装,如下所示:

  

  从ESFramework的帮助文档“ESFramework4.0.chm”中可以了解更多关于StreamMessage和TextMessage的内容,这里不再赘述了。  

  实际上,ESFramework根本不用关心消息体对象如何解析(如StreamMessage的Body属性就是一个byte[],框架不需要解析它),对于消息体的解析是交给应用来完成的,毕竟,消息体的具体内容都是与应用紧密相关的,通信框架不会尝试去了解消息体中包含了哪些信息。通信框架只关心消息头这一个协议对象,消息头中包含了足够的信息让通信框架来完成分派、监控、转换、和处理等任务。

  应用层如何构造和解析消息了?ESFramework通过IContractHelper接口来进行规范,这个接口由应用程序去实现。

/// <summary>
    /// 与消息协议相关的决策。
    /// 2005.10.07
    /// </summary>
    public interface IContractHelper :IStringEncoder

{

/// <summary>
        /// 解析消息体。
        /// </summary>
        /// <typeparam name="TBody">消息体的类型</typeparam>
        /// <param name="msg">被解析的消息</param>
        /// <returns>解析得到的消息体对象</returns>
        TBody ParseBody<TBody>(IMessage msg) where TBody :class ,new();

/// <summary>
        /// 创建消息。实现该方法时,需要根据body的长度来设置header中MessageBodyLength属性。通常给Server端使用。
        /// </summary>       
        IMessage CreateMessage<TBody>(IMessageHeader header, TBody body) where TBody : class;   
    }

 IStringEncoder接口用于编码和解码字符串,比如,你可以使用Unicode或者UTF-8对字符串进行编解码。

/// <summary>
    /// 字符串编解码器接口。
    /// </summary>
    public interface IStringEncoder
    {
        /// <summary>
        /// 将字节流解码为字符串。
        /// </summary>     
        string GetStrFromStream(byte[] stream, int offset, int len);

/// <summary>
        /// 对字符串进行编码返回字节数组。
        /// </summary>       
        byte[] GetBytesFromStr(string ss);
    }

 

五.ESPlus提供了默认消息头和IContractHelper的默认实现

  我们可以自己定义消息头、消息体的格式,然后根据这些定义来实现IStreamContractHelper接口或ITextContractHelper接口。当然,这是一件比较繁琐而且费时的事情,如果没有特别的需求,建议直接使用ESPlus提供的默认的ESPlus.Core.StreamMessageHeader、ESPlus.Core.TextMessageHeader以及ESPlus.Core.StreamContractHelper实现和ESPlus.Core.TextContractHelper实现。

1.TextMessageHeader       

  TextMessageHeader的定义如下:

/// <summary>
    /// 常用的基于文本的消息头定义。
    /// </summary>
    [Serializable]
    public class TextMessageHeader :IMessageHeader
    {
        #region Ctor  
        public TextMessageHeader() { }
        public TextMessageHeader( string _userID, int _messageType, string _destID ,int _messageID)
        {            
            this.MessageType = _messageType;            
            this.UserID = _userID;
            this.messageID = _messageID;
            this.DestUserID = _destID;
        }    
        #endregion

#region IMessageHeader 成员   
        #region UserID
        private string userID;
        public string UserID
        {
            get
            {
                return this.userID;
            }
            set
            {
                this.userID = value;
            }
        }
        #endregion

#region DestUserID
        private string destUserID = "";
        public string DestUserID
        {
            get { return destUserID; }
            set { destUserID = value; }
        }
        #endregion
       
        #region MessageType
        private int messageType;
        public int MessageType
        {
            get { return messageType; }
            set { messageType = value; }
        }
        #endregion

#region MessageID
        private int messageID = 0;
        public int MessageID
        {
            get { return messageID; }
            set { messageID = value; }
        }
        #endregion

#region StringEncoder
        public IStringEncoder StringEncoder
        {
            get
            {
                return DefaultStringEncoder.Instance;
            }
        }
        #endregion 
        #endregion       
    
        #region ICloneable 成员

public object Clone()
        {
            return this.MemberwiseClone();
        }

#endregion
    }

2.StreamMessageHeader

StreamMessageHeader的定义稍微复杂些,大家要特别注意几点:

(1)消息头转化为流后的长度必须是固定的,ESPlus默认为36字节。所以ToStream方法得到的byte[]的长度一定是36。

(2)MessageBodyLength属性指明了消息体的长度,ESFramwork依据该属性判断一条完整的消息是否接收完毕。

(3)如果使用ESPlus定义的默认的流消息头,则应用程序必须保证UserID的长度不超过11位。

(4)字符串编解码采用UTF-8。

3.StreamPlusContractHelper

ESPlus.Core.StreamContractHelper使用自带的紧凑的二进制序列化器对消息体(Body)进行序列化和饭序列化,其序列化得到的byte[]长度仅仅为.NET自带二进制序列化器的20%甚至更少。当然,ESPlus自带的序列化器的使用是有限制的,也就是说,你在定义协议体时必须注意到这些限制,这个我们后面再写专门的文章详加说明。但是,类似Entity(其中仅仅包含一些简单的数据和类型)的所有类都是可以被支持的,并且可以包含List、Dictionary等集合。一般,这对于通信要传递的数据来说,已经足够了。比如,下面是一个协议体定义的demo:

4.TextPlusContractHelper

TextPlusContractHelper采用XML来进行文本消息的封装,消息体借助于ESBasic.Helpers.XmlHelper来实现xml化和反xml化,XmlHelper目前仅仅支持两级节点。也就是说,向上面DEMO中的FileDetail类是可以被TextPlusContractHelper正常序列化/饭序列化的,但是MoveFileOrDirectoryContract就不行,因为其包含集合,而且内部还有复杂对象(FileDetail)。如果通信的内容比较复杂,而又想使用TextPlusContractHelper,那么使用者可以考虑通过字符串的拼装和拆解来手动完成复杂对象的解析和封装。

   为了使通信的消息个头更小,我们建议使用流协议 -- 即使用StreamMessage和对应的StreamMessageHeader、StreamPlusContractHelper,这样可以节省大量的带宽,而且消息解析/构造的性能也不会降低。

  关于消息的介绍就到这里,下篇我们将介绍ESFramework中消息处理的骨架流程

时间: 2024-09-28 20:09:26

ESFramework 4.0 进阶(01)-- 消息的相关文章

核心梳理——消息处理的骨架流程——ESFramework 4.0 进阶(02)

在ESFramework 4.0 概述一文中,我们提到ESFramework.dll作为通信框架的核心,定义了消息处理的骨架流程,本文我们来详细剖析这个流程以及该骨架中所涉及的各个组件.ESFramework的骨架流程如下图所示: 一.所有的网络引擎都使用同一消息处理骨架流程 ESFramework支持TCP/UDP.二进制协议/文本协议.服务端/客户端组合而成的2x2x2=8种引擎,无论是哪一种引擎,都实现了INetEngine接口,也都使用上图所示的消息处理骨架流程来处理所接收到的所有消息.

驱动力—— 通信引擎(上)—— ESFramework 4.0 进阶(03)

在ESFramework 4.0 进阶(02)-- 核心:消息处理的骨架流程一文中我们详细介绍了ESFramework中消息处理的骨架流程,并且我们已经知道,ESFramework中的所有通信引擎使用的都是这一套骨架流程.ESFramework内置了多种通信引擎以完全支持"客户端/服务端.TCP/UDP.文本协议/二进制协议"这些特性的组合.本文就来剖析ESFramework中的各种通信引擎. 一.通信引擎接口继承关系图 INetEngine是所有网络引擎的基础接口,接下来再派生出服务

ESFramework 4.0 快速上手(01) -- Rapid引擎

(在阅读该文之前,请先阅读 ESFramework 4.0 概述 ,会对本文的理解更有帮助.) ESFramework/ESPlatform 4.0 的终极目标是为百万级的用户同时在线提供支持,因为强大,所以使用也较为复杂,配置也较多.但是如果我们的应用只是一个中小型的通信应用(同时在线5000人以下),直接使用ESPlatform就有点显得杀鸡用牛刀了.ESPlus.Rapid提供了一种快速的方式,来解决类似中小型的通信应用,以最简洁的方式来使用ESFramework. 使用ESPlus.Ra

ESFramework 4.0 快速上手(06) -- Rapid引擎(续)

<ESFramework 4.0 快速上手>系列介绍的都是如何使用Rapid引擎(快速引擎) -- RapidServerEngine 和 RapidPassiveEngine.其实,大家可以将这两个引擎看作是两个壳,内部包装的才是真正的ESFramework的网络引擎, ESFramework支持很多种网络引擎(客户端/服务端.二进制协议/文本协议.TCP/UDP),而RapidServerEngine和RapidPassiveEngine采用的是基于TCP和二进制协议的服务端引擎和客户端引

离线消息如何实现?-- ESFramework 4.0 快速上手(02)

在ESFramework 4.0 快速上手一文中,主要介绍了如何使用ESPlus.Rapid命名空间中的引擎来快速地构建基于TCP的网络通信系统,即使是使用ESPlus.Rapid来进行ESFramework快速开发,也还有很多可以介绍的内容,于是,我想再多写几篇文章来说明现实通信系统中的一些常见需求如何使用ESFramework快速实现.本文是为第一篇,介绍离线消息的原理和实现. 一.如何截获离线消息 阅读了ESFramework 4.0 快速上手朋友都知道,一个在线用户给另一个用户发送文本信

WCF 4.0 进阶系列 -- 随笔汇总

WCF4.0 进阶系列–前言 WCF4.0 进阶系列--第一章 WCF简介 WCF4.0进阶系列--第二章 寄宿WCF服务 WCF4.0进阶系列--第三章 构建健壮的程序和服务 WCF4.0进阶系列--第四章 保护企业内部的WCF服务 WCF4.0进阶系列--第五章 在因特网环境下保护WCF服务 WCF4.0进阶系列--第六章 维护服务协定和数据协定 WCF4.0进阶系列--第七章 维持会话状态和设置服务操作的顺序 WCF4.0进阶系列—第八章 使用工作流实现服务 WCF4.0进阶系列—第九章

如何使用自定义消息?--ESFramework 4.0 快速上手(04)

在ESFramework 4.0 快速上手一文中,我们讲述了如何使用Rapid引擎可以快速地上手ESFramework开发,文中介绍了使用ESPlus.Application.CustomizeInfo命名空间下的类可以发送和处理自定义消息,本文我们就通过一个简单的例子来深入讲解如何使用自定义消息. 例子的场景很简单:假设客户端登陆到服务器之后,要求请求加入某个组,服务端收到该请求后,处理该请求,并给客户端相应的回复 -- 是否加入成功,客户端收到回复后,即可作出相应的处理. 一.定义消息类型和

EJB3.0 EJB开发消息驱动bean

(7)EJB3.0 EJB开发消息驱动bean JMS 一: Java消息服务(Java Message Service) 二:jms中的消息 消息传递系统的中心就是消息.一条 Message 由三个部分组成:             头(header),属性(property)和主体(body). 消息有下面几种类型,他们都是派生自 Message 接口.           StreamMessage:一种主体中包含 Java 基元值流的消息.其填充和读取均按顺序进行.          

&#39;\0&#39; &#39;\1&#39; &#39;\01&#39; &#39;\012&#39;

sizeof(“\0”)=2 sizeof(“\01”)=2 sizeof(“\012”)=2; sizeof(“\0123”)=3; '\0' 明确写在双引号中的\0被当作字符串本身处理'\1' 认为是字符的8进制表示'\012' (\x0c) 同上,8进制表示 '\0' '\1' '\01' '\012' 原文地址:https://www.cnblogs.com/didiaoxiaoguai/p/11723299.html