2.3 消息头
从前文可以看出,在消息的流化表示结构中,消息头(Message Header)是位于消息最前端的二进制字节块。它与后面的消息类型块一起,有效准确地控制了对每一个消息的正确接收与解释。
消息头中可以包括以下内容:
· 消息版本号(Version No)
· 消息序列号(Sequence No)
· 消息延续标志(Continue Flag)
· 消息数据长度(Data Length)
2.3.1 消息版本号
消息版本号,准确地讲,应该是指在整个消息体系层面表示消息结构的版本号。也就是说,不同的消息表示结构设计,应该有不同的版本号;而同一表示结构下的不同类型的消息,虽然可以有不同的类型编码或编号,但却具有同一个消息版本号。
在我们设计一种消息机制时,对消息表示结构的事先约定,应该不会是永远不变的。完全有可能根据一些实际的需求,对某一消息组成部分的内容进行增加、修改或删除,甚至有可能增加一个完整的消息组成部分。而我们知道,对消息进行发送或接收的程序,是完全根据消息表示结构来实现的,如果用发送/接收消息版本1的程序来发送/接收消息版本2的消息,则一定会发生非常严重的问题(整个消息机制将无法正常工作)。
所以,在设计消息体系时,在消息头中加入消息版本号的元素,是非常必要的。因此,在能支持不同消息版本的消息体系中,消息发送与接收的程序逻辑如下:
if(version_no == 1) { //处理版本1的代码 } else if(version_no == 2) { //处理版本2的代码 } else if(version_no == 3) { //处理版本3的代码 …… } else { //非法版本号!转入错误处理流程 }
这样一来,在同一个消息体系的不同历史时期,就能做到向下兼容。也就是说,加入新消息表示版本的较新消息体系,能正确处理构建在旧消息体系基础上的老系统发来的消息。
消息版本号根据不同的需求,可以采用不同类型的数据来表示。例如,一个字节char、两个字节short或四个字节int(对目前绝大多数设备而言)均可。需要注意的是,如果决定采用一个字节来表示消息版本号,就意味着在最初设计时,我们已经很清楚:在未来,消息的版本永远不会超过256个那么多。
可以想象,如果最初我们没有设计消息版本号,那么,该消息体系就缺少了未来可灵活扩展的重要特性。就是这么一个简单的考虑,便使消息体系拥有了非常强大的可扩充和向下兼容的重要特性。
2.3.2 消息序列号
对TCP消息来讲,消息序列号(Sequence No)是指同一个连接上发送出去的消息序号。因为无论我们的消息体系有多少个TCP连接在同时工作,同一个连接上的所有消息,都是按先后顺序一一发送与接收的,这就导致每个消息都会有一个序列号。当然,对UDP消息来讲,消息序列号(Sequence No)则是指该消息在整个系统运行中发送的序号,与连接无关。
如果没有异常情况出现,消息序列号应该是按照自然数规律递增的(当然,如果愿意,也可以将第一个消息序列号设计为0)。那么,一般建议消息序列号应该最少采用一个4字节的整型数(int)来表示。这样的话,该连接上应该可以持续使用232=4294967296个序列号,也就是说,可以支持连续发送这么多个消息。在实践中,可以体会到,在同一个TCP连接上,可以支持持续发送这么多个消息的能力,应该是足够了,否则将使应用系统设计不合理。
设计消息序列号,除了对不同的消息进行区分,对消息个数进行统计外(很多读者可能觉得这些没有太大意义),在对下一小节“消息延续标志”的介绍中,读者可以看到,实际上,消息序列号对正确持续接收同一个消息有可能出现的延续内容(一般是因为该消息的列表数据内容太大,不适合在消息体的一次发送中全部完成)具有重要的作用;另外,在后面章节对消息发送与接收的详细介绍中,读者还可以看到,在发送需要回复(Response Message,无论是异步还是同步接收回复)的消息时,为使发送消息能与回复消息正确对应,消息序列号也起到了非常重要的作用。
2.3.3 消息延续标志
消息延续标志(Continue Flag)的设计是为了处理消息内容过长,不适合在一个消息只需一次发送就能完成的情况。
准确地讲,如果消息接收程序发现某个消息头中的消息延续标志是true,那么,就会启动对同一消息继续接收数据的处理逻辑。该处理逻辑的大概流程是这样的:无论是对请求消息(Request Message)还是回复消息(Response Message)的接收,如果在消息头中发现消息延续标志是true,就会将该消息接收后,暂时存放在一个延续消息队列中(Continue Message List);对每一个接收到的后续消息,会用其消息头中的消息序列号与延续消息队列中的所有消息的序列号进行比对,当序列号相同时,就调用消息延续接收的接口,否则,就调用消息正常接收的接口。消息延续接收的接口一般会直接进入继续接收上次未完成数据的处理流程,而不需要像正常的消息接收接口那样,需要先从头处理一个完整的消息结构。
对这个流程的实现,会在后文第5章中详细介绍,这里,读者只需要了解以上大致流程,以帮助读者对设计中引入消息延续标志的理解。
由于消息延续标志只需要存储“是”与“否”两个状态,因此,根据具体需要,可以采用一个字节、两个字节或四个字节的数据类型来表示。
2.3.4 消息数据长度
消息数据长度(Data Length)记录了本消息中包含的数据内容的长度。这其实是很容易理解的,但需要注意以下几点:
· 在消息头中的消息数据长度,对每一种类型的每一个消息应该是不同的。这一点非常清楚,不必多说。
· 消息数据长度主要指消息体的内容大小,但也可以设计为包括消息类型块的长度在内,这一点可以根据设计者的需求来定。
· 在一个完整规范的消息体系中,消息头中的消息数据长度其实并不是作为从消息头后面接收二进制字节的长度依据(我们前面那个原始消息的设计便是这样做的)。这一点非常重要,我们并不需要根据该长度来接收消息的数据内容,在介绍到消息类型块中的消息体长度时,我们就会更清楚。
· 消息头中的“消息数据长度”不仅对每一个具体消息都不同,针对不同响应类型的消息来说,其组成部分也不同:对请求类型的消息,包含错误信息的回复消息(Response Message),以及无错误的回复消息都不同,其规则如下:
对请求消息:消息数据长度 = 消息类型块长度 + 消息体长度。对不包含错误信息的回复消息:消息数据长度 = 消息类型块长度+消息体长度。
对包含错误信息的回复消息:消息数据长度 = 消息类型块长度。因为这种情况下该回复消息应该没有消息体。注意:同样是消息类型块,请求消息、有错误及无错误的回复消息,其组成与长度也有很大不同,这会在2.4节讲消息类型块时介绍。
以上几点涉及的具体过程我们会在第12章中进行详细介绍。
在流化后的消息表示中,消息头是位于最前端的数据块,而在消息的代码表示中,消息头则应该是一个独立的代码单元。