大多数协议设计采用结构化方法,最主要的方法是控制软件的分层和数据的结构化。一个好的结构化协议设计,应具有以下特点:
① 简单。协议应该尽量简单而非复杂,因为复杂的协议比较容易出错且很难设计、实现、测试和验证。一个轻量(light-weight)协议具有简单、健壮和高效的特点。一个最典型的例子是在计算机网络管理协议中,IETF的简单网络管理协议(SNMP)取得了空前的成功,而比它复杂得多的由ISO制定的网络管理协议CMIP,尽管功能很强,但非常复杂,因而远没有SNMP流行。由于安全上的原因,SNMP后来有了第2版和第3版,功能虽然很强大,但相比第1版而言由于复杂了很多,很多功能应用得远不如第1版的几个功能广泛。
② 模块化。一个大的具有良好结构的协议可以由许多小的经过精心设计的且容易理解的模块组成。每一个模块一般完成一种功能。理解了模块的构造方式和模块间的交互方式就能很好地理解协议的工作方式。这样设计出来的协议比较容易理解和实现,并且容易验证和维护。因此,在实现一个复杂的功能时应采用模块化的方法将其分解成小的问题,每一个小的问题可以用一个独立的轻量级协议来实现。尽量不要将无关的功能混在一起实现,它们应该用独立的实体来设计和实现。
③ 有界性(bounded)、自稳定性(self-stabilizing)和自适应性(self-adapting)。有界性是指协议不能超出系统的限制,如报文队列的容量。自稳定性是指协议从任一状态开始,在有限时间内,经过有限的状态转换总能达到稳定状态。自适应性是指协议能根据环境的变化动态地适应这种变化,如自适应滑动窗口协议能根据信道的质量和发送的数据的多少动态地调整其流量控制策略。
④ 健壮性(robustness)。设计一个在正常条件下能很好地工作的协议不是一件困难的事。但是要能很好地处理诸多非正常的情况则比较难。设计协议时对其环境应尽量少做假定,以避免设计出来的协议过分依赖环境中的某些可能发生变化的特性。例如,20世纪70年代设计的很多链路级协议如果用在现在的高速数据链路上就不能很好地工作。一个“健壮”的协议设计不是不断地通过增加新的功能来处理期望的情况,而是应该删除可能阻止协议适应那些不期望的情况的非必须的假定。在局域网体系结构中,将数据链路层划分为两个子层:逻辑链路控制子层(LLC)和媒体访问控制子层(MAC)。将与媒体有关的功能放在MAC子层中实现,而将与媒体无关的功能放在LLC层中实现,就是一个很好的设计例子。
⑤ 一致性(consistency)。一个好的协议中不能出现死锁、活锁和不正确的终止。我们将在介绍协议验证时详细讨论这方面的问题。
在上述特点中,最基本的两个特点是简单和模块化。
根据这些特点,人们总结出协议设计的十大基本原则[8],这些原则是协议设计的指南,而非必须要做到的原则。它们是:
① 在开始设计协议之前,确保已清楚、完整地了解了所要解决的问题,包括所有的设计标准、要求和限制等。
② 在定义服务之前不要考虑用什么样的结构去实现这些服务,即在考虑如何做之前先考虑做什么。
③ 在设计模块的内部功能之前先设计模块的外部功能,即先考虑它与外部的接口。
④ 尽量用简单的方法来解决问题。奇巧的协议比简单的协议更容易出错,更难实现、验证,通常效率也低。复杂的问题通常是由简单问题构成的。设计者的任务是将复杂的问题划分成简单的问题,然后分而治之。
⑤ 不要将无关的功能混在一起。
⑥ 不要限制枝节性的东西。一个好的设计应该是可扩展的,能解决一类问题而不是某一特殊问题。
⑦ 在实现一个设计之前,先建立原型,并进行验证。
⑧ 实现协议,并进行性能分析;如果有必要,进行性能优化。
⑨ 检查最后的实现是否与协议设计中的要求一致,即进行协议的一致性测试。
⑩ 不要跳过原则①~⑦。这一条也是最常容易违反的一条规则。
ARPANET的研制经验表明,对于非常复杂的计算机网络协议,其结构应采用层次式的。可以举一个简单的例子来说明划分层次的概念。
假定我们在计算机1和计算机2之间通过一个通信网络传送文件。这是一件比较复杂的工作,因为还需要做不少的工作。
我们可以将要做的工作划分为三类。第一类工作与传送文件直接有关。例如,发送方的文件传送应用程序应当确信接收方的文件管理程序已做好接收和存储文件的准备。若两个计算机所用的文件格式不一样,则至少其中的一个计算机应能完成文件格式的转换。这两件工作可用一个文件传送模块来完成。这样,两个计算机可将文件传送模块作为最高的一层(见图2-4)。在这两个模块之间的虚线表示两个计算机系统交换文件和一些有关文件交换的命令。
图2-4 划分层次举例
但是,我们并不想让文件传送模块完成全部工作的细节,这样会使文件传送模块过于复杂。可以再设立一个通信服务模块,用来保证文件和文件传送命令可靠地在两个系统之间交换。也就是说,位于上面的文件传送模块可利用下面的通信服务模块所提供的服务。我们还可以看出,如果将位于上面的文件传送模块换成电子邮件模块,那么电子邮件模块同样可以利用在它下面的通信服务模块所提供的可靠通信的服务。
同样道理,我们再构造一个网络接入模块,让这个模块负责做与网络接口细节有关的工作,并向上层提供服务,使上面的通信服务模块能够完成可靠通信的任务。
从上述简单例子可以更好地理解分层可以带来很多好处。例如:
① 各层之间是独立的。某一层并不需要知道它的下一层是如何实现的,而仅仅需要知道该层通过层间的接口(即界面)所提供的服务。由于每一层只实现一种相对独立的功能,因而可将一个难以处理的复杂问题分解为若干个较容易处理的更小一些的问题。这样,整个问题的复杂程度就降低了。
② 灵活性好。当任何一层发生变化时(如由于技术的变化),只要层间接口关系保持不变,则在这层以上或以下的各层均不受影响。此外,对某一层提供的服务还可进行修改。当某层提供的服务不再需要时,甚至可以将这层取消。
③ 结构上可分割开。各层都可以采用最合适的技术来实现。
④ 易于实现和维护。这种结构使得实现和调试一个庞大而又复杂的系统变得容易,因为整个系统已被分解为若干个相对独立的子系统了。
⑤ 能促进标准化工作,因为每一层的功能及其所提供的服务都已有了精确的说明。
分层时应注意使每一层的功能非常明确。若层数太少,就会使每一层的协议太复杂。但层数太多又会在描述和综合各层功能的系统工程任务时遇到较多的困难。通常每一层所要实现的一般功能往往是下面的一种功能或多种功能:
① 差错控制。使得和网络对端的相应层次的通信更加可靠。
② 流控制。使得发送端的发送速率不要太快,要使接收端来得及接收。
③ 分段和重装。发送端将要发送的数据块划分为更小的单位,在接收端将其还原。
④ 复用和分用。发送端几个高层会话复用一条低层的连接,在接收端再进行分用。
⑤ 连接建立和释放。在交换数据之前,先交换一些控制信息,以建立一条逻辑连接。当数据传送结束时,将连接释放。
分层当然也有一些缺点,例如,有些功能会在不同的层次中重复出现,因而产生了额外开销。
一般来说,分层要遵循以下几个原则:
① 当需要有一个不同等级的抽象时,就应当有一个相应的层次。
② 每一层的功能应当是非常明确的。
③ 层与层的边界应选择得使通过这些边界的信息量尽量少一些。
④ 层数太少会使每一层的协议太复杂。
软件开发中的自顶向下方法同样可以应用于协议的设计。自顶向下的协议设计过程如图2-5所示。它的起点是系统总体设计时所提出的要求。总体设计将系统划分成若干层,并对各层提出了具体要求(服务特性、工作模式、功能接口等)。
图2-5 自顶向下的协议设计过程