您的位置:首页 > 其它

面对软件错误构建可靠的分布式系统-10API与协议

2007-11-09 10:02 459 查看
 API与协议

在我们编写一个软件模块的时候,我们需要描述如何使用它。有一种做法就是为模块所有的导出函数定义一套编程语言API。为了做到这一点,我们可以用3.9节提到过的类型系统。

定义API的方法其实是很普遍的。不同的语言之间,类型符号的细节有所不同,不同的系统之间,底层的语言实现对于类型系统的要求的强制性程度也不一样。如果对类型系统有严格的强制要求,那么这种语言就被称为是“强类型的”(strongly typed),否则它就被称为“弱类型的”(untyped)——这一点经常会引起混淆,因为许多要求进行类型声明的语言它的类型系统是很容易被违反的。Erlang语言不要求类型声明,但是是“类型安全”(type safe)的,意思是不能以一种会破坏系统的方式违反底层类型系统。

即使我们的语言不是强类型的,但是类型声明可以作为一种有价值的文档,而且可以作为一个动态类型检查器的输入,动态类型检查器能够用来进行运行时类型检查。

不幸的是,只按照惯常的方式写出API的对于理解程序的行为是不够的。例如,看下面的代码片断:

silly() ->

{ok, H} = file:open("foo.dat", read),

file:close(H),

file:read_line(H).

按照类型系统的要求和3.9节的例子中给出的API,这段程序是完全合法的。但是它明显是完全没有意义的,因为我们不可能期望从一个已经关闭了的文件中读取东西。

为了改正上面的问题,我们可以添加一个额外的状态参数。辅以一种相当明了的符号,关于文件操作的API可以这样写:

+type start x file:open(fileName(), read | write) ->

166

{ok, fileHandle()} x ready

| {error, string()} x stop.

+type ready x file:read_line(fileHandle()) ->

{ok, string()} x ready

| eof x atEof.

+type atEof | ready x file:close(fileHandle()) ->

true x stop.

+type atEof | ready x file:rewind(fileHandle()) ->

true x ready

这种API模型用了四种状态变量:start, ready, atEof和stop。状态start表示文件还没有被打开。状态ready表示文件已经准备好被读取,atEof表示到了文件的结尾。文件操作总是以start状态开始,而以stop状态终止。

现在API就可以这么解释了,例如,当文件处于状态ready是,进行file:read_line的函数操作是合法的。它要么返回一个字符串,这时候它仍然处于ready状态;或者它返回eof,此时它处于atEof状态。

在atEof状态的时候,我们可以关闭文件或回倒(rewind)文件,所有其他的操作都是非法的。如果我们选择回倒文件,那么文件将重新回到ready状态,这时候read_line操作就又变得合法了。

为API增加了状态信息,就为我们提供了一种判定一系列操作是否与模块的的设计相吻合的方法。

9.1 协议

可见我们可以标定一套API的使用顺序,其实同样的思想也可以应用到协议的定义上。

假设有两个部件使用纯消息传递的方式进行通信,我们要能够在某一个抽象层次说明一下这两个部件之间流动的消息的协议。

167

两个部件A和B之间的协议P可以用一个非确定的有限状态机(non-deterministic finite state machine)来描述。

假设进程B是一个文件服务器,而A是一个要使用这个文件服务器的客户程序,进一步假设会话是面向连接的。那么文件服务器应当遵循的协议可以按如下方式来说明:

+state start x {open, fileName(), read | write} ->

{ok, fileHandle()} x ready

| {error, string()} x stop.

+state ready x {read_line, fileHandle()} ->

{ok, string()} x ready

| eof x atEof.

+state ready | atEof x {close, fileHandle()}->

true x stop.

+state ready | atEof x {rewind, fileHandle()) ->

true x ready

这个协议描述的意思是,如果文件服务器处于start状态,那么它就可以接收{open, filename(), read|write}这种类型的消息,文件服务器的响应要么是返回一个{ok, fileHandle()}类型的消息,并迁移到ready状态,要么是返回一个{error, string()}的消息,并迁移到stop状态。

如果一个协议用类似上面的方式来描述,那么就可能写一个简单的“协议检查”程序,置于进行协议通信的两个进程中间。图9.1就展示了在进程X和Y之间放一个协议检查器C的情形。

168

图9.1:两个进程和一个协议检查器

当X向Y发送一个消息Q(Q是一个询问)时,Y会以一个响应R和一个新状态S作为回应。值对{R, S}就可以用协议描述中的规则进行类型检查了。协议检查器C位于X和Y之间,根据协议描述对X和Y之间来往的所有消息进行检查。

为了检查协议规则,检查器就需要访问服务器的状态,这是因为协议描述可能还有如下的条目:

+state Sn x T1 -> T2 x S2 | T2 x S3

在这种情况下,只观察返回消息T2的类型并不足以区分服务器的下一个状态是S2还是S3。

如果我们回忆一下图4.3的简单的通用服务器的例子,我们程序的主循环就可以是这样的:

loop(State, Fun) ->

receive

{ReplyTo, ReplyAs, Q} ->

{Reply, State1} = Fun(State, Q),

Reply ! {ReplyAs, Reply},

loop(State1, Fun)

end.

这个主循环又可以很容易地改成:

loop(State, S, Fun) ->

receive

169

{ReplyTo, ReplyAs, Q} ->

{Reply, State1, S1} = Fun(State, S, Q),

Reply ! {ReplyAs, S1, Reply},

loop(State1, S1, Fun)

end.

这里S和S1代表协议描述中的状态变量。注意接口(即协议描述中使用的状态变量的值)的状态与服务器的状态State是不同的。

进行了上面的修改后,通用服务器就彻底变成了一种允许安装在客户和服务器之间的动态协议检查器了。

9.2 API还是协议?

前面我们展示了如何用两种本质上相同的方式来做同样的事情。我们可以在我们的编程语言上强加一个类型系统,或者我们可以在用消息传递方式通信的两个部件之间强加一个契约检查机制。这两个方法中,我更喜欢契约检查器这种方法。

第一个方面的原因跟我们系统的组织方式有关系。在我们的编程模型中,我们采用了独立部件和纯消息传递的方式。每个部件被当作是“黑盒子”,从黑盒子的外面,完全看不到里面的计算是怎么进行的。唯一重要的事情就是黑盒子的行为是否遵循他的协议描述。

在黑盒子的内部,可能因为效率或其他编程方面的原因,我们需要使用一些晦涩的编程方法,甚至违背所有的常识规则和好的编程实践。但是只要系统的外部行为遵守了协议描述,就没有丝毫关系。

只要简单的扩展,协议说明就可以扩展成系统的非功能属性。例如,我们可以向我们的协议描述语言中添加一个时间的概念,那么我们就可以这样来表达:

+type Si x {operation1, Arg1} ->

value1() x Sj within T1

| value2() x Sk after T2

意思是operation1应该在T1时间内返回一个value1()类型的数据结构,或在

170

T2时间后返回一个value2()类型的数据结构。

第二个方面的原因跟我们所做的工作在系统中的位置有关。在一个部件的外面放置一个契约检查器决不干涉到该部件本身的构造,并且还给我们的系统增加或删除各种自我测试手段提供了一种灵活的途径。使得我们的系统可以进行运行时检查,并以能以更多的方式进行配置。

9.3 交互部件系统

Erlang系统如何与外界通信呢?——当我们想要构建一个分布式系统,而Erlang只是许多交互部件中的一个时,这个问题就变得很有意思了。在参考文献[35]中我们看到:

任何PLITS系统都是建立在模块(module)和消息(message)这两种基本构件之上的。模块是一种自包含(self-contained)的实体,就如同Simula或Smalltalk中的类、SAIL进程、CLU模块一样。模块本身用什么编程语言来编码并不重要,我们希望做到不同的机器上的不同模块完全可以用不同的语言来编写。——[35]

为了做一个交互部件系统,我们必须使得不同部件在许多方面达成一致,我们需要:

      
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息