2.2 消息的总体结构
本节介绍消息体的总体结构,即消息是由哪几个部分组成的,它们之间的组织关系如何?或许对不同的消息产品或协议来说,其消息的组成要素与结构会有所不同,但其核心内容与设计思想基本大同小异,所以深入掌握一种典型方法后,就可以触类旁通,举一反三。更重要的是,正如前文所说,读者通过对一种方法的详细深入了解,就能揭开消息机制的神秘面纱,从而了解其内部与底层究竟都干了些什么。
2.2.1 消息组成要素
典型的消息一般由以下三大部分组成。
1.消息头(Message Header)
消息头中包含对该消息的宏观描述信息,由于它对消息的正确接收是至关重要的,因此必须在消息发送时加入该部分内容。
任何一种消息机制都应该有消息头的存在。消息头中不包含任何关于该消息需要承载的真正数据或信息。
2.消息类型块(Message Type Block)
消息类型块主要包含对消息类型及相关信息的描述。例如,消息响应类型:是请求消息(Request),还是回复消息(Response)?消息功能类型编号:我们的系统可能定义了多种不同功能的消息以完成不同的任务,是哪一个?消息远程调用操作类型:一个消息有可能可以实现多种远程调用请求,该消息承载的是哪一个?等等。
同样,消息类型块也是较宏观的信息,不包含任何关于该消息需要承载的真正数据或信息。
也许读者会问,为什么不把消息头与消息类型块合二为一呢?应该说有些消息机制在实现时,可能并没有明确地将消息头与消息类型块区分开来。但从后文的阐述中大家可以看到,将消息头与消息类型块区分开来的必要性与优势,特别是在消息接收时,对消息类型块处理的逻辑、分支与复杂程度要远大于对消息头的接收过程。
3.消息体(Message Body)
不言而喻,消息体承载了消息的真正任务,即包含了我们需要通过该消息传送的所有数据或信息。
一般来讲,消息体的尺寸要远远大于消息头与消息类型块的总和。另一方面,与消息头和消息类型块不同的是,消息体同时还是消息发送/接收及后续消息处理的核心对象,而消息头和消息类型块却一般只与消息发送/接收的传输过程有关。
下面会接着讲消息的这三个部分之间的组成关系,即消息的总体结构,对此,我们将从代码与传输两个层面进行描述。
2.2.2 代码层面消息结构
在代码层面,消息头、消息类型块、消息体这三个部分应该是由相对独立的代码单元(包含类、结构或其他)结合实现其表示的(并不一定是三个,实际情况中往往更多)。
而这些代码单元之间除了有一些定义依赖、相互调用的关系之外(这会与消息表示的具体实现方法有关,将在后文详细介绍),这里主要要说的是:对应用系统程序员来讲,消息头与消息类型块的代码单元部分虽然存在,却是透明的,即应用系统程序员不用关心它们在该层面的表示;而对消息体代码层面的表示,却需要应用系统程序员自己来负责。
具体表现为:应用系统程序员在使用消息机制完成某种需求,发现需要定义新功能类型的消息时,在消息表示层面,可能只需要采用代码定义真正需要传输的内容即可,并不需要关心消息头与消息类型块部分代码单元的存在。但事实上,消息机制本身会自动补充这两个部分,以形成一个完整的代码层面消息表示。
当然,应用系统程序员在根据需求制作消息体代码时,一般都是采用某种工具自动生成,或是参照某个已有的样例修改而来的,这样可以减少错误的发生,我们在第10章“消息的制造”中会专门介绍这一部分内容。
代码层面消息表示的各部分之间,或许会有相互调用的关系及继承等关系,但严格来讲,并没有先后顺序的概念,它们同时存在于源代码之中,而应用系统程序员需要关心的部分只有消息体。
另外,从图2.1以及本章后面的代码示例还可以看出,其实,在一个良好的消息设计中,每当需要定义新功能类型的消息时,即使是需要应用系统程序员自行负责的消息体表示部分,也只是有一小部分代码需要重新编写,而消息体中相当多的其他代码都是可以作为公共部分抽象出来复用的,即图中的“消息体模板”(公共代码)。这里所说的“模板”,并不是指供参考修改的代码样例,而是指一种能以参数化方式支持代码复用的编程方法,如C++的template,便是实现它的一种重要而有效的方法。
图2.1 消息的代码表示结构概念图
2.2.3 传输层面(流化后)消息结构
在流消息机制中,代码层面的消息结构(一组相互关联的类或结构),在需要进行传输时,必须首先进行流化,以采用TCP/IP来实现传送与接收的通信过程。
流化后的消息结构同样包括消息头、消息类型块和消息体三个部分,与代码层面表示不同的是,这时,这三个部分之间有着非常严格的先后顺序,不容出错。
如图2.2所示,一般来说,对不同的消息来讲,消息头的长度一定是固定的,但消息类型块与消息体的长度却是可变的。而消息类型块的可变与消息体的可变则有着实质性的不同:消息类型块的长度会根据消息响应类型的不同而不同。因为我们的消息响应类型总是有限的,并且消息类型编码或者编号总是可以用一个固定长度的空间来存放,所以,消息类型块的长度一般只有有限的几个固定长度可选;而消息体的长度却会根据每个具体消息的不同而不同。也就是说,其长度会依据每次传输时实际需要传送的数据量的大小而变化,即使是同一功能类型(同一编号,后面会讲到消息响应类型与功能类型的概念)消息的不同实例,每次传输时,也不一定相同。因此,严格来讲,在消息传输层面的表示结构中,只有消息体才是真正可变长度的。
图2.2 消息的传输层面结构概念图
让我们再看看第1章1.3节那个原始消息设计的例子,我们先发送了一个4个字节的结构长度,紧接着发送了需要传输的结构实例的实际数据;在接收端,根据发送方与接收方约定的这个简单协议,接收方就可以先接收4个字节的长度,然后再根据这个长度完整接收实际的数据,保证不出现多收、少收或错收的情况。这其实就是一个最简单的消息头与消息体的例子。当然,我们现在发现,如果需要用这个原始的设计来传送/接收不同的结构,那么只有在程序中定下什么时候接收什么类型的结构才可以,否则,即使流数据可以被准确接收,也无法进行解释(反流化)。这时,有些读者可能会想到,对这个原始消息的设计进行一点改进,即在最开始的4个字节的长度后面,加上一个消息体类型编号,这样,接收程序就会根据这个编号进行数据解释(反流化)的工作,以灵活地、分别得到正确类型的结构体实例。这样一来,我们就得到了一个包含消息头、消息类型块和消息体的消息结构雏形。
从后面的内容可以看到,完整的消息表示法要远比这复杂得多。从下一节开始,我们开始介绍消息表示的具体内容。这里需要再次强调的是,本书中关于消息设计的具体细节,是依据作者参与研发的典型消息产品展开的,并不一定精确反映所有消息体系的实际情况,但其设计机制与关键问题基本大同小异。