Fielding博士论文导读----第6章

来源:互联网 发布:外汇画线分析软件 编辑:程序博客网 时间:2024/05/20 18:17
REST其实并不是什么新的东西,从Web的历史来说,甚至可以说非常古老。Fielding和他的协议团队自从1994年以来就在内部使用REST来指导现代Web架构协议的创作。不过Fielding直到2000年才通过这篇博士论文向世人揭示出REST的全貌。而REST真正流行开来,还是要等到Ajax流行之后,特别是在出现了一些成熟的服务器端REST开发框架之后,使得REST这种抽象的架构风格变成了我们能够日常实践的开发架构。

Fielding在这一章中回顾了使用REST指导HTTP和URI协议的设计,以及指导libwww-perl、Apache HTTP服务器等HTTP/URI协议的实现的过程中所积累的经验和教训。

Fielding写道:
"这个名称“表述性状态转移”是有意唤起人们对于一个良好设计的Web应用如何运转的印象:一个由网页组成的网络(一个虚拟状态机),用户通过选择链接(状态转移)在应用中前进,导致下一个页面(代表应用的下一个状态)被转移给用户,并且呈现给他们,以便他们来使用。"
这句话与上一章的“因此,REST的模型应用是一个引擎”那句相呼应,对于读者理解REST架构的本质,REST为何称作“表述性状态转移”非常重要。

REST对于Web基础协议创作最大的价值,是可以用来判断提议对协议的扩展哪些是与Web所需的架构风格相一致的,哪些与Web所需的架构风格相背离的。尽量避免引入与Web所需架构风格相背离的协议扩展,否则这一类的扩展将最终导致Web的崩溃。

Fileding写道:
“重要的是REST确实能够完全捕获一个分布式超媒体系统的那些被认为是Web的行为和性能需求的核心的方面,这样在这个模型中对行为进行优化,将能够导致在已部署的Web架构中得到最适宜的行为。”

在创作URI协议的过程中应用REST所获得的经验:
在创作URI协议的过程中,Fielding重新定义了资源的含义,将资源与其最初缺乏不灵活的简单文档映射脱钩。资源实际上可以代表服务器端任意可命名的抽象概念。
Fielding写道:
“在REST中对于“资源”的定义基于一个简单的前提:标识符的改变应该尽可能很少发生。因为Web使用内嵌的标识符,而不是链接服务器,创作者需要一个标识符,这个标识符能够紧密地匹配他们想要通过一个超媒体引用来表达的语义,允许这个引用保持静态,甚至是在访问该引用所获得的结果可能会随时间而变化的情况下。REST达到了这个目标,通过将一个资源定义为创作者想要标识的语义,而不是对应于创建这个引用时的那些语义的值。然后留给创作者来保证所选择的这个标识符确实真正标识出了他所想要表达的语义。”

对于资源的操作是间接通过资源的表述来进行的,Fielding将这种间接的操作方式称作“操作影子”。

Fielding写道:
“将“资源”定义为一个URI标识了一个概念,而不是标识了一个文档,这给我们带来了另一个问题:一个用户如何访问、操作或转移一个概念,使得他们在选择了一个超文本链接后能够得到一些有用的东西。REST通过定义在被标识的资源的“表述”之上执行的操作,而不是在资源本身之上执行的操作回答了这个问题。一个来源服务器维护着从资源的标识符到每个资源相对应的表述集合的映射,因此可以通过由资源标识符定义的通用接口转移资源的表述来操作一个资源。”

Fielding还写道:
“REST对于资源的定义来源于Web的核心需求:独立创作跨多个可信任域的互相连接的超文本。强制接口的定义与接口的需求相匹配会使得协议似乎含糊不清,但这仅仅是因为被操作的接口仅仅是一个接口,而不是一个实现。这些协议是与一个应用动作的意图密切相关的,但是接口背后的机制必须要确定该意图如何来影响底层实现中资源到表述的映射。
这里所隐藏的信息是关键的软件工程原则之一,也就是REST使用统一接口的动机。因为客户端被限制为只能对资源的表述执行操作,而不是直接访问资源的实现,因此资源的实现可以以任何命名权威所希望的形式来建造,而不会影响到使用资源的表述的客户端。此外,如果当资源被访问时,存在着资源的多个表述,可以使用一个内容选择算法来动态地选择一个最适合客户端能力的表述。当然,其缺点就是对资源进行远程创作不像对文件进行远程创作那么直接。
这里所隐藏的信息是关键的软件工程原则之一,也就是REST使用统一接口的动机。因为客户端被限制为只能对资源的表述执行操作,而不是直接访问资源的实现,因此资源的实现可以以任何命名权威所希望的形式来建造,而不会影响到使用资源的表述的客户端。”

如果你对面向对象设计/面向对象编程(OOD/OOP)相当熟悉,这一大段话看起来是多么地似曾相识。你理解的没有错,资源其实就是服务器所暴露出来的接口,它是一个抽象的概念。面向资源编程,就相当于GoF《设计模式》中一再强调的面向接口编程——OOP的基本法则。

将资源最初很具体的文档映射转变为类似于面向对象设计中的接口之后,带来的一个问题是远程创作不再像以前那么直接。为何在论文中使用了“远程创作”这个词呢?大家可以回忆一下最初的Web是用来做什么的,其实就是供一群分布在欧洲和美洲原子物理实验室的科学家和研究人员来交流研究信息的。这些研究信息一般是格式随意的笔记,作为保存在一个远程服务器上的文档,研究者远程访问和编写这些文档。所以一个资源最初就是直接映射到服务器的一个文档。但是随着Web应用的出现,Web变得越来越动态,这种资源到文档的简单映射非常不灵活,必须要加以改变。不过“远程创作”这个词仍然被保留了下来,代表对位于远程服务器上的资源的操作(创建、获取、修改、删除)。

原先直接文档映射时,修改一个资源是很直接的。现在这种通过接口来修改资源则是间接的,需要通过资源的表述来进行。资源的表述携带了资源的状态信息,客户端可以通过改变收到的资源表述并且提交到服务器来修改资源的状态。在REST和HTTP协议中使用“表述”这个词也是刻意的,HTTP协议有意设计为好像是两个人在对话:
GET http://www.xxx.com/users/3
...

...


客户端:嗨,兄弟,把编号为3的用户信息告诉我?
服务器:好的,这个用户的信息包括....

PUT http://www.xxx.com/users/3
...
email=xxx@gmail.com

客户端:哦,这个用户刚才告诉我,他的email地址改了,新的地址是xxx@gmail.com,麻烦帮我改一下。
服务器:没问题... 已经改掉了。

在这个场景中,服务器返回的html页面和客户端传给服务器的参数名-值对是关于同一个资源不同格式的表述,它们都携带了这个资源的状态。对于资源的所有操作都是通过资源的表述和统一接口来完成的。重新定义资源之后,资源的范围已经远远超过了服务器端文档的范围。事实上,按照新的定义,服务器端任何可命名的概念都可以定义为一个资源。

Fielding写道:
“资源是一种概念上的映射——服务器接收到标识符(标识这个映射),将它应用于当前的映射实现(mapping implementation,通常是与特定集合相关的树的深度遍历和/或哈希表的组合)上,以发现当前负责处理该资源的处理器实现,然后处理器实现基于请求的内容选择适当的动作+响应。所有这些特定于实现的问题都隐藏在Web接口之后,它们的性质无法由仅能够通过Web接口访问资源的客户端来作出假设。”

按照Fielding的设想,资源所代表的语义以及它的URI应该是保持长期稳定的,但是除此以外,资源的实现是可以变化的。将资源的实现封装在它的URI所构成的接口之后,这正是面向对象设计通过接口来封装变化的思想。所以资源的URI和在它之上通过HTTP统一接口执行的一组标准操作,就构成了服务器暴露给客户端访问的Facade。

这是一个关于Web架构设计的极端重要的思想,也不是很难理解。但是,很可惜的是,10多年来,深刻理解这个思想的开发者很少。如果Web应用的开发者都理解了这个思想,那么Web上面缺乏维护的超链接和“404 not found”错误就会少的多。

Fielding继续写道:
“对于设置资源标识符和用表述组装那些资源的动作而言,语义是一个副产品。服务器或客户端软件绝对不需要知道或理解URI的含义——它们仅仅扮演一个管道,通过这个管道,资源的创建者(一个作为命名权威的人)能够将表述与通过URI标识的语义关联起来。换句话说,在服务器端没有资源,仅仅是通过由资源定义的抽象接口提供答案的机制。这看起来似乎很奇怪,但是这正是使得Web跨越如此众多的不同实现的关键所在。”

这样的设计,极大简化了客户端用户代理(通常即浏览器)和来源服务器(例如Apache Httpd或M$ IIS)的开发。

Fielding写道:
“按照用来组成已完成产品的组件来定义事物,是每一个工程师的天性。Web却并非是以这种方式运作的。基于在一个应用动作期间每个组件的角色,Web架构由在组件之间的通信模型之上的约束组成。这防止了组件对于每件事物作出超越资源抽象的假设,因此隐藏了抽象接口任何一端的真实的机制。”
相信读者读到这里,已经差不多完全领会了Fielding的设计思路。这实在是非常精妙的设计!

接下来Fielding指出了使用URI的Web应用中经常出现的与REST不相容(不匹配)的一些地方。
一个例子是URI中包括标识当前用户的信息,如果服务器通过URI重写而不是cookie来跟踪session信息,就会发生这种情况。服务器“通过记录用户的动作来跟踪他们的行为... 由于违反了REST的约束,这些系统会导致共享缓存变得效率低下,这降低了服务器的可伸缩性,并且在一个用户与其他用户共享那些引用时会导致不希望的结果。”
如果说,任何软件设计都是一种权衡。那么在服务器端session中保存很多用户会话状态信息,这样的设计也是一种权衡。并不是完全不应该这样做,而是架构师在这样做的时心里一定要清楚,这样做其实会带来很大的代价,问题是这个代价是否值得付出。

另外一个例子是客户端将Web服务器简单地看作一个分布式文件系统,即类似于NFS这样的东西,然后对Web服务器做镜象。完全静态的Web服务器,这样做镜象还可以,但是对于一个动态的Web服务器,是不能这样做镜象的。

这些问题的根源还是在于Web应用的开发者,包括Web框架的设计者并没有深入理解REST,不理解HTTP和URI的设计意图。REST虽然融入了HTTP和URI协议的设计之中,但是很难强制这些协议的使用者必须要按照REST风格来设计开发Web应用。当然,这些问题会随着REST的日渐普及和深入人心而改变。

在创作HTTP协议的过程中应用REST所获得的经验:

最初的HTTP 1.0协议的一些方面不是很适合REST的要求,在设计HTTP 1.1的过程中,有必要对这些方面加以隔离,使得它们不至于影响新协议的部署。
HTTP1.1为了这个目的,引入了协议版本控制。此外协议版本控制还可以用来增大未来与REST不兼容的协议扩展部署的难度。

Fielding写道:
“这些规则(指协议版本控制)的存在协助了多个协议修订版的部署,并且防止了HTTP架构师遗忘掉协议的部署是其设计的一个重要方面。这些规则是通过使得对于协议兼容的改变和不兼容的改变容易区别来做到这一点的。兼容的改变很容易部署,对于协议接受能力的差异能够在协议流(protocol stream)中进行沟通。不兼容的改变难以部署,因为它们在协议流能够开展通信之前,必须做一些工作来确定协议的接受能力。”

HTTP 1.1还扩大了响应状态码的范围,定义了一个通用的规则来解释新的响应状态码,这样新的响应能够进行部署而不会严重损害老的客户端

HTTP 1.1还增加了很多自描述的消息,即HTTP头信息字段。前面我们已经知道,自描述的消息是HTTP统一接口的一部分。关于自描述的消息一段的描述中,与我们的开发工作相关的最重要的部分是缓存控制和内容协商。
缓存控制方面,HTTP 1.1添加了Cache-Control、Age、Etag和Vary几个头信息字段。

内容协商分为抢先式协商、反作用式协商和透明式协商三种。HTTP 1.1同时支持这三种内容协商方式,不过,对于Web开发者来说,反作用式协商用的最多,因为它能得到更好的性能。反作用式协商就是Fielding在第5章中举例时提到的“先响应后思考”的工作方式。

HTTP 1.1还通过支持持久连接和直写式缓存,改善了协议的性能。我们现在使用浏览器访问HTTP服务器,默认情况下使用的都是HTTP 1.1的持久连接。

接下来Fielding指出了在HTTP协议早期版本识别出的与REST不相容(不匹配)的一些地方。包括区分非权威的响应、Cookie的问题等等。Cookie因为与REST的应用状态模型、语义可见性相违背,因此在设计REST架构时,应该尽量避免使用Cookie。

此外,如果想要扩展HTTP头信息字段的含义,需要注意:
“HTTP头信息字段名称仅当它们所包含的信息对于正确理解消息并非是必需的时候,才能够被任意扩展。”
一般情况下,不应该改变那些标准HTTP 1.1头信息字段的含义,企图用这些头信息字段实现某种非标准的功能。可以使用自定义的HTTP头信息字段,而且这些自定义的HTTP头信息字段不应该影响HTTP通信链的参与方对于HTTP消息的正确理解。

除了创作这些Web架构基础协议,Fielding还亲自参与了这些协议的一些软件实现的开发工作。Fielding简单描述了他在libwww-perl、Apache Httpd等项目中的工作,这些项目的成功充分验证了REST的有效性。

最后,Fielding总结了在所有这些实践活动中所获得的教训。
在“基于网络的API的优势”中,Fielding指出了这种新的设计方法相对于传统的“基于库的API”的优势:

Fielding写道:
“一个基于网络的API对于应用的交互而言,是一种包含有已定义语义的在线(on-the-wire)的语法。一个基于网络的API没有在应用的代码上强加任何除了读/写网络的需求之外的限制,但是确实在能够有效地跨接口进行通信的一组语义上添加了限制。其有利的方面就是,性能仅仅受限于协议的设计,而不是受限于该设计的特殊实现。
    一个基于库的API为程序员做的工作要多得多,但是通过做这些工作,也带来了大量更多的复杂性,并且其负担超出了任何单个系统必须承受的限度,这种方法在一个不同种类的网络中的可移植性较差,而且总是会导致首先选择通用性,而不是性能。作为一个副作用,它也导致了在开发过程中产生惰性(为任何事情都去责备API代码),而不去努力解决其他通信参与方不合作的行为。”

Fielding在详细对比了Web的架构(即REST)与CORBA这样的中间件系统之间的差别之后说:
“为何这些差别是很重要的?因为它将一个网络中间组件能够成为有效的代理(effective agent)的系统,和一个网络中间组件最多只能成为路由器的系统区分了开来。”
理解这一点是很重要的,在HTTP通信链中很容易透明地加入一些新的中间组件,而在某种分布式对象架构风格(例如CORBA、DCOM、EJB)的通信链中很难做到。分布式对象架构风格的架构都是“基于库的API”,并不是说它们使用了网络,就自动成为了“基于网络的API”。

Fielding还说:
这种形式的区分也可以在将消息解释为一个单元(a unit)或者解释为一个流(a stream)中看到。HTTP允许接收者或发送者来自行决定。CORBA的IDL甚至(仍然)不允许使用流,即使当它确实得到了扩展以支持流之后,通信的双方仍然被绑定在相同的API上(译者注:即CORBA的基于库的API),而不是能够自由地使用最适合于它们的应用类型的东西。
    
CORBA为什么逐渐被废弃了,变成了一种遗留的架构?很多人会马上说那是因为它的复杂性。那么它为什么一定要设计的这么复杂?几乎没有人能回答。很多年以来,国内的架构师看到软件大厂所鼓吹的架构你方唱罢我登场,疲于跟随。Fielding为我们清晰地指出了这些遗留架构中存在的本质问题。

在“REST不是一种RPC”中,Fielding明确指出了REST架构风格与RPC架构风格之间的区别。
Fielding写道:
“将HTTP和RPC区分开的并不是语法,甚至也不是使用一个流作为参数所获得的不同的特性,尽管它帮助解释了为何现有的RPC机制对于Web来说是不可用的。使得HTTP与RPC存在重大不同的是:请求是使用具有标准语义的通用的接口定向到资源的,这些语义能够被中间组件和提供服务的来源机器进行解释。结果是使得一个应用支持分层的转换(layers of transformation)和间接层(indirection),并且独立于消息的来源,这对于一个Internet规模、多个组织、无法控制的可伸缩性的信息系统来说,是非常有用的。与之相比较,RPC的机制是根据语言的API(language API)来定义的,而不是根据基于网络的应用来定义的。”
也就是说,RPC没有使用统一的接口,因此实现RPC风格API的服务器可伸缩性要比实现REST风格API的服务器差的多。

同时,Fielding还指出了“HTTP并不是一种传输协议”。很多人把HTTP看作一种能够穿越防火墙的简单易用的传输协议,仅仅是因为HTTP的消息体可以包含任意的内容。

“REST不是一种RPC”和“HTTP并不是一种传输协议”是关于HTTP和REST的最大的两个误解,而且这两个误解通常会同时出现,其影响范围非常广阔。
虽然有很多人使用以REST架构风格设计的HTTP/URI协议来实现一种RPC风格的API,并且仅仅将HTTP看作一种传输协议,但是必须明确指出,以这样的方式使用HTTP是一种巨大的误导。SOAP就是这两个误解的一个典型的例子,关于SOAP的问题,我们在以后的章节再详加探讨。

最后,Fielding还讨论了REST对于媒体类型设计的影响。
在这一部分中,我们需要注意的是REST并非对于所有的媒体类型一视同仁,事实上,REST会淘汰掉那些不适合其架构要求的媒体类型。目前HTML是最适合REST要求的媒体类型,REST也是特别针对HTML优化过的。使用其他的媒体类型,特别是一些自定义的二进制格式,在REST架构风格中未必会得到理想的结果。

Fielding举了JavaScritp最终战胜Java Applet的例子。虽然JavaScript在很多方面代表了应用开发语言发展的趋势(函数化、动态类型等等),在我看来比Java更为理想,而且JavaScript还是真正的Web标准,但是JavaScript战胜Java Applet并不是因为这些原因。
“JavaScript更好地适合于Web技术的开发模型。它具有低得多的门槛,既是因为它作为一种语言的总体复杂性比较小,也是因为一个新手程序员将他最初的工作代码整合起来需要花费的努力比较小。JavaScript对于交互的可见性所产生的影响也比较少。独立的组织能够按照与复制HTML相同的方式来阅读、验证和复制JavaScript的源代码。与之相反,Java是作为二进制包下载的——用户因此必需信任Java执行环境中的安全限制。同样,Java拥有很多更多的功能,允许这些功能存在于一个安全环境中被认为是很可疑的,包括将RMI请求发送到来源服务器的能力。RMI并不支持中间组件的可见性。
    也许两者最重要的区别是,JavaScript仅仅导致了很少的用户可觉察的延迟。JavaScript通常作为主要表述的一部分来下载,然而Java applet要求一个独立的请求。Java代码一旦被转换为字节代码的格式,要比通常的JavaScript代码大得多。最后一点是,当HTML页面的其余部分正在被下载时JavaScript就能够执行,Java则要求包含类文件的完整的包被下载并且安装之后,应用才能够开始执行,因此Java并不支持增量的呈现。”
    
简而言之,JavaScript战胜Java Applet是因为它更符合REST的要求,在REST的严格挑选之下,它成为了最终的胜利者。

到了这里,我们关于Fielding博士论文的导读就结束了。


原创粉丝点击