消息设计与开发
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.5 消息体

本节我们开始介绍消息体,这是消息表示的真正核心内容。可以说,消息的其他组成部分其实都是为消息体服务的。

2.5.1 消息体构成

消息体由哪些部分组成?其结构是什么样的?这是我们首先必须了解的。但与消息头和消息类型块不同的是,对消息体的构成来说,其流化后的表示与代码层面的表示有非常大的区别,因此这里我们专门分开介绍。

1.流化后的消息体构成

消息二进制流表示(流化后)中,由于关于消息的宏观描述信息(版本号、响应类型、功能号、处理标志、长度等)都已经在消息头与消息类型块中包含了,因此,消息体部分只需要包含该消息需要承载的真正数据与信息即可。

前面我们讲过,一个消息应该有能力容纳各种数据类型及其组合,这个需求是毫无疑问的,但关键的问题是如何来设计消息的表示方法以满足这个需求?方法应该是有很多的,这里我们对其中较典型的一种方法进行介绍。

我们可以将消息体承载的所有数据分为以下两大部分:

· 消息本体数据

· 消息列表数据

我们以应用系统中最为典型的一种需求为例来进行说明:从数据库中查询并传输查询结果作为消息承载的目标。假设我们要求消息体可以承载两部分数据,第一部分是SQL查询语句需要的各种参数或可以全部以单值表示(非组合数据类型)的总体描述信息,第二部分是查询的结果。显然,第一部分的数据可以用一组基本类型的数据来表示,第二部分则应该是一组由类似于C语言struct结构的实例单元组成的列表。因此,第一部分数据就对应“消息本体数据”,第二部分数据就对应“消息列表数据”。

有些读者会问,只设计这两部分数据或许对从数据库中查询这种需求很有效,它能满足实践中所有的需求吗?答案是肯定的,实践证明(读者仔细分析后也会清楚),无论实践中有多么复杂的消息传输需求,我们都可以以包含这两部分数据的消息体流来有效地表示。当然,要特别注意一点,对消息机制的基础原则来讲,不允许有消息嵌套的情况出现。对实践中出现消息嵌套的需求,必须采用以下两个方法之一来解决。

· 在应用程序设计层面想办法消除这种嵌套关系。例如,分成两个关联消息来传输。需要说明的是,对应用需求的复杂情况经过分析、抽象后,总能适合我们的消息设计。同时,按照以上消息结构设计来约束用户对传输数据的组合也有助于应用系统程序员将他们的需求更加合理化、条理化和清晰化。

· 在基础消息体系之上再一次进行包装,使应用系统程序员感觉似乎他们定义的消息可以处理消息嵌套的情况,而事实上,在基础消息体系的实现中,还是将嵌套以独立的消息来发送和接收的。

这里顺便指出一点:已定义消息库是分布式应用程序的核心基础代码,不应该随意增加或修改。对一个建立在消息体系之上的分布式应用程序,在开发人员定义新的消息(新的消息功能类型编号)时,应该特别小心:首先应该检查系统中已有的消息能不能满足需求,如果确定需要增加新的消息,也不应该是个人的随意行为,必须经过规范的管理流程审批。当然,对已定义的消息进行修改也是如此,这实际上涉及一个软件开发项目管理的问题,本书不再深入论述。

“消息本体数据”其实包含两类,一种是请求类本体,一种是回复类本体,具体是哪一种,根据消息类型块中的“消息响应类型”便可以知道。同时“消息本体数据”后面也不一定有“消息列表数据”存在,这根据消息类型块中的“消息处理标志”便可以知道。在第12章中,读者可以详细看到消息体系是如何通过这样的控制准确发送与接收各种消息体的。

2.代码层面的消息构成

虽然我们是按照上述内容设计的流化后消息体结构,但在实际传输消息体时,实际最多只包含两部分的内容(本体数据与列表数据),然而,在消息体的代码层面,却远不止这些内容。

首先,所有不同功能类型的消息体代码定义中,都必然有一些公用的代码,这些代码对所有的消息体都是一样的,如消息体的整体结构控制、一些公用接口定义等内容。

其次,由于消息头、消息类型块等相关代码都是公共部分,也就是说,为所有不同的消息定义所公用,因此关于某个具体消息是什么版本,功能号是多少,包含多少种不同的请求操作,消息体中实际包含了几部分内容等信息,我们可称之为消息描述信息,需要与具体的消息体本身一起定义。

接着,在消息接收的过程中,根据接收到的消息体二进制流,需要创建新的消息实例(反流化),要求对不同功能类型的消息,应该采用不同的规则。这是一个典型的switch…case…逻辑。可以想象,如果应用系统只需要少数几个固定的消息定义,则可以在某个程序单元中将这个switch…case…的逻辑写死,但我们知道,新功能类型的消息必须可以由应用系统程序员在需要时灵活创建,因此关于这个新定义消息的创成接口,当然也需要与具体的消息体本身一起定义,而在上面提到的专门处理switch…case…逻辑的程序单元中,只需要增加一个类似登记的动作(我们称消息注册)即可。

然后进入我们的正题:消息本体数据与消息列表数据的代码定义。这应该包括同一功能类型消息的请求本体代码单元、回复本体代码单元和列表数据代码单元的制作。当然,这种制作不是随意进行的,而是根据一定的样板修改而来的,或者是通过专用的工具自动生成的。本书第10章会专门介绍这个内容。

最后,就是关于消息流化接口的定义。与上面消息创成接口定义的理由相同,对某一个新功能类型的消息定义,其具体的流化接口也不可能在事先定义好的公共模板中全部完成(公共模板代码中只包含了各种标准数据类型的流化/反流化接口),所以,在代码层面,与该消息体相关的流化/反流化接口也需要与具体的消息体本身一起定义。

因此,消息体的代码表示应该包含以下内容:

· 消息体公共接口

· 消息描述信息

· 消息本体数据

· 消息列表数据

· 消息创成接口

· 消息流化接口

下面,我们将一一介绍这些组成内容。

2.5.2 消息体公共接口定义

从本小节起到2.5节结束,我们介绍的都是消息体代码层面表示的内容。我们先看看消息体的公共接口定义。关于消息体的公共接口定义,应该是由独立的代码单元(一个或多个)来完成。该代码单元与消息头和消息类型块的代码表示一样,都是属于消息体系公共代码的范畴,应用系统程序员增加一个新的消息功能类型时,只需要复用这部分代码即可,不需要重新编写制作。

首先,该公共接口定义的代码单元中描述了消息体的构成模板,我们知道,每个消息都是由本体数据与列表数据两大部分组成的。注意,由于是公共代码,这里并没有清楚表达每一个具体类型的消息体的构成细节,而只是一个宏观的模板定义。读者们马上可以想到,这个需求可以用C语言的宏定义、C++的模板(template)或者Java的Interface来实现,事实的确如此,在2.6节“消息表示的面向对象实现”中读者就可以看到这一点。

另外,该代码单元中还包括了一些公共函数的接口定义,如消息体大小获取接口,消息体的公共发送接口,消息体的公共接收接口,消息体的公共复制(clone)接口,消息体描述信息获取接口(版本号、功能编号、序列号、处理标志等),消息流化/反流化公共接口等。读者可能会问:每个消息体的大小、发送/接收细节都不相同,如何在公共代码中准确定义该部分内容呢?这其实是个程序设计技巧的问题:通过后面的内容大家可以看到,将与每一个具体的消息体构成细节相关的信息都抽象到其他的独立代码单元中,由应用系统程序员在创建新的消息时自行定义,通过对继承、模板、宏定义等技术的结合,完全可以将所有消息体以上接口的实现在公共代码中准确表达。

2.5.3 消息描述信息定义

接下来看消息描述信息的定义。消息描述信息包括以下内容:

· 消息版本号:指该消息是属于哪个版本的。如前所述,消息版本号的设计是为了使消息体系具有未来可扩充性。只有当消息结构需要扩充,而现有的消息表示、流化、发送与接收机制已经无法正确处理扩充后的消息结构时,才需要增加新的消息版本。所以,消息版本号很少改变,甚至可能永远不会改变。

· 消息功能类型编号:这是区分该消息与其他消息的唯一编号,也就是说,当我们发现需要增加一个新的消息体定义时,就需要增加一个新的编号。由于前文讲过,一个消息功能类型编号需要有同时容纳两种响应类型的能力,在本书介绍的消息设计中,采用了奇偶数的方法来区分,因此,该编号在消息体定义时一般是只有偶数或奇数(本书中统一采用偶数)。

· 请求操作接口定义:前文讲过,一个消息可以承载多种不同的请求操作类型,接收方的应用层程序可以对不同的请求操作类型编写不同的处理逻辑。那么,该消息定义了哪几种请求操作类型,就需要在消息体的代码表示中表明。

· 消息体构成定义:在本书介绍的消息设计中,消息体的实际数据内容由本体数据与列表数据两部分组成。而对不同的请求操作类型,可能其构成成分也会不同。因此,在消息体的代码表示中,需要具有消息构成描述的部分,其内容便是写明该消息由哪几部分组成,并且针对每一种请求操作类型,其组成都可能不同。比如,请求操作类型1可能只包括消息本体数据;请求操作类型2可能包括消息本体数据与消息列表数据。注意,这一点在消息表示中很重要,因为只有如此,我们的接收程序才能知道需要接收些什么内容。

有些读者会有疑问:不是在消息头与消息类型块定义中已经有了关于消息版本号与消息功能类型号的内容了吗,为什么在消息体中还要定义?一定要注意,这里所讲的消息表示法,是指消息在代码层面的表示,而消息头与消息类型块的代码,是属于各种消息公用的公共代码,其中只是定义了存储版本号与类型号的变量空间;而这里所讲的消息描述信息中的版本号与类型号,才是真正的、与某个具体功能类型的消息相关的版本号与类型号数值。

在实际的代码实现中,以上消息描述信息都可以采用宏定义的方法来实现,在本书2.6节就可以看到。

2.5.4 消息本体数据

消息本体数据的代码表示很简单,就是把我们抽象总结出来的本体数据组成元素按顺序一一用定义变量的方法表示出来。

因此,消息体定义时,主要是把本体数据抽象出来,并定义正确的数据类型。前文我们讲过,本体数据一般都是用基本数据类型的组合来表示,而我们抽象出来的本体数据的每一个元素采用哪种基本数据类型来表示,则是一件需要应用系统程序员仔细斟酌的事情。

例如,对整型数,是采用int16_t、int32_t还是int64_t?对浮点数,是用float还是double?对字符串,其长度应该定义多少?对二进制流,如何定义?

其中,特别要注意的是,对字符串数据,考虑其编码格式非常重要:是UTF-16(Unicode),还是UTF-8?消息代码表示时与消息数据流传输时的字符串编码一样吗?并且,这与不同的编程语言也有关系,Windows平台上的VC++语言,UNIX上的C/C++及Java语言,在这一点上也各有不同。

换句话说,如果一个Java开发的应用程序与一个UNIX C/C++开发的应用程序之间需要采用消息进行通信,那么,这两个应用程序都要用自己的语言对同一个消息体分别进行表示,其原则针对每一种数据类型都有可能不同,而字符串类型的数据就特别要注意。

还要指出的是,每一种不同的数据类型,在消息发送与接收时采用的原则和规则也不同。例如,除了按照传输编码的需求进行相应的转码以外,对每一个字符串发送时其实都在其实体数据前面加上了字符串长度。这将会在第3章“消息的流化”内容中详细介绍,这里我们只重点介绍消息的代码表示法。

在消息体的代码表示法中,对每一类消息,应该包括两个独立的定义消息本体数据的代码单元,即“请求类消息本体数据代码单元”和“回复类消息本体数据代码单元”。

2.5.5 消息列表数据

消息列表数据其实是一组相同结构的数据单元的集合,在消息体的代码表示中,它也是一个独立的代码单元。

消息列表数据的代码单元中包含了我们设计新消息时抽象为列表数据的所有数据元素(如组成二维表一行中的各个列),同样,每个元素依然是要用基本数据类型表示(即不支持消息嵌套)。表示消息列表数据的代码单元只需要完整描述列表集合中的一个实例的组成就可以了,真正使用该消息时,将会由应用系统程序员根据需要创建这种实例的集合(数组、vector或list)。

需要指出的是,消息体的代码单元表示中一部分是公共代码,一部分需要创建新消息时编写制作,而所有这些都只是完成了消息体的定义,当应用程序中使用到该消息时,需要创建其实例。对每一个实例来说,其消息本体数据与列表数据的组成模板都相同,但其值与列表的大小都各不相同,这意味着同一类型消息的不同实例在流化后,其大小也是各不相同的。

另外,虽然在创建每个新功能类型的消息时,必须对消息本体数据、列表数据的代码表示等进行编写制作(一般是依据一定的模板修改或采用专用工具自动生成),但其实在应用程序生成具体的消息实例时,根据不同的情况(主要是指不同的请求操作类型),这两个部分也是有可能两者都包含、只包含一个或者两者都不包含。

2.5.6 消息创成接口

在前文中我们讲过,在消息机制的公共代码中,有一个代码单元是专门负责消息接收时根据消息头与消息类型块中接收到的信息创建具体功能类型的消息体,以正确完成消息流中消息体数据的接收过程。关于这些代码单元(不止一个,我们将其称为消息注册的代码单元),我们不将它归入消息表示法的范围,而是将其作为消息接收与发送机制中的内容。

但在消息表示法中,也需要负责有关消息创成的一部分内容,那就是与具体功能类型的消息体有关的创成代码部分,我们需要把它抽象出来,与消息体定义的其他代码表示一起制作,这就是消息体代码表示中消息创成接口部分的代码。

后面我们可以看到,这部分代码实际上可以采用C语言中宏定义的方式来实现,而在消息体定义中,关于消息创成接口的宏定义,实际上只有与负责消息注册代码单元的其他宏定义一起,才能构成一个合法完整的可编译代码单元。可以看出,这还是一个代码设计技巧的问题。

这里的消息创成接口的宏定义代码中其实包含两类信息,一类是关于消息体的创建定义,其中应该包括消息功能编号,消息版本号,请求消息本体数据代码单元定义符,回复消息本体数据代码单元定义符,消息列表数据代码单元定义符等信息;另一类是关于消息请求操作类型与消息体构成对应关系的定义,其中应该包括消息功能编号,消息版本号,请求操作类型及对应的消息处理标志等信息。

到现在,读者应该可以很容易地猜到,其实,负责消息注册的公共代码单元也应该有两类,一类负责消息创建,另一类则负责消息请求操作类型与消息处理标志的对应关系。

2.5.7 消息流化接口

按常理来讲,消息流化相关的内容应该是相对独立的一个话题,我们有一章专门介绍它,但关于为什么在消息体的代码表示中需要加入关于消息流化/反流化接口的理由,在前文我们已经讲过了,其本质还是代码组织的优化问题。

消息体中关于消息流化接口的相关代码其实也是由宏定义来实现的,无论是消息本体还是消息列表的代码单元中,都包含流化接口的内容。这些宏定义代表的实际上是消息体中每一个基本数据类型的流化/反流化函数。

同样,从代码优化的角度来讲,消息的流化接口也应该由公共代码与具体消息功能类型相关的代码两部分组成。公共代码部分可以作为一个独立的代码单元存在,其中集中统一定义了对各种基本数据类型进行流化/反流化时实际需要调用的函数接口,当定义一个新类型的消息时,该部分代码可直接复用,不需要重新编写;消息类型相关的专有代码部分存在于表示消息体实体数据(本体数据与列表数据)的代码单元中,它们严格按相关基本数据类型数据元素定义的顺序排列,专有代码中的消息流化接口定义实际上正是专有代码与公共代码的接口所在。可以看出,这样的代码组织可以最大限度地减少代码重复编写的次数,提高代码复用率。

从后面2.6节的内容大家可以看到,在实际实现中,可以将消息流化接口的公共代码部分作为基类,而所有的消息体代码单元都从其继承而来。

这里要强调的是,无论是流化接口的公共代码部分,还是与类型相关的消息体专有代码相关部分,在本章消息表示法涉及的内容中,其任务都是定义好流化/反流化需要调用的函数接口即可(在专有代码中,这种定义要求是严格按变量顺序进行的),而这些函数的具体实现细节,则属于第3章“消息的流化”中的内容,这里不再讨论。

最后还有一点要指出,根据不同的目标,同一种基本数据类型有可能采用不同的流化接口。例如,在C语言中,对字符串型数据与二进制流型数据,其消息体中的代码表示可能都是char数据,但其流化的接口函数却完全不同,这也需要定义新消息体的程序员非常清楚才行。