以文本方式查看主题

-  W3CHINA.ORG讨论区 - 语义网·描述逻辑·本体·RDF·OWL  (http://bbs.xml.org.cn/index.asp)
--  『 Web Services & Semantic Web Services 』  (http://bbs.xml.org.cn/list.asp?boardid=10)
----  《RESTful Web Services 中文版》连载 —— 《前言》  (http://bbs.xml.org.cn/dispbbs.asp?boardid=10&rootid=&id=62708)


--  作者:admin
--  发布时间:5/16/2008 5:54:00 PM

--  《RESTful Web Services 中文版》连载 —— 《前言》

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

前言

复杂的系统总是由简单的系统演变而来的。

—John Gall

Systemantics

我们写这本书,是要告诉你一项令人瞩目的新技术。喏,它很热门,它会彻底改变我们编写分布式系统的方式。我们要讲的是万维网(World Wide Web,简称Web)。

没错,Web不是什么新技术,也不如昔日那么火了,而且从技术角度来看,它并不是那么令人瞩目。但它的确改变了我们许多。这10年来,Web已经改变了我们生活的方式;不过,更多潜在的改变将等待我们。Web是简单的、无所不在的;然而,它作为分布式编程平台的潜力却被忽视了。我们编写本书的目的,就是要让大家体验Web的这种潜力。

说Web作为分布式编程平台的潜力被忽视了,这听上去也许令人感到诧异。毕竟,本书还要与其他Web服务相关书籍竞争。问题是,大部分如今的 “Web服务”都与Web毫无干系。它们采用像COM、CORBA那样的重量级分布式对象访问架构——这与Web的简单性背道而驰。如今的“Web服务” 架构重复或忽略了Web赖以成功的每一种特性。

其实并不需要那样。Web背后的(underlying)技术足以支撑强大的远程服务——其实那些服务就存在着,而且我们每天都在使用它们。这种服务可以延伸到巨大的规模——其实这已经实现了。以Google搜索引擎为例,它不就是一个对海量数据库进行查询并返回结构化搜索结果的远程服务吗?通常,我们不把网站(web site)当作“服务(service)”来看,因为网站的最终用户是人,而服务是程序之间的对话。但网站就是服务。

每个Web应用(包括每个网站)都是一个服务(service)。只要遵从Web的理念而不是违反它,只要不把Web特有的能力隔离在很多层抽象之下,你就能够让可编程应用(programmable applications)利用这种能力。现在是让“Web服务”回归“Web”理念的时候了。

令网站易于被上网者使用的那些特性,同样也令Web服务API易于被程序员所使用。为寻找服务的设计原则,我们可以从网站的设计原则着手,并思考如果网站的使用者不是人而是程序,那最终将得到什么样的设计原则。

本书就是这么做的。我们的中心目标,就是展示Web基础技术(HTTP应用协议、URL命名标准、XML标记语言)的强大能力、适用场合及局限等。本书主要讲的是Web背后的(underlying)一套设计原则——表示性状态转移(Representational State Transfer),或简称为REST。我们率先为“REST式(RESTful)”Web服务提出了最佳实践(best practices)。我们不会采用含糊或臆断性的语言,相反,将用具体的建议来取代那些坊间传言(folklore)和隐性知识。

我们引入了面向资源的架构(Resource-Oriented Architecture,ROA)作为用于设计REST式Web服务(RESTful web services)的一组切合实际的原则。我们还会教你如何编写客户端程序来调用REST式服务。我们将采用一些真实的REST式服务作为案例,比如: Amazon S3(Simple Storage Service)、各种Atom发布协议的变形,以及Google Maps等。我们也会举一些流行的但不符合REST原则的例子(比如 del.icio.us 的社会性书签API),然后对它们进行重构。

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

简单的Web

为何我们对Web如此着迷,以至于认为它能解决所有问题?也许我们上当了,成为炒作的受害者。尽管HTTP并不是最受欢迎的 Internet协议,但Web无疑是被炒作得最凶的一种Internet技术。据统计,全球Internet流量中的大部分源自电子邮件(归因于垃圾邮件)或BitTorrent(归因于侵犯版权)。假如明天Internet就不复存在,那么人们最为怀念的将是电子邮件。那为何要如此重视Web呢?是什么使得HTTP——一种为物理实验室之间传递研究记录而设计的协议——同时能够适合分布式Internet应用呢?

实际上,说HTTP是为某某目的而设计的,对它是一种极大的恭维。已经有人说了,HTTP与HTML是“Internet协议里的放屁坐垫(Whoopee Cushion)与欢乐蜂鸣器(Joy Buzzer),只能搞些小把戏”(译注1)——这还是一个喜爱它们的人说的。(注1)第一版HTTP(即HTTP 0.9)的确看上去像是搞小把戏,比如下面这个客户端与服务器交互的例子:


客户端请求                   服务器响应

GET /hello.txt              Hello, world!

就这么简单。你连接到服务器,把文档路径给它,然后服务器就把文档内容返回给你。HTTP 0.9 差不多就只支持这么些了,看似只是毫无特色地照搬比它复杂一点的文件传输协议(FTP)。
令人意外的是,答案基本就是这样。我们可以半开玩笑地讲,HTTP是特别适合分布式Internet应用的,因为它没有值得一提的特性。你讲要什么,它就给什么。跟功夫片里的手法同出一辙,(注2)HTTP的缺点转化成了优势,它的简单性转化成了强大能力。

HTTP 0.9是特地设计成那么简单的。从HTTP 0.9中我们可以看到可寻址性(addressability)和无状态性(statelessness)——正是这两条基本设计原则,令HTTP较其同类更加优秀,并得以延伸到今天如此巨大的规模。在HTTP 0.9所缺少的特性中,大部分已被证实是多余的,甚至是有副作用的(其实,添加那些特性将有损于Web),而其余的许多特性已在HTTP 1.0和1.1版中实现了。Web赖以成功的另两项重要技术是URL和HTML(以及后来的XML)——它们在许多重要方面也是简单的。

显然,这些“简单的”技术在功能上已足够强大,它们支持Web及Web上的应用。在本书中,我们进一步提出:万维网(World Wide Web)是一个简单而灵活的分布式编程环境。而且我们认为其原因是:为人类使用而设计的human web,跟为软件程序调用而设计的“programmable web”没有本质区别。我们认为,如果Web能很好地为人类所用,那么它也同样能很好地为程序所用。我们只要多作一些考虑即可。计算机程序擅长于构建和解析复杂的数据结构,但是在理解文档方面,它们无法做到像人类一样灵活。

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/


复杂的大Web服务

用作构建Web服务的协议与标准有很多,它们大多是构筑在HTTP之上的。这些标准被统称为WS-*标准栈。它们包括WS- Notification、WS-Security、WSDL及SOAP等。在本书中,我们用“大Web服务(Big Web Services)”来称呼这些技术,以较为礼貌地表达我们对它的鄙视。
本书不对这些标准作详细介绍。我们觉得,实现Web服务并不一定非得用大Web服务(Big Web Services),相反,用Web就足够了。我们相信,Web基础技术足以成为默认的分布式服务平台。

对于某些WS-*标准(例如SOAP),你可以做到使用它们,而不违反REST及面向资源的架构(Resource-Oriented Architecture)。但实际上,它们被用于实现基于HTTP的远程过程调用(Remote Procedure Call,RPC)。有时,采用RPC式架构是合适的;有时,有比Web的优点更为重要的需求得考虑。这没关系。

不必要的复杂性是我们所不期望的。有些场合,采用普通老式的HTTP(plain old HTTP)就足以应付了,但程序员或公司却经常采用大Web服务(Big Web Services)。这样做的效果是,HTTP成为一种用于传输庞大XML负载(payload)的协议,而“真正的”描述信息在XML里。最终的服务变得太过复杂、难于调试,而且要求客户端必须与服务端具备同样的配置环境。

大Web服务(Big Web Services)确实有个优点:现今的开发工具使你只需点一下鼠标,就可以根据代码生成Web服务(尤其是Java或C#开发)。在用这些工具来生成符合WS-*标准栈的RPC式Web服务时,REST式Web服务的简单性这一优点对你来说也许已经不重要了,因为这些工具已经隐藏了所有的复杂性,不用让你操心。毕竟带宽和CPU都便宜。

假如是在同质(homogeneous)环境中,且服务都在同一个防火墙之后的话,那么这种做法是可行的。如果你的机构有足够的政治影响力,可以要求防火墙外的其他机构也采用跟你们同样的方式。但是,假如期望服务逐渐发展到Internet规模,就必须能够应付没有事先考虑到的客户端,比如采用你从没想到的自定义软件栈(software stacks)来访问服务。用户会试图把你的服务与其他你没听说过的服务集成起来。貌似很难?这在Web上已经司空见惯了。

抽象(abstraction)也不是完美的。每增加一个层次,就多一些故障点(failure point)、互操作难题和可伸缩性问题。现代化开发工具能够隐藏复杂性,但它们无法为引入复杂性解释原因——它们就知道不断地增加层次。要令服务融入 Web,就要在适应性(adaptability)、可伸缩性(scalability)及可维护性(maintainability)方面多加注意。而简单性(simplicity)——HTTP 0.9被忽视的优点——则是这三者的先决条件。系统越复杂,出错时纠正起来就越困难。

如果你提供REST式Web服务,可以把复杂性投入“实现更多特性、实现不同服务之间的交互”中。成功地提供服务,意味着不光把服务构筑在Web “之上”,而且应该令服务融入Web:设计服务时,应遵循跟“良好的网站设计所采用的”一样的原则。跟基本的Web协议靠得越拢,这就越容易实现。

关于REST

REST虽然简单,但它是良好定义的(well-defined);而且,不能因为“Web服务和网站是一样的东西”,就以简单性作为“把 Web服务实现成功能贫乏的网站”的借口。可惜到目前为止,主要的REST参考文献只有Roy Fielding 2000年的博士论文第5章——就博士论文来说,那是很好的资料;不过对于大部分现实问题,它并没有给出回答。(注3)这是因为,那篇博士论文不是把 REST作为一种架构来诠释,而是把REST当作一种评判架构的方法来介绍的。“REST式(RESTful)”这个术语就像“面向对象(object- oriented)”这个术语一样。一门语言、一种框架或一个应用也许是采用面向对象的方法设计的,但这并不一定确保其架构是面向对象的架构。(注3)

即使采用像C++或Ruby这样面向对象语言,也有可能写出不是真正面向对象的程序出来。按照REST的标准,HTTP在理论上是很好的。(这是理所当然的,因为Fielding参与了HTTP标准的编写,而且他在博士论文里描述了Web的架构。)不过,在现实中,网站、Web应用及Web服务等经常与REST原则相违背。那么,在对具体的Web服务进行设计时,你怎样才能确信自己正确运用了REST原则呢?

大部分关于REST的信息来源都是非正式的:邮件列表(mailing list)、Wiki、博客等(我在附录A中给出了一些很好的链接)。直至如今,REST的最佳实践(best practices)还只是一些坊间传言(folklore)。所需要的,是一个以REST元架构(meta-architecture)为基础的具体架构(architecture):一套为设计“体现了Web的潜力”的服务而制定的基本准则——我们将在第4章介绍这样一种架构,即面向资源的架构(Resource-Oriented Architecture,简称ROA)。它并不是唯一的高层(high-level)REST式架构,但是我们认为,用它来设计“易于为客户端所用”的 Web服务是很好的。

我们通过制定这个面向资源的架构(ROA),把来自坊间传言(folklore)的经验提炼为Web服务设计的最佳实践(best practices)。它们仅作为一个建议的基线(suggested baseline)。如果你曾经力图领会REST,希望我们的架构能够令你自信地认为所做的是“真正的”REST。我们也希望,面向资源的架构(ROA)能够帮助整个社区加快提出并系统化最佳实践我们希望让程序员可以轻易地构建优雅的、符合设计用途的、融入Web的(而不是仅仅构筑在Web之上的)分布式 Web应用。

但我们知道,你光掌握这些技术还是不够的。我们两人都曾有过这样的经历:所工作的机构,在主要的架构决策上没有按照我们的意思去做。如果没机会采用 REST式架构,你是无法在REST式架构方面取得成功的。因为,除了技术知识以外,我们还必须教你怎样用言辞去推荐REST式解决方案。我们已经把面向资源的架构(ROA)定位为一种替代RPC式架构(即如今的SOAP+WSDL服务所采用的)的简单方案。RPC式架构通过一个复杂的、编程语言式的接口,来暴露其内部算法(algorithms)——这种接口,每个服务都各不相同。而ROA则通过一个简单的、文档处理接口,来暴露其内部数据(data)——这种接口是统一的。我们将在第10章对这两种架构加以比较,并教你如何来推荐采用ROA。

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

统一两种Web

程序员把网站作为Web服务来用,这已经有好多年了——当然,是非正式的。(注4)要让计算机理解供人类阅读的网页,是比较困难的,但这并没有阻止程序高手们用自动化客户端来抓取网页,然后用屏幕抓取程序来提取有关信息。随着时间的推进,出现了RSS、XML-RPC和SOAP这些对程序员友好的技术——它们用于按正式认可的方式来暴露网站功能。这些技术形成了一种programmable web,它是对human web的扩展,它为软件程序提供了方便。

我们写这本书的最终目的,是统一programmable web与human web。我们设想的是单个互联的网络:一个“在一组服务器上运行,采用一套协议并遵从一组设计原则”的万维网(World Wide Web)。那是一个既可被人也可被计算机程序使用的网络。

若不是得益于分配不当的国防资金、创新的工程项目、“差点则更好”(worse-is-better)的工程实践、大科学、天真的自由理想主义、古怪的自由主义政治、技术迷信,以及那些认为自己找到了发财之路的程序员和投资者的汗水与资本,因特网(Internet)和万维网(World Wide Web)也许不一定会诞生。

Web作为一个简单的、开放的(至少现阶段)和基本统一的网络应用平台,它容纳了大量的人类知识,并为人类所努力钻研的许多领域提供支持。我们认为,现在应该开始认真考虑将网站设计原则应用于服务设计,以便自动化客户端可以访问那些信息和算法。假如同意的话,本书将教你怎么做。

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

本书的内容

在本书中,我们主要关注一些实际问题:如何设计与实现REST式Web服务(RESTful Web Services),以及调用它们的客户端。其次我们还关注于理论方面:什么是REST式架构风格?为什么Web服务应该尽量符合REST风格?我们的讨论不会涵盖方方面面,但是力图切入如今的重点话题。因为这是同类书中的第一本,我们会不断反复这一关键问题——即如何设计一个REST式服务。

前3章,将从客户端的角度来介绍Web服务,并告诉你REST式服务有什么特别不同。第1章  Programmable Web及其分类
我们在这一章对Web服务作总体性介绍。Web服务运行于Web上,请求外部服务器提供数据或运行算法的程序。我们将展示三种常见的Web服务架构: REST式架构、RPC式架构及REST-RPC混合架构。我们会为每一种架构举一个HTTP请求/响应的例子,并给出典型的客户端代码。

第2章  编写Web服务客户端

在这一章,我们教你如何用HTTP库和XML解析器来为现有Web服务编写客户端。我们将介绍一个流行的REST-RPC服务—— del.icio.us Web服务,并用Ruby、Python、Java、C#及PHP等语言来示范其客户端代码。对于其他一些语言,我们虽然没有展示代码,但会推荐一些适用的HTTP库和XML解析器。JavaScript和Ajax将在第11章中作单独介绍。

第3章  REST式服务有什么特别不同?

我们将吸取第2章的经验,并把这些经验运用于一个纯REST式服务——Amazon S3(Simple Storage Service)。我们将在构建S3客户端时举例说明一些重要的REST概念:资源(resource)、表示(representation)、统一接口(uniform interface)。
下面6章是本书的核心,它们关注于如何设计和实现自己的REST式服务。

第4章  面向资源的架构

本章正式介绍REST:不是抽象地介绍,而是以一个具体的Web服务架构为背景进行介绍。我们的架构基于四个重要的REST概念:资源、资源名称、资源表示(representation),以及资源间的链接。它的服务应根据四个REST特征来评判:可寻址性、无状态性、连通性和统一接口。

第5章  设计只读的面向资源的服务

我们提出了“把想法或需求转变为REST式资源”的系列步骤。这些资源是只读的,也就是说客户端可以从服务获取数据,但是不能改变服务的数据。我们以一个设计地图服务(类似Google Maps)的例子来说明这些步骤。

第6章  设计可读写的面向资源的服务

我们扩展了上一章的步骤,以允许客户端创建、修改并删除资源。我们以为地图服务新增两种资源(用户账户和自定义地点)为例,示范这些步骤。

第7章  一个服务实现

我们把一个RPC式服务(即第2章那个del.icio.us REST-RPC混合服务)重构为一个纯REST式服务。然后,我们把这个服务实现为一个Ruby on Rails应用。通过这一章,你可以把所学到的知识操练一遍!

第8章  REST和ROA最佳实践

在这一章,我们把之前给出的关于服务设计的建议汇集起来,并补充一些新的建议。我们将告诉你如何利用HTTP的标准特性来解决常见问题和进行优化。我们还将为棘手的特性(比如事务,也许你认为这没法用REST式Web服务来实现)给出面向资源的设计。

第9章  服务的技术构件

我们在这一章描述在REST的三大技术(HTTP、URI和XML)之上的其他技术:有些是用于传递状态的文档格式,比如XHTML及其微格式(microformat);有些是用于为客户端状态推进的超媒体格式,比如WADL;有些是用于构建REST式Web服务的控制流,比如Atom发布协议。
最后3章涉及一些专门话题,这些专题各自都可单独作为一本书来讨论。

第10章  面向资源的架构 VS 大Web服务

将拿我们的架构及一般的REST架构跟其他著名架构来进行比较。我们认为REST式Web服务较“基于SOAP、WSDL和WS-*”的服务而言,更为简单、可伸缩性更好、更易于使用、更加符合Web的理念,且更能应付各种各样的客户端。

第11章  将Ajax应用作为REST客户端

在这一章,我们将以Web服务来阐述用于Web应用的Ajax架构:一个Ajax应用,就是一个在浏览器里运行的Web服务客户端。本章是对第2章内容的延伸。我们教你如何用XMLHttpRequest和标准的JavaScript库来编写用于REST式Web服务的客户端。

第12章  REST式服务框架

在最后这一章里,我们将讨论三种流行的框架:Ruby on Rails、Restlet(用于Java)和Django(用于Python)。它们可以简化REST式Web服务的开发。

我们还有3个附录,希望对你有用。

附录A  REST相关资源与REST式资源

第一部分列出了一些与REST式Web服务有关的标准、教程和社区。第二部分给出了一些现有的、公开的、可供你调用和学习的REST式Web服务。

附录B  42种常见的HTTP响应代码

这部分解释标准HTTP的每一个响应代码(以及一个扩展),并解释何时你会在REST式Web服务里用到这些代码。

附录C  常见的HTTP报头

这部分向你解释各种HTTP报头,包括每个标准的HTTP报头,以及一些用于Web服务的扩展报头。

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

你应该阅读哪些部分?

本书的组织,适于那些对Web服务的概况感兴趣的读者:有些读者通过实践学习Web服务,但他们对于Web服务没有太多经验。如果你属于这种情况,可以采取一条最简化的阅读路线,即从本书开头一直读到第9章,然后在剩下的章节中选择你所感兴趣的部分。

如果你经验比较丰富,那么可以采取另一种阅读路线。如果你关心如何为现有服务编写客户端,那么可以只关注第1、2、3和11章——那些关于服务设计的章节对你可能帮助不大。如果你想要创建自己的Web服务,或者想知道REST到底是什么意思,你不妨从第3章开始阅读。如果你想比较REST与WS- *,那么你可以选第1、3、4和10章作为起点。

说明

本书有两位作者(Leonard和Sam),在本书的1-11章,我们将把这两个身份合并为第一人称“我”。而在最后一章(第12章)里,这个第一人称“我”代表的是一大群人,因为有众多Django和Restlet开发者们参与到“展示如何用他们的框架来构建REST式服务”中来。

我们假定你是一名胜任的程序员,但不假定你在Web编程方面有任何经验。我们在书中介绍的内容并不限定于某一编程语言。我们提供了以各种语言编写的REST式客户端及服务的示例代码。除非为了示范特定的框架或语言,否则我们将采用Ruby(http://www.ruby-lang.org)语言来描述。

我们选用Ruby是因为:即使对于不懂Ruby的程序员来说,它也是简洁易读的。(因为这是一门很好的语言,而且它的名字跟Sam的姓有着令人困惑的联系。)Ruby的标准Web框架——Ruby on Rails——也是最主要的一种REST式Web服务实现平台之一。如果你不懂Ruby也没关系,我们在代码中嵌入了许多有助于理解Ruby语法的注释。

本书的示例程序可以从本书官方网站([URL=http://www.oreilly.com/catalog/9780596529260]http://www.oreilly.com/catalog/9780596529260[/URL])上下载,其中包括第7章的Rails应用的完整代码,以及第12章中Restlet和Django应用的相应代码。在书中,许多客户端我们只给出了 Ruby实现;但在示例程序中,你还可以找到它们的Java实现。这些客户端程序采用了Restlet库,并且是由Restlet开发者Jerome Louvel和Dave Pawson编写的。若相对Ruby而言,你更熟悉Java,那么这些代码对你掌握代码背后的概念会有帮助。特别值得注意的是,示例代码里还包括一个完整的第3章Amazon S3客户端的Java实现。

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

致谢

最后,我们要感谢那些令我们得以直接在HTTP上编程的人们。对于Sam来说,是Rael Dornfest和他的Blosxom博客应用。Leonard的经验来自于20世纪90年代中期编写屏幕抓取应用。他感谢那些把网站作为Web服务来设计的人们,特别是那位在线连环画“Pokey the Penguin”的匿名作者。

我们曾有过这样的想法,不过是Roy Fielding充实了它。对于我们,那只是一种感觉,而Roy Fielding则在他的博士论文里命名并定义了它。Roy的理论基础正是我们所赖以为基础的。

在本书编写过程中,我们从REST社区获得了巨大的帮助。我们非常感激给我们反馈的人:Benjamin Carlyle、David Gourley、Joe Gregorio、Marc Hadley、Chuck Hinson、Pete Lacey、Larry Liberto、Benjamin Pollack、Aron Roberts、Richard Walker和Yohei Yamamoto。还有一些人通过他们的作品在不知不觉中帮助我们:Mark Baker、Tim Berners-Lee、Alex Bunardzic、Duncan Cragg、David Heinemeier  Hansson、Ian Hickson、Mark Nottingham、Koranteng Ofosu-Amaah、Uche Ogbuji、Mark Pilgrim、Paul Prescod、Clay Shirky、Brian Totty和Jon Udell。当然,书中的所有看法及可能存在的错误与疏漏都是我们自己的责任。

在本书编写过程中,编辑Michael Loukides为我们提供了很多帮助与智慧。我们还要感谢Laurel Ruma和其他所有为本书出版付出劳动的O’Reilly同事们。

最后,需要特别感谢的是Jerome Louvel、Dave Pawson和Jacob Kaplan-Moss。他们在Restlet和Django方面的经验令我们有了本书的第12章。

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

[此贴子已经被作者于2008-5-16 19:32:37编辑过]

--  作者:admin
--  发布时间:5/16/2008 6:42:00 PM

--  《RESTful Web Services 中文版》连载 —— 《第一章 Programmable Web的分类》

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

第1章 Programmable Web及其分类

在编写计算机程序时,并非只能使用自己设计的算法,你还可以使用编程语言标准库里的算法;另外,也可以从相关图书或第三方库里找到更多的算法。除非在做非常前沿的东西,否则一般不必自己设计算法。

运气好的话,数据也能找到现成的。某些应用是完全由用户所提供数据驱动的。有时,获得数据是轻而易举的。比方说,假如要对垃圾邮件进行分析,你很容易获得所需数据:你可以下载一些公共数据集——单词表、地理数据、素数表、公共领域文本(public domain texts)等——它们就像第三方库一样供你使用。但是,假如你需要其他种类的数据,那就不是这么容易了。哪里有这些数据呢?它们越来越多地来自于 programmable web(译注 )。

作为人类用户,当你要寻找有关某话题的图书时,多半会用浏览器打开一个在线图书馆或在线书店的URI,比方说[URL=http://www.amazon.com/]http://www.amazon.com/[/URL]。

提示:“URL”是一个常被用来表达网址的术语。但在本书中,我将遵从HTTP标准,统一采用“URI”这一术语。因为每个作网址用的URI也是一个URL,所以你可以用“URL”来替换我说的“URI”,它们是一个意思。  

你将收到服务器返回的一个网页,即一个HTML格式的文档,浏览器将把该HTML文档以图形化的方式展示在你面前。你会看到该网页里包含一个搜索表单(form),可以在其中输入要搜索的主题(比如“web services”),然后提交该表单。此时,浏览器将再次发出HTTP请求,这次请求的目标URI将提供你所寻找的主题的搜索结果。继续Amazon的例子,浏览器第二次请求的目标URI可能是这样一个URI:[URL=http://amazon.com/s]http://amazon.com/s[/URL]? url=search-alias%3Dstripbooks&field-keywords=web+services。
位于amazon.com的Web服务器将返回另一个HTML格式的文档,其中包含对搜索结果的描述和指向其他搜索选项的链接等(见示例1-1)。同样地,浏览器将把该文档以图形化的方式展示在你面前,供你浏览,并决定下一个动作。

示例1-1:amazon.com返回的HTML文档(片断)

...
<a href="http://www.amazon.com/Restful-Web-Services-Leonard-Richardson/dp/...>
<span class="srTitle">RESTful Web Services</span>
</a>
  
by Leonard Richardson and Sam Ruby
    
<span class="bindingBlock">
(<span class="binding">Paperback</span> - May 1, 2007)
</span>


human web上充满大量的数据(data):图书信息、评价、价格、到货时间、消息、照片等(当然还有各种垃圾信息)。同时,它也充满大量的服务(services):搜索引擎、在线商铺、博客、wiki、计算器,还有游戏等。不需要把所有这些数据与程序都安装到你的本地计算机上;相反,只需安装一个程序——Web浏览器——就可以通过它来访问网上的数据与程序了。

programmable web也差不多,它跟human web的主要不同在于:programmable web返回的不是令人赏心悦目的HTML页面,而是冷漠刻板的XML文档。programmable web不是专门面向人类用户使用的,它的数据是准备作为“软件程序的输入”用的。

示例1-2是一段Ruby脚本,它用programmable web来完成一项传统的human web的任务:搜索所有匹配某个关键字的图书。它通过采用Ruby/Amazon库([URL=http://www.caliban.org/]http://www.caliban.org/[/URL] ruby/ruby-amazon.shtml)将访问Web的细节隐藏在编程语言接口之后。

示例1-2:一段用于搜索图书的Ruby脚本

#!/usr/bin/ruby -w
# amazon-book-search.rb
require 'amazon/search'

if ARGV.size != 2
  puts "Usage: #{$0} [Amazon Web Services AccessKey ID] [text to search for]"
  exit
end
access_key, search_request = ARGV
req = Amazon::Search::Request.new(access_key)
# 对于搜索结果中的每一本书…
req.keyword_search(search_request, 'books', Amazon::Search::LIGHT) do |book|
  # 打印出书名和作者
  puts %{"#{book.product_name}" by #{book.authors.join(', ')}}
end


你需要注册一个Amazon Web Services账号([URL=http://aws.amazon.com/]http://aws.amazon.com/[/URL]),然后把上述Ruby代码里的Access Key ID改为你自己的Access Key ID,才能成功运行本程序。下面是一个运行本程序的例子: $ ruby amazon-search.rb C1D4NQS41IMK2 "restful web services""RESTful Web Services" by Leonard Richardson, Sam Ruby"Hacking with Ruby: Ruby and Rails for the Real World" by Mark Watson

理想情况下,programmable web在工作方式上跟human web是一致的。当amazon- book-search.rb调用 Amazon::Search::Request#keyword_search 方法时,Ruby程序的作用就好比一个Web浏览器:向[URL=http://xml.amazon.com/onca/xml3?KeywordSearch]http://xml.amazon.com/onca/xml3?KeywordSearch[/URL]= restful+web+services&mode=books&f=xml&type=lite&page=1发出 HTTP请求。位于xml. amazon.com的Web服务器将响应该请求:返回一个XML文档。这个返回的XML文档(如示例1-3所示)如同你在Web浏览器里看到的HTML 文档,包含了对搜索结果的描述,只不过它比HTML文档更加结构化。

示例1-3:xml.amazon.com返回的XML文档(片断)

...
<ProductName>RESTful Web Services</ProductName>
<Catalog>Book</Catalog>
<Authors>
<Author>Leonard Richardson</Author>
<Author>Sam Ruby</Author>
</Authors>
<ReleaseDate>01 May, 2007</ReleaseDate>
...

对Web浏览器来说,它的职责只是发出HTTP请求,然后以一种人类可以理解的方式把服务器的响应呈现出来。它不必思考这个HTTP响应是什么意思 ——那是人类用户的事。而Web服务客户端就没这么轻松了:因为程序是事先编好的,所以它必须既像Web浏览器一样获取数据,还要像“人类用户”一样,判断这些数据是什么意思;Web服务客户端必须自动地从HTTP响应中提取含义,并根据含义决定下一个动作。

示例1-2所示的Web服务客户端先解析XML文档,提取一些相关信息(书名及作者),然后把这些信息在标准输出(standard output)上打印出来。实际上,amazon-book-search.rb这个程序就像一个专用的小型Web浏览器,把数据传递给人类用户。它也可以在无人工干预的情况下,容易地对Amazon的图书数据做一些其他操作,比如:把书名存入数据库,或者以作者信息来驱动一个推荐引擎(recommendation engine)等。
另外,数据并非只能由服务器传向客户端。正如你可以修改human web的某些部分(比如在你的博客上发布文章,或者购买一本书)一样,也可以编写程序来修改programmable
web。你可以把它当作一个存储空间,也可以把它当作一个算法源,具体要看你需要什么样的服务,以及是否有人提供这种服务。

示例1-4显示的是一个修改programmable web的Web服务客户端:Ruby的s3sh命令行解释器(command shell)([URL=http://amazon.rubyforge.org/]http://amazon.rubyforge.org/[/URL])。它是Amazon S3(Simple Storage Service)([URL=http://aws.amazon.com/s3]http://aws.amazon.com/s3[/URL])的各种客户端中的一个。我会在第3章详细介绍S3的工作原理。如果你想学习使用s3sh的话,到时可以仔细研究。

你只要知道Amazon S3的作用是“在桶(一种带标签的容器)里存放对象(一种带标签的数据)”,就不难理解这个s3sh脚本了。s3sh程序在S3之上构建了一个交互式编程接口,而其他一些客户端则把S3用于备份或Web寄存。S3是一个非常灵活的服务。

示例1-4:用s3sh和S3处理programmable web

$ s3sh
>> Service.buckets.collect { |b| b.name }
=> ["example.com"]

>> my_bucket = Bucket.find("example.com")

>> contents = open("disk_file.txt").read
=> "This text is the contents of the file disk_file.txt"

>> S3Object.store("mydir/mydocument.txt", contents, my_bucket.name)

>> my_bucket['directory/document.txt'].value
=> "This text is the contents of the file disk_file.txt"

本章将对programmable web的现状作一个概述:它用到哪些技术,这些技术采用什么架构,以及哪些设计风格比较流行?我会展示一些实际代码及实际HTTP交互的例子,但本章的主要目的,是希望你将万维网(World Wide Web)看成一种把计算机程序彼此衔接起来的方式,就如同使彼此连系起来一样。

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

Programmable Web的分类

programmable web是基于HTTP和XML技术的。虽然HTML、JSON(JavaScript Object Notation)、纯文本或二进制文档也在使用,但是采用较多的还是XML。programmable web还是基于HTTP的:不采用HTTP,就不是基于Web的(注1)。除了这些仅有的共识,剩下的就差不多是争论了。没有固定的术语,而且同一术语(就像“REST”)各人有各人的用法,让人感到含糊和困惑。目前所需要的是一种对programmable web进行统一分类的方法,有了这样的分类,各个术语的含义就清晰化了。
把programmable web设想为一个生态系统,就像海洋一样,容纳着多种多样的奇异生物。旧时的科学家和水手们根据生物的外表来对它们进行分类,于是,鲸被归为鱼类。而现代的科学家们根据动物在生命进化树中的位置来对它们进行分类,所以鲸被放在了哺乳动物一类。对于programmable web上的服务,我们也有两种类似的分类方法:根据所采用的技术(比如URI、SOAP、XML-RPC等),或者根据背后的(underlying)架构与设计思想。(注1)

这两种对海洋生物的分类方法常常和谐相处,没必要通过DNA检测来确定金枪鱼是更像鲶科鱼,还是更像海葵。但是,假如你很想知道为什么鲸在水下不能呼吸的话,你就不能(根据外表)把它归为鱼类,而应(根据内在的构造)将它归为哺乳类(注2)。

就programmable web的分类来说,目前的术语,大部分是根据外表(即所采用的技术)来对服务进行分类的。虽然在大多数情况下没有问题,但它们存在概念上的缺陷,而且会导致“鲸是鱼”的错误。我将提出一种基于架构的分类,并说明如何根据背后的设计原则作技术选择。我会在书中不断提及这里给出的分类,但我主要关心的是那些 programmable web里跟“REST”有一定联系的部分。

[此贴子已经被作者于2008-5-16 19:37:02编辑过]

--  作者:admin
--  发布时间:5/16/2008 6:53:00 PM

--  《RESTful Web Services 中文版》连载 —— 《第一章 Programmable Web的分类》续

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

HTTP:信封里的文档

假如要我对海洋动物进行分类的话,我会先从一些共性(比如DNA、分子结构、胚胎发育规律,等等)开始讨论,然后说明为什么在这些共性上的分化会导致动物之间的差别。为了对programmable web进行分类,我想先对HTTP(所有Web服务共有的协议)作一个概述。
HTTP是一种基于文档的协议。客户端把文档放在信封(envelope)里,然后发给服务器;作为回应,服务器把响应文档放在信封里,然后发给客户端。HTTP对信封格式有严格的标准,但它并不关心信封里面的内容。示例1-5是一个信封的例子:当访问oreilly.com

主页时,Web浏览器发出HTTP请求(HTTP request)。我截断了其中超长的两行,以便适应印刷页面的大小。

示例1-5:一个发给[URL=http://www.oreilly.com/index.html]http://www.oreilly.com/index.html[/URL]的HTTP GET请求

GET /index.html HTTP/1.1
Host: www.oreilly.com
User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.7.12)...
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,...
Accept-Language: us,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-15,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive

也许你对HTTP还不熟悉,所以我来讲解一下上述HTTP请求的各个主要部分。全书都采用这里的术语。

HTTP方法(HTTP method)

对于这个请求,方法是“GET”。在其他讨论“REST”的地方,你会看到HTTP方法也被称作“HTTP动词(HTTP verb)”或“HTTP动作(HTTP action)”。

HTTP方法的名称,就如同编程语言里的方法名称,表示客户端希望服务器如何处理该信封。这里,客户端(即我的Web浏览器)希望从服务器([URL=http://www.oreilly.com/]www.oreilly.com[/URL])获取(GET)一些信息。

路径(path)

这是URI里主机名(hostname)后面的部分。对于这个请求,URI是[URL=http://www.oreilly/]http://www.oreilly[/URL]. com/index.html,路径是“/index.html”。拿信封作比喻的话,路径就是信封上的地址。在本书中,有时“URI”只表示路径。

请求报头(request headers)

它们是一组关键字-值对(key-value pairs),起元数据(metadata)的作用,就像贴在信封上、写有信息的不干胶标贴一样。这个请求有8个报头(headers):Host、User-Agent、Accept,等等。关于HTTP报头有一个标准(参见附录C),不过应用程序也可以定义自己的报头。
实体主体(entity-body),也称作文档(document)或表示(representation)

这是放在信封里的文档。本例是一个没有实体主体的HTTP请求——也就是说,信封里面是空的!一般来说,GET请求都没有实体主体——完成请求所需的全部信息都在路径(path)和报头(headers)里。

HTTP响应(HTTP response)也是一个放在信封里的文档。HTTP响应跟HTTP请求在形式上差不多。在浏览器向位于oreilly.com的服务器发出如示例1- 5所示的请求后,该服务器将向Web浏览器作出响应。示例1-6显示的是经简化的HTTP响应。

示例1-6:对发给[URL=http://www.oreilly.com/index.html]http://www.oreilly.com/index.html[/URL]的HTTP GET请求的响应

HTTP/1.1 200 OK
Date: Fri, 17 Nov 2006 15:36:32 GMT
Server: Apache
Last-Modified: Fri, 17 Nov 2006 09:05:32 GMT
Etag: "7359b7-a7fa-455d8264
Accept-Ranges: bytes
Content-Length: 43302
Content-Type: text/html
X-Cache: MISS from www.oreilly.com
Keep-Alive: timeout=15, max=1000
Connection: Keep-Alive

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
...
<title>oreilly.com -- Welcome to O'Reilly Media, Inc.</title>
...

HTTP响应可分为三个部分。

HTTP响应代码(HTTP response code)
它是一个通知客户端“HTTP请求是成功还是失败”的数字代码,并告诉客户端应如何对待信封及信封里的内容。对于本例,响应代码是200(“OK”),可见GET操作成功了。我将在附录B对各种HTTP响应代码进行解释。

响应报头(response headers)

跟请求报头(request headers)一样,响应报头也是贴在信封上的“不干胶标贴”。本例的HTTP响应有11个报头:Date、Server,等等。
实体主体(entity-body)或表示(representation)

同样地,它也是放在信封里的文档。不过这次它不是空的!该实体主体实现(fulfill)了我的GET请求。 HTTP响应里其余的部分,就像是粘有不干胶标贴的信封,告诉Web浏览器如何来处理这个文档。

有一些非常重要的标签是值得单独提及的。响应报头Content-Type给出实体主体的媒体类型(media type)。对于本例,媒体类型是text/html——这告诉Web浏览器,它可以把实体主体作为一个HTML文档(也就是一个网页)来呈现。

关于媒体类型有一个标准([URL=http://www.iana.org/assignments/media-types/]http://www.iana.org/assignments/media-types/[/URL])。最常见的媒体类型有文本文档(text/html)、结构化数据文档(application/xml)及图像(image/jpeg)等。在别处讨论 REST或HTTP时,你会看到媒体类型也被称作“MIME类型”、“内容类型(content type)”或“数据类型(data type)”。

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

方法信息

HTTP是programmable web上的所有服务所共有的。 而Web服务之间差别的形成,是因为它们在两个问题上的做法有所不同。 你只要了解一个Web服务是如何处理这两个问题的,就可以清楚地知道该服务是否跟Web的理念保持一致。

第一个问题是:客户端如何把自己的意图传达给服务器。 服务器怎么知道该请求是要获取数据、删除数据、还是改写数据呢? 服务器为什么要做这个操作,而不是做那个操作呢?
这种关于对数据采取什么操作的信息,我称之为方法信息(method information)。 要在Web服务里传达方法信息,一种方式是把它放在HTTP方法(HTTP method)里——这是REST式Web服务(RESTful Web Services)的做法,后面我还会详细讲述。 现在,请记住五种最常见的HTTP方法是GET、HEAD、PUT、DELETE和POST。 通过它们足以分辨“获取数据”(GET)、“删除数据”(DELETE)以及“改写数据”(PUT)。
HTTP方法具有标准化的名称,这是一个很好的优点。 当然,跟编程语言的方法名称相比,HTTP方法的名称要少得多。 有些Web服务喜欢在HTTP请求里的其他地方(比如URI路径或请求文档里,而不在HTTP方法里)寻找针对特定应用的方法名称。

例1-7就是一个把方法信息放在URI路径(path)里的Web服务客户端的例子。该客户端调用的是用于在线照片分享的Flickr Web服务,它将搜索Flickr上的照片。 要运行本程序,你得先申请一个Flickr账户,并申请API key(http://www.flickr.com/services/api/keys/apply/)。

例1-7 搜索Flickr上的照片
#!/usr/bin/ruby -w
# flickr-photo-search.rb
require 'open-uri'
require 'rexml/document'

# 返回指向一个小尺寸的Flickr照片的URI
def small_photo_uri(photo)
server = photo.attribute('server')
id = photo.attribute('id')
secret = photo.attribute('secret')
return "http://static.flickr.com/#{server}/#{id}_#{secret}_m.jpg"
end

# 在Flickr上搜索与给定tag相匹配的所有照片,并打印出照片的URI。
# 对于每一个搜索结果
def print_each_photo(api_key, tag)
# 构造URI
uri = "http://www.flickr.com/services/rest?method=flickr.photos.search" +


"&api_key=#{api_key}&tags=#{tag}"

# 发出HTTP请求,获取实体主体
response = open(uri).read

# 把实体主体作为XML文档来解析
doc = REXML::Document.new(response)

# 对于每个找到的照片…
REXML::XPath.each(doc, '//photo') do |photo|
# …生成URI,并打印出来
puts small_photo_uri(photo) if photo
end
end

# 主程序
if ARGV.size < 2
puts "Usage: #{$0} [Flickr API key] [search term]"
exit
end

api_key, tag = ARGV
print_each_photo(api_key, tag)


XPath介绍
XPath是一个特定领域的语言。利用它,不用写许多代码就能对XML文档进行切分。 它有许多令人望而却步的特性,不过你只要掌握少许知识就明白怎么用了。关键一点是:把XPath表达式看成一种从XML文档里提取标签(tags)或元素(elements)的规则。 本书中出现XPath表达式不会很多,我会在每次使用时加以解释。
要知道一个XPath表达式是什么意思,你只要从右往左读就行了。比如,表达式 //photo 的意思是:

寻找所有photo标签 photo
无论它在文档里出现在何处 //

用REXML::XPath.each(doc, '//photo') 这行简单的Ruby代码可以实现遍历每一个photo标签(tag),而不必遍历整个XML树。


该程序向http://www.flickr.com/services/rest?method=flickr.photos.search&api_key=xxx&tag=penguin这个(或类似)URI发出HTTP请求。那么服务器是怎么知道客户端想要做什么的呢?  嗯,URI表明方法名称是flickr.photos.search;不过为什么HTTP方法(HTTP method)是GET呢?嗯,我确实是要获取(get)信息,所以,没准HTTP方法里的才是真正的方法信息。

不过这一假设不是总能成立。因为Flickr API除了“获取(GET)”型方法(比如flickr.photos.search和flickr.people.findByEmail)以外,还支持很多其他方法(比如flickr.photos.addTags和flickr.photos.comments.deleteComment等等),而所有这些方法,无论它们是否真的“获取(GET)”数据,都是通过GET请求来调用的。 可见,Flickr把真正的方法信息放在查询变量(query variable)method里的,并希望客户端忽略HTTP方法(HTTP method)。

相比之下,SOAP服务一般是把方法信息放在实体主体(entity-body)和HTTP报头(HTTP header)里的。 例1-8是一段Ruby脚本,它用Google SOAP API来搜索Web。

例1-8 用Google的搜索服务来搜索Web
#!/usr/bin/ruby -w
# google-search.rb
require 'soap/wsdlDriver'

# 做一次Google搜索,并把各个搜索结果打印出来
def print_page_titles(license_key, query)
wsdl_uri = 'http://api.google.com/GoogleSearch.wsdl'
driver = SOAP::WSDLDriverFactory.new(wsdl_uri).create_rpc_driver
result_set = driver.doGoogleSearch(license_key, query, 0, 10, true, ' ',
 false, ' ', ' ', ' ')
result_set.resultElements.each { |result| puts result.title }
end

# 主程序
if ARGV.size < 2
puts "Usage: #{$0} [Google license key] [query]"
exit
end

license_key, query = ARGV
print_page_titles(license_key, query)

正当我在写这本书时,Google宣布弃用SOAP搜索服务,转而建议采用一种REST式、面向资源的服务(不过,它在使用上要受到条款限制,而SOAP版服务没有这一限制)。 因为Google的SOAP服务是我所知道的最好的例子,而且我也相信你不会实际去运行这个程序,所以我就没修改这个例子了。 你只要看一下这段代码及相关的SOAP和WSDL文档就好了。

嗯,也许从这段代码中看不出什么,因为WSDL库(WSDL library)已经隐蔽了大部分细节。 实际的细节是这样的:当你调用doGoogleSearch方法时,WSDL库会向位于http://api.google.com/search/beta2的Google SOAP服务“端点(endpoint)”发出一个POST请求。所有对Google SOAP API的调用都采用这个URI,而且HTTP方法(HTTP method)总是POST。

所有这些细节(比如doGoogleSearch的定义)都是在WSDL文档(参见http://api.google.com/GoogleSearch.wsdl)里定义的,例1-9给出了定义doGoogleSearch的片断。

例1-9 Google搜索服务的WSDL文档片断
<operation name="doGoogleSearch">
<input message="typens:doGoogleSearch"/>
<output message="typens:doGoogleSearchResponse"/>
</operation>

由于每次调用都采用同一个URI和相同的HTTP方法(HTTP method),所以方法信息(比如这里的“doGoogleSearch”)无法放在URI或者HTTP方法里, 而只能放在POST请求的实体主体(entity-body)里。 例1-10显示了一个用于搜索“REST”的HTTP请求。

例1-10 一个作SOAP RPC调用的HTTP请求
POST search/beta2 HTTP/1.1
Host: api.google.com
Content-Type: application/soap+xml
SOAPAction: urn:GoogleSearchAction

<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<gs:doGoogleSearch xmlns:gs="urn:GoogleSearch">
<q>REST</q>
...
</gs:doGoogleSearch>
</soap:Body>
</soap:Envelope>

这里的方法信息是“doGoogleSearch”。 我们注意到,上述SOAP信封(Envelope)里有一个XML标签(tag)的名称是 doGoogleSearch,它跟例1-9所示WSDL文档里的那个操作(operation)的名称相同,跟例1-8所示Ruby脚本里的一个方法名称也相同,而且跟HTTP请求报头里的SOAPAction值也相同(有些SOAP实现,会在HTTP请求报头、而不是实体主体里寻找方法信息)。

看过Google SOAP API的例子之后,让我们言归正传,现在来看一下Google搜索引擎本身。 要用浏览器在Google上搜索“REST”,你要向http://www.google.com/search?q=REST发送GET请求,然后你会得到一个HTML响应。 这里,方法信息是放在HTTP方法里的:你在获取(GET)搜索结果列表。

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

作用域信息

第二个问题是:客户端如何告诉服务器对哪些数据进行操作。 假如服务器已经知道客户端要删除数据了,但它怎么知道客户端要删除的是哪些数据呢? 为什么服务器要对这些数据、而不是对那些数据进行操作呢?

我称这种信息为作用域信息(scoping information)。 一个容易想到的放置作用域信息的地方是URI路径,其实大部分网站也都是这么做的。我们再来考察一下 http://www.google.com/search?q=REST 这个擎URI:这里的方法信息是“GET”,作用域信息是“/search?q=REST”,客户端是要获取(GET)一个有关“REST”的搜索结果列表,而不是获取有关“水母(jellyfish)”的搜索结果列表(其作用域信息应该是“/search?q=jellyfish”)、Google首页(其作用域信息应该是“/”)、或其它什么。
很多Web服务都把作用域信息放在URI路径里,Flickr API就是一例: 它的URI里有许多查询变量(query variables)都属于作用域信息,比如 tags=penguin 限定了 flickr.photos.search 这个方法只搜索具有“penguin”标签的照片。 如果一个服务,其方法信息(method information)是一个编程语言的意义上的方法(method),那么可以把它的作用域信息看成是该方法的一组参数。例如,你完全可以把 flickr.photos.search(tags=penguin) 理解为用某个编程语言写的一行代码。
另一种选择,是把作用域信息放在实体主体(entity-body)里, SOAP Web服务通常都采取这种方式。 我们来回顾一下例1-10:SOAP信封(Envelope)里含有一个 q 标签(tag),其内容为字符串“REST”——这就是作用域信息,它被放在包含方法信息的标签(doGoogleSearch)里。

采用什么样的服务设计,决定了哪些信息属于方法信息,哪些信息属于作用域信息。 Flickr和Google就是典型的例子,它们的网站与Web服务功能相同,但却具有不相同的设计。 以Flickr为例,下面两个URI包含同样的信息:
•  http://flickr.com/photos/tags/penguin
•  http://api.flickr.com/services/rest/?method=flickr.photos.search&tags=penguin

在前一个URI里,方法信息是“获取(GET)”,作用域信息是“具有‘penguin’标签的照片”;而在后一个URI里,方法信息是“搜索照片”,作用域信息是“penguin”。 从技术上看,二者并无区别——它们的HTTP方法都是GET;但只要你回顾一下,并注意到 flickr.photos.search 这个方法名称令HTTP的GET方法失去了原本用意,你就能体会到二者在架构上的区别了。

再看Google的例子: 对于Google SOAP API,方法信息是“做一次搜索(doGoogleSearch)”, 作用域信息是搜索请求(q);而对于Google网站,“search”与“q”的值都属于作用域信息,方法信息是标准的HTTP GET。 (假设Google SOAP API提供了一个名为 doGoogleSearchForREST 的方法的话,那么,由于方法信息里已包含足够信息,因此就不再需要“搜索‘REST’”这样的作用域信息了。)

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

[此贴子已经被作者于2008-5-16 19:37:31编辑过]

--  作者:admin
--  发布时间:5/16/2008 7:41:00 PM

--  《RESTful Web Services 中文版》连载 —— 《第一章 Programmable Web的分类》续

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

相互竞争的服务架构

我们已经给出了“不同Web服务会有不同做法”的两个主要问题,现在我要据此对不同风格的Web服务进行分类了。 根据我的研究,常见的Web服务架构主要有三种: REST式架构、RPC式架构和REST-RPC混合架构。 我将依次对它们进行介绍。

REST式、面向资源的架构
本书的主题是符合REST风格的Web服务架构——按照Roy Fielding博士论文里的评判标准,它们可以获得很高的得分。 现在,虽然许多架构从技术上说是REST式的,‡ 但我希望关注于那些最适合Web服务的架构;所以,当我谈及REST式Web服务时,我指的是那些具备Web特征的服务——我称它们为面向资源的(resource-oriented)。 我们将在第三章通过一个真实的Web服务——Amazon S3(Simple Storage Service)——来介绍面向资源的REST的基本概念, 然后在第四章,向你逐个介绍REST的标志特征,并定义一种非常适合REST式Web服务的架构——面向资源的架构(Resource-Oriented Architecture)。

REST式架构意味着,方法信息(method information)都在HTTP方法(HTTP method)里; 面向资源的架构(ROA)意味着,作用域信息(scoping information)都在URI里——二者结合起来是很强大的。 一个面向资源的REST式Web服务,通过HTTP请求的第一行(如“GET /reports/open-bugs HTTP/1.1”)就能基本了解客户端要做什么了,HTTP请求的其余部分只是具体细节而已。实际上,很多HTTP请求只要第一行就行了。 如果HTTP方法跟方法信息对不上,那么服务就算不上是REST式的;如果作用域信息不放在URI里,那么服务就不是面向资源的。 虽然并非只有这两条要求,但它们是很好的经验。
一些知名的REST式面向资源的Web服务包括:

•  提供Atom发布协议(http://www.ietf.org/html.char ters/atompub-charter.html)及其变型的服务,例如Gdata(http://code.google.com/apis/gdata/)。
•  Amazon S3(Simple Storage Service)(http://aws.amazon.com/s3)
•  Yahoo!提供的大部分Web服务(http://developer.yahoo.com/)
•  许多其他未采用SOAP的、只读的Web服务
•  静态网站
•  很多Web应用(尤其是像搜索引擎这种只读的)

‡ 令你意想不到的是:用作Web搜索的Google SOAP API,从技术上说,也是一种REST式架构;许多其它只读的SOAP和XML-RPC服务,也是REST式架构。不过,它们这种架构方式并不好,因为它们根本不具备Web的特征。

每当我谈到非REST式架构或非面向资源的架构时,我都是有一定目的的。 在这一章,我将在programmable web的背景之下,对REST式Web服务加以全面考察。 在第二章,我会提到一些真实的Web服务,并指出:无论一个服务是否正好符合我所推荐的架构,你都可以采用同样的客户端工具访问它。 在第十章,我会就“应如何设计programmable web”这一悠久的话题发表观点。

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

RPC式架构

RPC式Web服务(RPC-style Web Service)通常从客户端收到一个充满数据的信封(envelope),然后发回一个同样充满数据的信封。 RPC式架构意味着:方法信息和作用域信息都在信封(envelope)或报头(headers)里。 具体采用哪种信封,并不影响这里的分类,不过HTTP是一种常见信封格式(毕竟,采用HTTP才称得上是Web服务)。 另一种常见的信封格式是SOAP(把SOAP信封放在HTTP信封里,在HTTP上传送SOAP文档)。 各个RPC式服务采用自己的词汇,就像计算机程序一样 (你每次写程序,定义的函数名称都不相同)。 而REST式Web服务则相反,它们共用一套标准词汇,即HTTP方法。 REST式服务里的每个对象都具有统一的基本接口。

XML-RPC是最典型的RPC架构的例子。 虽然目前XML-RPC主要是一种遗留协议(legacy protocol)了,但由于它相对简单、而且比较容易解释,所以我还是准备从它开始讲起。 例1-11所示的Ruby客户端用于访问一个XML-RPC服务,该服务的作用是查询具有统一产品代码(Universal Product Code)的产品。

例1-11 一个访问XML-RPC服务的例子:根据UPC查询产品
#!/usr/bin/ruby -w
# xmlrpc-upc.rb

require 'xmlrpc/client'
def find_product(upc)
server = XMLRPC::Client.new2('http://www.upcdatabase.com/rpc')
begin
response = server.call('lookupUPC', upc)
rescue XMLRPC::FaultException => e
puts "Error: "
puts e.faultCode
puts e.faultString
end
end

puts find_product("001441000055")['description']
# "Trader Joe's Thai Rice Noodles"

XML-RPC服务就像C语言一样,你可以调用一个带参数(“001441000055”)的函数(lookupUPC),并获得返回值。

方法信息(函数名)和作用域信息(参数)都放在XML文档(如例1-12所示)里。

例1-12 一个描述XML-RPC请求的XML文档
<?xml version="1.0" ?>
<methodCall>
<methodName>lookupUPC</methodName>
<params>
<param><value><string>001441000055</string></value></param>
</params>
</methodCall>

这个XML文档是放在信封里传给服务器的。这里的信封(envelope)就是一个HTTP请求,它由HTTP方法、URI、报头和实体主体等部分组成,其中实体主体就是上面的XML文档(如例1-13所示)。

例1-13 一个包含了描述XML-RPC请求的XML文档的HTTP信封
POST /rpc HTTP/1.1
Host: www.upcdatabase.com
User-Agent: XMLRPC::Client (Ruby 1.8.4)
Content-Type: text/xml; charset=utf-8
Content-Length: 158
Connection: keep-alive

<?xml version="1.0" ?>
<methodCall>
<methodName>lookupUPC</methodName>
...
</methodCall>


上述HTTP信封里的XML文档,将随你所调用方法的不同而有所变化,但HTTP信封的格式总是不变的。无论你对这个UPC查询服务做什么请求,URI总是http://www.upcdatabase.com/rpc,HTTP方法总是POST。 简单地说,XML-RPC服务未采用HTTP的很多特性;它只暴露一个URI(称为“端点”),并且该URI只支持一种HTTP方法——POST方法。

REST式服务为不同的作用域信息暴露不同的URI;而RPC式服务一般为每个“文档处理器”(用于打开信封,并把信封转换为软件指令)暴露一个URI。 我们来做个对比,假设上述UPC查询服务被设计为一种REST式架构的话,那么其客户端代码将如例1-14所示:

例1-14 假想的示例代码: 一个REST式UPC查询服务
require 'open-uri'
upc_data = open('http://www.upcdatabase.com/upc/00598491').read()
...

这里,方法信息包含在HTTP方法里(默认的HTTP方法是GET,它对应于上例中的lookupUPC),作用域信息包含在URI里。 这个假想的服务暴露的URI不只一个,每个UPC代码都有与之对应URI。 与上例不同的是,这里的HTTP信封是空的——它是一个没有实体主体的HTTP GET请求。
另一个RPC式服务的例子可以参见例1-8:Google SOAP API是一个采用SOAP作为信封格式的RPC式服务。

较多采用或只采用HTTP POST的服务,多半是RPC式服务。 虽然这不是绝对的,但这至少可以说明该服务没有把HTTP方法用于表达方法信息。 如果一个REST式服务过多地采用HTTP POST,那么它就容易演变为REST-RPC混合架构。
下面是一些知名的RPC式Web服务的例子:
•  所有采用XML-RPC的服务
•  几乎所有的SOAP服务(这一点是有争议的,本章后面的“Programmable Web涉及的技术”一节对此进行了探讨)
•  少部分Web应用(通常是没设计好的)

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

REST-RPC混合架构

这一术语是我创造的,它用于形容那些介于REST式架构与纯RPC式架构之间的Web服务。 这些服务通常是由那些对真实Web应用懂得比较多、但对REST理论不精的程序员们创建的。
我们再次回顾一下这个调用Flickr Web服务时使用的URI: http://www.flickr.com/services/rest?api_key=xxx&method=flickr.photos.search&tags=penguin。尽管URI里包含“rest”字样,但它显然是一个采用HTTP信封的RPC式服务。 另一方面,它的作用域信息(“具有‘penguin’标签的照片”)是放在URI里的——从这一点看,它跟REST式面向资源的服务有点相像;不过它的方法信息(“搜索照片”)也被放在URI里了,而前面我们说过,对于REST式服务,方法信息应该放在HTTP方法里,其余部分全部作为作用域信息。 看来,这个服务只是把HTTP当作信封格式来用,然后按照自己的意愿来放置方法信息和作用域信息——这是一个RPC式服务,鉴定完毕!
不过,我们来看例1-15。

例1-15 一个向Flickr Web服务发HTTP请求的例子
GET services/rest?api_key=xxx&method=flickr.photos.search&tags=penguin HTTP/1.1
Host: www.flickr.com

这是当客户端调用Flickr Web服务时发出的HTTP请求。这里看上去,貌似方法信息是在HTTP方法里的。

这个请求的意图是获取(GET)数据。  获取什么数据呢?  一个搜索“具有‘penguin’标签的照片”的结果列表。原先貌似方法信息的数据(“搜索照片”),现在看上去像是作用域信息(“photos/tag/penguin”)了。刚才那个被鉴定为RPC式的服务,现在呈现出REST风格了。

这是一个错觉。当一个RPC式服务采用普通老式HTTP(Plain Old HTTP)作为信封格式、且方法信息和作用域信息刚好都在HTTP请求的URI路径里的话,就会产生这种错觉。假如HTTP方法是GET,并且请求服务的意图也是“获取(GET)”信息的话,就会很难分辨方法信息是在HTTP方法里、还是在URI里了。 所以你会把一个RPC式服务的HTTP请求,看成是REST式Web服务的HTTP请求。 这个HTTP请求里可能含有“method=flickr.photos.search”这样的信息,但这个信息会被误认为是作用域信息(就像“photos/”和“search/”是作用域信息一样)。 这些RPC式服务,不经意间或多或少地带着点REST式Web服务的特征。 它们只是把HTTP作为一种信封格式来用,不过它们使用HTTP信封的方式可能刚好跟REST式服务的做法雷同。

许多只读的Web服务,尽管它们起初也许是按RPC风格设计的,但它们都可称得上是完全REST式和面向资源的! 但是,如果该服务允许客户端修改数据的话,就会出现客户端所使用的HTTP方法与真正的方法信息不一致的情况——这样它就不具备REST式服务的特征了。 像这样的服务,我称之为REST-RPC混合服务。

举个例子。即使客户端的意图是修改数据,Flickr Web API仍旧让客户端使用HTTP GET。如果你要删除一个照片,你需要向一个包含“method=flickr.photos.delete”的URI发出GET请求,尽管你的这个请求的意图并不是获取(GET)数据。 Flickr Web API是一种REST-RPC混合架构: 当客户端通过GET方法获取数据时,它是REST式的;当客户端通过GET方法修改数据时,它是RPC式的。
一些知名的REST-RPC混合Web服务包括:
•  del.icio.us API
•  Flickr Web API
•  许多被说成是REST式架构的Web服务
•  大部分Web应用

从设计的角度来看,我认为不会有人特意把服务设计为REST-RPC混合架构。 由于HTTP工作方式的原因,任何采用普通HTTP、并暴露多个URI的RPC式服务,往往最终成为REST式架构或者混合架构。 许多程序员按他们设计Web应用的方式来设计Web服务,并最终形成混合架构的服务。
混合架构的存在已经造成了不少混乱。 从事Web应用设计的人容易设计出REST-RPC混合架构,而且他们常常声称这种混合架构是REST式架构——他们仍在以设计human web的方式来设计Web服务。 已经有不少功夫花费在分辨REST式架构与其它架构上了。

我把“其它架构”称为REST-RPC混合架构,这是众多新词中的一个, 我认为这个新词是看待这些常见而令人困惑的服务的最准确有效的方式。 假如你知道它们的其他称呼(在写本书的时候,“HTTP+POX”是最流行的),你不妨继续往下读,后面我会用我自己的语言来解释这些其它术语。

Human Web是基于Programmable Web的
在前一节里,我说过所有静态网站都是REST式的。 我还说,Web应用可归为三种架构,其中大多数都属于REST-RPC混合架构。 因为human web完全是由静态网站和Web应用构成的,所以,可以说整个human web都是基于programmable web的! 现在,你应该不会对此感到惊讶。 一个Web浏览器就是一个发送HTTP请求(request)、然后以某种方式来处理响应(response)并呈现给人类用户的软件程序。 Web服务客户端不就是这样嘛! 所以说,只要它是基于Web的,它就是Web服务。

我写本书的目的,并不是要扩大programmable web的范围——那几乎是不可能的,因为差不多所有具备HTTP接口的事物都已包括在programmable web里了。 我的目的是促进把programmable web建设得更好: 更加统一,更加结构化,并且充分利用HTTP自身的特性。

Programmable Web涉及的技术
我已经根据Web服务背后的(underlying)架构对Web服务进行了分类——把鲸鱼跟鱼区分开了。 现在,我可以对它们所采用的技术加以考察,而不会把技术与架构混为一谈了。

HTTP
所有Web服务都用HTTP,只是它们使用的方式有所不同。 对于一个REST式Web服务,它会在HTTP方法里寻找方法信息、在URI里寻找作用域信息;而RPC式Web服务则往往忽略HTTP方法,在URI、HTTP报头或实体主体里寻找方法信息与作用域信息。 有些RPC式Web服务把HTTP作为一种容纳文档的信封,而另一些RPC式Web服务则把HTTP作为一种容纳另一个信封的信封。

URI
同样地,所有Web服务都用URI,只是它们在使用方式上有所差别。 我对此总结如下: 一个REST式面向资源的服务为客户端可能操作的每一则数据暴露一个URI;一个REST-RPC混合服务,为客户端可能操作的每一个操作暴露一个URI (比如获取数据用一个URI,删除数据用另一个URI); 一个RPC式服务,为每个处理远程调用的进程暴露一个URI,  一般来说这样的URI只有一个,  即服务的“端点(endpoint)”。

XML-RPC
有些Web服务(主要是遗留服务)在HTTP之上使用XML-RPC。 XML-RPC是一种用于表达“函数调用及其返回值”的数据结构格式。 正如其名称所暗示的,它是专门用于RPC式Web服务的。

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

SOAP
很多Web服务在HTTP之上使用SOAP。 跟HTTP一样,SOAP也是一种信封格式,只不过它是基于XML的。

我下面这则观点可能会引起争议。 基本上,现在每个采用SOAP的Web服务都属于RPC式架构。 因为许多SOAP程序员觉得RPC式架构地位较低,所以更乐意称他们的服务为“面向消息的(message-oriented)”或“面向文档的(document-oriented)”服务,于是他们对我这个观点持有异议。
嗯,所有Web服务都是面向消息的,因为HTTP本身就是面向消息的。一个HTTP请求就是一个消息: 一个内含文档的信封。 问题在于文档里内容是怎样的。 基于SOAP的服务,要求客户端在HTTP信封里放入另一个信封(SOAP文档)。 还是那句话,真正的问题在于这个信封里有什么内容。 如同HTTP信封的实体主体里可以包含任何数据一样,SOAP信封里也可以包含任何XML数据。 但是现在所有基于SOAP的服务,其SOAP信封里包含的是一个对RPC调用的描述(采用跟XML-RPC差不多的格式)。
对RPC描述进行组织、并给予不同的叫法(比如“document/literal”或“wrapped/literal”等)有多种多方的做法。但不论你怎么做,你的服务的方法信息在词汇上不统一,而且你的服务是在文档里(而不是在信封上)寻找作用域信息。 这些便是RPC式架构的标志特征。

我要强调的是,这些并不是针对SOAP本身、而是针对SOAP目前的使用状况而言的。 SOAP跟HTTP一样,只是一种把数据放在信封里的方式。 但是现在,唯一放在信封里的数据是像XML-RPC这样关于“如何调用一个远程函数,或者该函数的返回值是什么”的数据。 我会在第十章对此做深入讨论。

WS-*
这些标准定义各种特定用途的SOAP报头。  

WSDL
WSDL(Web Service Description Language,Web服务描述语言)是一套用于描述SOAP Web服务的XML词汇。 客户端通过参考WSDL文档,可以确切地知道它可以调用哪些RPC式方法、这些方法需要哪些参数、以及返回值是什么数据类型。 几乎所有现有的基于SOAP的服务都发布有WSDL文档,因为假如没有WSDL文档作为参考的话,将很难使用这些服务。 正如我将在第十章讲述的,SOAP令人联想到RPC式架构,主要是由WSDL造成的。

WADL
WADL(Web Application Description Language,Web应用描述语言)是一套用于描述REST式Web服务的XML词汇。 跟WSDL一样,普通客户端通过参考相应的WADL文档,可以了解如何使用Web服务的全部功能。 我会在第九章讲述WADL。

由于REST式服务的接口比较简单,所以WADL之于REST式服务的必要性,并不像WSDL之于RPC式SOAP服务那么大。 在本书编写之时,已经有少数真实Web服务提供官方的WADL文档了。比如Yahoo!的Web搜索服务就是一例——这是件好事。

其他术语

还有一些在REST讨论中常见术语我没有介绍。 因为我觉得这些术语或者不够准确、或者超出了本书范围,所以我没有提及它们。 不过我还是应该告诉你我为什么这么认为,以便你自己判断。 如果你从没听说过这些词,可以放心跳过本节。
面向服务的架构(Service-Oriented Architecture,SOA)
这是业界极为时髦的一个词。 我不准备对此作深入讨论,因为两个原因:第一,这个词的定义不是很明确;第二,就它的定义目前所及程度而言,它似乎意味着“一种基于生产和消费Web服务的软件架构”。 而在本书中,我讨论的是个别服务的设计问题。 SOA书籍所涉及的层面略高一点,比如讲述如何把服务作为软件组件,如何把这些服务集成为一个统一的整体等等。 本书不涉及此类话题。
SOAP是REST的对手
如果你曾卷入过有关Web服务的争论,你会听过很多这种论调。 但在本书中,你不会听到,因为这个看法不正确。 REST式架构的主要竞争对手是RPC式架构,而不是像SOAP这样特定的技术。 没错,几乎所有现有的基于SOAP的服务都属于RPC式架构,但SOAP跟HTTP一样,只是一种把文档放在信封里的方式。



SOAP被与RPC式架构联系在一起,主要是由于历史偶然性、以及当前自动化工具的缘故。
对立确实存在,不过我不准备在书中讨论过多。 大概说来,这个对立是那些“把文档放在SOAP信封里、然后外面再套一个HTTP信封”的服务与那些只用HTTP信封的服务之间的。
HTTP+POX
这个词表示“HTTP加普通老式XML(Plain Old XML,POX)”。 大致说来,它指的是那些被我称为REST-RPC混合架构的服务。 它们与REST式设计有重叠之处(尤其是在获取数据时),但是它们的基本架构是面向RPC的。
我不喜欢这个词,因为普通老式XML(Plain Old XML)不是一个准确的词。 这些服务值得关注的地方,并不在于它们采用普通老式XML文档(相对于封装在SOAP信封里的XML文档而言)。 这些服务中有的根本不提供XML,而是提供JSON、纯文本或二进制文件。 它们值得关注的地方在于它们是RPC式架构——这是令它们跟REST背道而驰的根本所在。
STREST
这个词的意思是“遭服务劫持的”REST(Service-Trampled REST)。 这也是一个代表REST-RPC混合架构的词。 它较HTTP+POX准确一些,因为它传达了“REST式架构被其他事物(这里指的是RPC式架构)取代”的意思。
这是一个漂亮的缩写词,但我不喜欢它,因为它认可了“只有RPC式服务才是真正的Web服务”这一说法。在这个词中,“劫持”REST的那个服务(Service)指的是RPC式服务。 因为,如果说你认为REST式服务是真正的服务的话,你就不会说出“帮帮忙!我有一些REST式服务,但它被这个服务(Service)劫持了”这样令人困惑的话。“遭RPC劫持的REST(RPC-Trampled REST)”或许更准确一些,但这样缩写就不好看了。
高级REST和初级REST
这是在术语上区分真正的REST式架构与REST-RPC混合架构的另一个办法。高级REST(High REST)服务,指的是哪些遵从Roy Fielding的博士论文的服务。 它们把方法信息放在HTTP方法里,把作用域信息放在URI里。 初级REST(Low REST)服务,指的是那些未完全遵从Roy Fielding的博士论文的服务。 因为初级REST服务往往在某些方面偏离正统的REST(转向RPC),所以我宁愿采用一个更具体的术语。

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/


--  作者:admin
--  发布时间:5/16/2008 7:53:00 PM

--  《RESTful Web Services 中文版》连载 ——《第四章 面向资源的架构》

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

第四章 面向资源的架构

我已经向你展示了REST的能力,但是我还没有系统地讲述该能力是怎样形成的,以及如何来展现该能力。 在这一章,我将概述一个具体的REST式架构——面向资源的架构(Resource-Oriented Architecture,ROA)。 ROA是一种把实际问题转换成REST式Web服务的方法:它令URI、HTTP和XML具有跟其他Web应用一样的工作方式,令程序员们易于使用它们。
在第一章,我根据REST式Web服务(RESTful web services)在两个问题上的不同做法来对它们进行分类。 这两个问题分别对应于REST的四个标志特征中的两个:

•  作用域信息(scoping information)(“服务器发送的为什么是这些数据,而不是其他数据”)——该信息是放在URI里的。这是可寻址性(addressability)原则。
•  方法信息(method information)(“为什么服务器应当发送而不是删除这些数据?”)——该信息是放在HTTP方法里的。 HTTP方法只有少数几种,而且人人都事先知道这些方法的作用。这是统一接口(uniform interface)原则。
在这一章,我将介绍面向资源的架构(ROA)的功能成分:资源、资源名称、资源的表示、资源间的链接。 我将解释并宣传ROA的特性:可寻址性(addressability)、无状态性(statelessness)、连通性(connectedness)和统一接口(uniform interface)。 我将展示Web技术(HTTP、URI和XML)是如何实现这些功能成分、以获得上述特性的。
在前面几章,我通过现有的一些Web服务(如S3)举例说明了一些概念。 在本章,我除了会引用一些现有的Web服务(web services)以外,还会引用一些现有的网站(web sites)来讲解概念。 希望此刻我已经让你相信“网站就是Web服务,而且许多Web应用(比如搜索引擎)就是REST式Web服务”了。 我在讲述一些抽象概念(如可寻址性)时,会给出一些真实的URIs,这样你就可以通过在浏览器里输入这些URIs来亲身体会有关概念了。

面向资源的架构?

为什么要发明一个新词“面向资源的架构(Resource-Oriented Architecture,ROA)”呢? 为什么不直接用REST? 嗯。我确实在本书的封面上提到REST,并且我认为符合面向资源的架构也符合REST风格。  

但是,REST并不是一种架构,而是一组设计原则。 你可以讲“在遵守这些原则方面,一个架构做得比另一个架构好”,但是你不能讲“REST架构”,因为不存在一个叫“REST架构”的东西。
直到目前为止,人们已经习惯于在设计服务时,根据他们自己对REST的理解发明一次性架构(one-off architectures)。 这样做最显著的结果,是产生了各式各样的REST-RPC混合Web服务,而其创建者们还声称它们为REST式的。 为此,我提出了一组构建真正REST式Web服务的具体规则,以期能够结束这一状况。 在接下来的两章里,我会给出一些简单的步骤,你只要根据这些步骤就能把需求转换成一个个资源(resources)。 即便你不喜欢我的规则,你至少可以知道做哪些改变是不会导致违反REST风格的。

作为一组设计原则,REST是非常通用的。 具体地说,它并不限定于Web。 REST不依赖于HTTP机制或URI结构。 但因为我讨论的是Web服务,所以我特地用Web相关技术来讲解面向资源的架构(ROA)。我想在特定的编程语言中探讨如何用HTTP和URI来实现REST。 假如将来出现非基于Web的REST式架构,它的最佳实践(best practices)将跟ROA的差不多,只是具体细节会有点差别。 到时我们会有办法解决的。

REST的传统定义留下了一些空白,这给实践者们创造了传播坊间传言(folklore)的环境。 我特意在Roy Fielding的博士论文及W3C相关标准的基础上更进一步——我希望对部分问题做一个了结,把那些坊间传言提炼为一组明确定义的最佳实践。 即便REST是一种架构,用REST来称呼我的架构也是不公平的。 我会把自己的实践经验与建议,与那些关于Web的总体想法结合起来。

我提出ROA这个新词的根本原因是:“REST”这个词被太多地用在派别之争里。 假如某人提到这个词,那通常暗示着,他本人所赞同的架构才是真正的REST式架构,而赞同别的REST式架构的人则会对此持有异议。 尽管REST社区在一些基本问题(比如URI和HTTP的价值)上已基本达成一致,但REST社区还是存在着不同派别。
最理想的状况是没有派别之争。可是,太多的经验告诉我,仅凭意愿并不能结束它。 所以,我要给我的关于“应当如何设计REST式应用”的思想取一个不同的名字。 这样,万一我的这些想法被不可避免地用到派别之争中,那些不同意我的人就可以针对我的面向资源的架构(ROA),而不是跟REST式架构或一般的REST混为一谈。 先要把概念理清了,才可能做到理解。

“面向资源的(resource-oriented)”和“面向资源的架构(resource-oriented architecture)”这样的措辞已经被用于描述一般的REST式架构了。* 我承认“面向资源的架构”并不完全是我原创的词,但我的用法跟先前的用法刚好吻合,而且我觉得采用这个词比声称代表整个REST要好。

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

什么是资源?

任何事物,只要具有被引用的必要,它就是一个资源(resource)。 如果你的用户“想为它创建一个超文本链接,关于它作一些断言,获取或缓存它的表示,在其他表示里包含它的一部分,标注它,等等”,那么你应该将它作为一个资源。†
通常,一个资源就是某个可以存放在计算机上并体现为比特流的事物,比如:一个文档、数据库里的一条记录、或者运行某算法的结果等等。 一个资源(resource)可以是一样实物(比如一只苹果),也可以是一个抽象的概念(比如勇气)——但正如我们将看到的,这类资源的表示(representation)肯定会令我们大失所望。
下面是一些资源的例子:
* 某软件的1.0.3版
* 某软件的最新版本
* 2006年10月24日发布的第一篇博客文章
* 一张阿肯色州小石城的地图
* 关于水母的一些信息
* 与水母有关的资源列表
* 大于1024的最小素数
* 大于1024的第五小素数
* 2004年第4季度的销量
* Alice和Bob两人之间的关系
* bug数据库里的待解决bug列表

URIs

是什么,令资源称得上是一个资源? 它必须至少有一个URI。 URI既是资源的名称,也是资源的地址。 如果一则信息没有URI,它就不能算是一个资源,也不能算真正在Web上,而只能算作描述另一个资源的一些数据。

*  我所知道的最早使用“面向资源”一词的例子,是2004年James Snell在IBM developerWorks上发表的一篇文章:“Resource-oriented vs. activity-oriented Web services”(http://www-128.ibm.com/developerworks/xml/ library/ws-restvsoap/)。2006年8月(本书发布之前),Alex Bunardzic使用了“面向资源的架构(Resource-Oriented Architecture)”这个词(http://jooto.com/blog/index.php/2006/08/08/replacing-service-oriented-architecture-with-resource-oriented-architecture/)。虽然我并不完全赞同这些文章中的观点,但我承认他们在我之前使用了该术语。

* “The Architecture of the World Wide Web”(http://www.w3.org/TR/2004/REC-webarch-20041215/#p39)里有很多值得参考的内容: “软件开发者们应该相信,为应用分配URI将是有益的,即使最初其效用并不明显。” 这其实可以作为ROA的口号。

还记得我在前言里为HTTP 0.9举的那个客户端与服务器交互的例子吗? 以这个获取http://www.example.com/hello.txt的HTTP 0.9请求为例:

客户端请求            服务器响应
GET /hello.txt         Hello, world!


HTTP客户端是这样处理资源的:首先连接到该资源所在服务器(本例中www.example.com),然后向该服务器发送方法(“GET”)及该资源的路径(“/hello.txt”)。 虽然现在的HTTP 1.1比HTTP 0.9稍微复杂一些,但它们的工作原理是一样的。服务器域名及路径都在资源的URI里。

客户端请求         服务器响应

GET /hello.txt         HTTP/1.1
                       Host:www.example.com
                       200 OK
                       Content-Type: text/plain
                       Hello, world!

Tim Berners-Lee在Universal Resource Identifiers—Axioms of Web Architecture (http://www.w3.org/DesignIssues/Axioms)这篇文章里很好地概括了URI背后的原理。我会在本节详细讲解构造URI及为资源分配URI的原理。

URI是Web的基础技术。 超文本系统在HTML之前就有了,同样地,Internet协议在HTTP之前也已经存在了。不过此前,它们两两之间是没有联系的。是URI把所有这些Internet协议互联起来,形成Web,就如同TCP/IP把各种网络(Usenet、Bitnet、CompuServe等)互联起来,形成Internet一样。 然后,Web征用了那些协议,并消灭了它们,这跟Internet对私有网络所做的一样。
如今,我们浏览Web(而不是Gopher),我们从Web(而不是FTP站点)上下载文件,我们在Web(而不是WAIS)上搜索出版物,我们在Web(而不是Usenet新闻组)上交谈。 版本控制系统(如Subversion、arch等)也是基于Web的(而不是采用专有的CVS协议)。 甚至连Email也逐渐转向Web了。
Web之所以能够把其它协议消灭,是因为它采取了一种简单的方式来给可用资源作标签,而这是许多其它协议所没有的。 Web上的每个资源都至少有一个URI。 你可以把URI写在户外广告牌上, 人们可以看到那个广告牌后,可以在Web浏览器里打开那个URI,直接进入你期望他们进入的资源。 这似乎比较不可思议,但在URI被发明之前,这种我们司空见惯的交互是不可能实现的。

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

URI应具有描述性

这是ROA在Roy Fielding的博士论文及W3C推荐标准(recommendations)[ ]的为数不多的建议之上提出的第一点。 我建议,资源及其URI应该给人以直觉上的关联。 对于前面我给出的资源,下面是一些不错的URIs:
* http://www.example.com/software/releases/1.0.3.tar.gz
* http://www.example.com/software/releases/latest.tar.gz
* http://www.example.com/weblog/2006/10/24/0
* http://www.example.com/map/roads/USA/AR/Little_Rock
* http://www.example.com/wiki/Jellyfish
* http://www.example.com/search/Jellyfish
* http://www.example.com/nextprime/1024
* http://www.example.com/next-5-primes/1024
* http://www.example.com/sales/2004/Q4
* http://www.example.com/relationships/Alice;Bob
* http://www.example.com/bugs/by-state/open

URI应具有一定的结构。 虽然各个URI有所不同,但它们应该有一定的模式:假如/search/Jellyfish是用于获取有关水母的信息的URI,那么用于获取有关老鼠的信息的URI就应该是/search/Mice,而不是/i-want-to-know-about/Mice。 假如客户端知道一个服务的URI的结构,那么它就可以通过构造URI来访问该服务——客户端在使用你的服务方面,将被赋予极大的自由,甚至可能创造出超出想象的使用方式。
正如我们在下一章的“命名资源”一节中将看到的,这并不是REST的必备规则。 从技术上讲,URI不是非得具备一定结构或模式,但我认为需要这样。 良好的Web设计需要这条规则,REST式及混合架构服务也一样。

URI跟资源的关系

我们来设想一些极端的例子:两个资源有可能是同一个吗? 两个URIs可以指示同一个资源吗? 一个URI可以指示两个资源吗?

根据定义,任何两个资源都不可能是同一个。 假如你声称有两个资源是同一个,那么实际上你所说的只是一个资源。 不过,两个不同的资源在某一时期指向同样的数据,这是有可能的。 假设当前软件版本为1.0.3,那么http://www.example.com/software/releases/1.0.3.tar.gzhttp://www.example.com/software/releases/latest.tar.gz将在一定时期内指向同一个文件。 但那两个URIs的意义不同:一个总是指向某一特定版本;另一个总是指向当前时刻的最新版本。 这是两个概念,两个资源。 假设你要报告一个1.0.3版的bug,你不应该使用指向最新版的链接。

一个资源可以有一个或多个URI。  位于http://www.example.com/sales/2004/Q4的销量信息也许另外还有一个URI http://www.example.com/ sales/Q42004。 一个资源有多个URIs的好处是,客户端对该资源的引用变得更容易;坏处是,同一资源具有多个URI将产生“稀释效应”:有的客户端用这个URI,有的客户端用那个URI,而且无法自动验证这些URIs是指向同一个资源的。

一种方案是:假如资源有多个URIs,那么选择其中一个作为该资源的“规范”URI。 当客户端请求该规范URI时,服务器返回响应代码200(“OK”),并附上正确的数据;当客户端请求其他URI时,服务器返回响应代码303(“See Also”),并给出该资源的规范URI。 虽然客户端无法仅凭外观得知这两个URIs是否指向同一资源,但它可以分别向这两个URI发出一个HEAD请求,看是否其中一个URI重定向到另一个,或者二者都重定向到第三个URI。

还有一个方案,就是对所有这些URI一视同仁、作同样的响应;但是对于非规范URI的请求,将在响应报头Content-Location里给出“规范”URI。

因为 sales/2004/Q4 和 sales/Q42004 是同一个资源“2004年第4季度销售信息”的不同URIs,所以你将从这两个URIs获得相同的字节流。 虽然 releases/1.0.3.tar.gz 和 releases/latest.tar.gz 是不同的资源(前者表示“1.0.3版”,后者表示“最新版”),但它们也可能会返回相同的字节流。
一个URI只能指示(designate)一个资源。 假如一个URI指示多个资源的话,它就不是统一资源标识符(Universal Resource Identifier)了。 不过,当你请求一个URI时,服务器可以返回给你关于多个资源的信息:包括你所请求的资源,也包括其它相关资源的信息。 在你获取一个网页时,一般服务器只返回该网页的一些信息,但其中也会包含一些指向其它网页的链接。 在你用Amazon S3客户端获取一个S3桶(bucket)时,你将得到一个文档,其中既包含关于该桶的信息,也包含关于相关资源(即桶里的对象)的信息。

可寻址性

我已经对资源及资源的URI作了介绍,现在我可以进一步探讨ROA的两个特性了:可寻址性(addressability)和无状态性(statelessness)。

如果一个应用将其数据集里有价值的部分作为资源(resources)发布出来,那么该应用就是可寻址的(addressable)。 因为资源是通过URI暴露的,所以一个可寻址的应用会为它可能提供的每一则信息都发布一个URI。 一般来说,URI的数量是无限的。

从最终用户角度来看,可寻址性(addressability)对任何网站或Web应用来说,都是最重要的方面。 用户很聪明,只要数据有充分的价值,即便有一些缺陷,他们也不会在意、或者会有办法变通;但假如不具备可寻址性,用户就没辙了。
设想一个指示(designate)“有关水母的资源列表”这个资源的URI:http://www.google.com/search?q=jellyfish。这个在Google里搜索水母(jellyfish)的URI跟 http://www.google.com 一样都是真实的URI。但是,假如HTTP不是可寻址的,或者Google搜索引擎不是可寻址的Web应用,那么我就无法在书里直接给出这个URI了——我只能告诉你:“用浏览器打开google.com,在搜索框里输入‘jellyfish’,然后点击‘Google 搜索’按钮。”

这不是学术上的担忧。 在90年代中期 ftp:// URI流行起来之前,那时人们只能这样写:“用FTP匿名登陆ftp.example.com,然后进入 pub/files/ 目录,再下载文件 file.txt。”URI使得FTP具有跟HTTP一样的可寻址性。 现在人们只要写“下载ftp://ftp.example.com/pub/files/file.txt”就行了。 虽然在技术上步骤还是一样的,但现在这些步骤可以由机器自动完成了。

因为HTTP和Google都是可寻址的,所以我可以在书中给出该URI。 你可以在浏览器地址栏里输入这个URI,以直接进入Google搜索应用的特定状态。

页面打开之后,你可以把该页面加入书签,以便以后使用。 你可以在自己的网页里提供到该URI的链接。 你还可以通过Email把该URI告诉别人——假如HTTP不是可寻址的话,那你只有把整个页面下载下来,然后将该HTML文件作为附件发给其他人。

为了节省带宽,你可以为你的本地网络设置一个HTTP代理缓存(proxy cache)。 当第一次有人请求 http://www.google.com/search?q=jellyfish 时,该缓存将在本地保存一份该文档的副本;当下次有人再访问同样的URI时,该缓存就直接把该副本发给请求者,而不是重新下载一次。 这一切得以实现,要求每个页面都有一个唯一的标识串,即一个地址(address)。
一个URI可以作为另一个URI的输入。 比如,你可以用一个外部Web服务来验证某个网页的HTML代码是否符合规范,或者把某个网页里的文本翻译为另一种语言。 这些Web服务都以URI作为输入。 假如HTTP不是可寻址的,那你就无法告诉它们你希望对哪个资源进行操作了。

在Amazon S3里,每个桶(bucket)、每个对象(object)都有自己的URI,所以S3服务是可寻址的。 尚未创建的桶或对象虽然还不是资源,但它们也有自己的URI,你可以通过向它的URI发出PUT请求来创建该资源。
你的计算机上的文件系统也是一个可寻址的系统,命令行应用可以接受文件路径为参数,然后对该文件进行一些操作。

电子表格里的单元格也是可寻址的,你可以在公式里引用单元格,这样公式在求值时就会用到被引用的单元格的值。 URI就好比这里的文件路径和单元格。
可寻址性(addressability)是Web应用最大的优点,它令客户端可以灵活自由地使用网站(甚至可能创造出超出网站设计者想象的使用方式)。 遵照可寻址性规则,将给你和你的用户带来REST的很多优点。 REST-RPC服务如此常见的原因就在于,它们把可寻址性跟过程调用编程模型(procedure-call programming model)结合起来了。 我在“Resource-Oriented Architecutre”中把“resource”写在最前面,是因为具备可寻址性的是资源。

Web应该是可寻址的,这似乎是理所当然的。可是,许多Web应用却不是可寻址的,尤其是Ajax应用。 正如我将在第十一章说到的,大多数Ajax应用只是调用REST式或混合Web服务的客户端,但当你把它当作网站来使用时,你会发现它跟网站的感觉不太像。

我就不在这里反复批评他们了。让我们继续关于Google特性的讨论,我们现在来看Gmail在线Email服务。 在最终用户看来,Gmail的URI始终是https://mail.google.com/。无论你作什么操作,无论你从Gmail获取什么信息或者向Gmail上传什么信息,你不会看到别的URI。 “关于水母的电子邮件讯息”这一资源不是可寻址的,但前面提到的“有关水母的资源列表”这个资源是可寻址的。‡ 不过,正如我将在第十一章所展示的,在Gmail应用背后存在一个可寻址的网站——在那里,“有关水母的电子邮件讯息”这个资源确实有一个URI,即 https://mail.google.com/mail/?q=jellyfish&search=query&view=tl。问题是,该网站不是你直接使用的;该网站是一个真实的Web服务,其真正调用者是一个在你的浏览器里运行的JavaScript程序。§  Gmail Web服务是可寻址的,不过调用该服务的Gmail Web应用不是可寻址的。

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

无状态性

可寻址性是ROA的四个主要特征之一。 ROA的第二个特征是无状态性(statelessness)。 我会给你两个无状态性的定义:一个较为一般性的定义,和一个更切合ROA实际的定义。
无状态性(Statelessness)意味着每个HTTP请求都是完全孤立的。 当客户端发出一个HTTP请求时,请求里包含服务器实现(fulfill)该请求所需的全部信息,服务器不依赖任何之前请求提供的信息。 假设本次请求需要之面某个请求提供的信息,那么客户端应当把那个信息也包括在本次请求里。

‡ Gmail有一个可寻址的版本,位于URI https://mail.google.com/mail/?ui=html。如果你用这个纯HTML接口的话,那么“关于水母的电子邮件讯息”这个资源就是可寻址的了。
§ Python的libgmail库(http://libgmail.sourceforge.net/)也可以调用这个Web服务。

更实际地,我们从可寻址性方面来考虑无状态性。 可寻址性要求:服务器所能提供的每一则有价值的信息都应该作为资源来发布,而且每个资源都有自己的URI。无状态性要求:服务器可能的状态也是资源,也应该有自己的URI。 客户端不必为了让某个请求可被服务器接受而诱使服务器进入某状态。
在human web上,你经常会碰到这样的情况:你的浏览器的“返回(back)”按钮不起作用了,你无法在你的浏览历史里前后或进退了。 有时,这是因为你执行了一个不可撤销的操作(比如发布了一篇博客文章,或购买了一本书),不过经常这是由于你所浏览的网站违反了无状态性原则:该网站期望你按照一定的顺序来发出请求,比如先发A请求,再发B请求,然后再发C请求;假如你在发出B请求后,又发了一个B请求(而不是它所期望的C请求),那么它将无法理解你的行为。
我们再来看一个搜索的例子。 搜索引擎是一个具有无数状态的Web服务:至少对于你搜索的每个串,它都有一个与之对应的状态。每个状态都有自己的URI。 你可以用URI http://www.google.com/search?q=mice 向该服务请求一个有关老鼠(mice)的资源列表。 你可以用URI http://www.google.com/search?q=jellyfish 向该服务请求一个有关水母(jellyfish)的资源列表。 如果你写不出这个URI,你可以先打开 http://www.google.com/,然后填写表单执行搜索。

你在请求有关老鼠或有关水母的资源列表时,你无法一次获得完整的列表。 你会先得到列表中的一页,里面包括10条左右搜索引擎认为与你的查询最为匹配的结果。 如果你想获得更多结果,你需要再做一次HTTP请求。 第二页及其后各页都是该应用的不同状态,所以它们需要有自己的URI(比如http://www.google.com/search?q=jellyfish&start=10)。如同所有可寻址的资源一样,该应用状态也可被发给其他人、被缓存或者被加入书签(以便以后回到该状态)。

图4-1是一个简单的状态图,它显示了一个HTTP客户端是如何与一个搜索引擎的四个状态进行交互的。

因为客户端每次请求后都回到最初的状态,所以这是一个无状态的应用。 各个请求是相互独立的。 客户端可以按任意次序向这些资源发出任意次请求。 它可以在请求第2页之前请求第1页(或者根本不请求第1页),服务器对此不会介意。

作为对比,图4-2给出了假设上述搜索引擎是一个有状态的应用的情形:一个状态推进到另一个状态,就像许多桌面应用那样。

倘若HTTP被设计为允许有状态的交互,那么HTTP请求将简单许多:当客户端启动一个搜索引擎会话时,搜索引擎可以自动启动搜索,而不必客户端发送搜索请求,因为搜索引擎保存有该客户端上次搜索的记录;假如客户端正在查看前10条结果,接着要看第11至20条结果,那么只要发送一个“start=10”的请求就行了,而不必发送 /search?q=mice&start=10,重复之前的请求“我要搜索,具体地说要搜索老鼠。”


此主题相关图片如下:
按此在新窗口浏览图片
图4-1 一个无状态的搜索引擎


此主题相关图片如下:
按此在新窗口浏览图片
图4-2 一个有状态的搜索引擎


FTP就是这样的。FTP有一个“工作目录(working directory)”的概念,除非你改变它,否则它在整个会话过程中不会变化。 你可以登录一个FTP服务器,用cd命令进入某个目录,然后用get命令下载一个该目录下的文件。如果你要下载同目录下的其他文件,直接用get命令下载就行了,不需要重新用cd命令进入目录。 为何HTTP不支持这样呢?

状态可以简化单个HTTP请求,但它会令整个HTTP协议变得更加复杂。 FTP客户端要比HTTP客户端复杂许多,原因就在于客户端和服务器的会话状态必须要保持同步。 即使在可靠的网络上,这也是一项复杂的工作,何况Internet还不是一个可靠的网络。

从协议里去掉状态,将大大减少出错条件。 因为每次交互都只包括单个请求,所以服务器不必为客户端超时而担忧。

因为客户端发出的每个请求里都含有全部所需信息,所以服务器绝不会搞错客户端所处的状态。 决不会出现“因为服务器保存了某个没有告诉客户端的状态,而导致客户端在错误的‘工作目录’下执行操作”的情况。

无状态性还引入了一些新特性。 在负载均衡(load-balanced)服务器上分配无状态的应用将容易许多:因为各个请求之间没有相互依赖,所以它们可被放在不同的服务器上处理,而不需服务器之间作任何协作;要提升规模,只需往负载均衡系统里添置更多服务器就行了。 对无状态的应用作缓存也是比较容易的:只要看一下请求,就可以决定是否要缓存一个HTTP请求的结果了;前一个请求的状态不会影响对当前请求的缓存处理。
客户端也将从无状态性中获益。 客户端可以在浏览到第50页有关“老鼠”的搜索结果时,把当前URI(/search?q=mice&start=500)加入书签——这样,客户端可以在一周后方便地回到这个状态,而不必重新历经许多前面的状态。 在一个HTTP会话里经历几小时后得到的有效URI,可以在新会话里直接访问,而且同样有效。
要令你的服务具备可寻址性(addressability),你需要花费一些努力、把应用的数据划分为资源的集合。 由于HTTP本身是一个无状态的协议,所以你编写的Web服务默认地具备无状态性,你需要花费一些努力来改变它。
改变无状态性,最常用方法就是利用HTTP会话(session)。 当一个用户首次访问你的网站时,他会得到一个唯一的字符串,用以标识他在该网站上的会话。 这个字符串可以保存在cookie里,也可以放在给该用户的所有URI里。 这是一个Rails应用设置的会话cookie:
Set-Cookie: _session_id=c1c934bbe6168dcb904d21a7f5644a2d; path=/
这是一个PHP应用在URI里给出会话ID的例子: http://www.example.com/forums?PHPSESSID=27314962133
值得注意是,这些无意义的十六进制或十进制字符串并不是状态。 状态保存在服务器端的某个数据结构里,这些字符串只是该数据结构的键(key)。 有状态的URIs(stateful URIs)并不违反REST精神:服务器通过这种方式把下一个状态告诉客户端。 (但是,cookies是有点违反REST精神的,我会在第八章“Cookies的问题”一节中详细谈到。) cookie会对Web服务客户端产生“如同Web浏览器的‘返回(back)’按钮失灵”一样的影响。
设想Google搜索引擎返回的一个HTML网页里有一个URI,该URI里包含查询变量 start=10。 也就是说,服务器向客户端发送了下一个可选的状态。

但那些URI得包含真正的状态才行,而不是仅包含一个跟“保存在服务器上的状态信息”对应的键(key)。start=10自身具有一定含义,而PHPSESSID=27314962133则不。REST式架构要求把状态保存在客户端,并且在发给服务器的每个请求中都包括这些状态(假如需要的话)。
  
服务器可以通过向客户端发送有状态的URIs(stateful URIs)为客户端提供进入新状态的机会,但服务器自己不应保存任何状态。

关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/

应用状态 VS 资源状态

我们在谈论“无状态性”时,把什么视为“状态”呢? 我们调用Web服务,是因为服务可以提供我们所需要的数据,那么这个服务端的持久化数据(persistent data)跟这个我们极力避免保存在服务端的状态(state)有何区别呢? Flickr Web服务允许你往自己的帐户上传图片,那些图片是保存在服务器上的。 但是,假如仅仅出于不让服务器保存任何状态的目的,而要求客户端在发给flickr.com的每个请求里都附上所有图片,那就太麻烦了。 那将毁灭该服务的整个目标。 但这一场景,跟前面我提到的那个我主张不应放在服务器上的客户端会话状态,有何不同呢?

这是一个术语的问题。 无状态性(statelessness)意味着:有一种状态,是服务器不应保存的。 实际上,状态分两种。 从现在开始,我将区分应用状态(application state)与资源状态(resource state):前者应该保存在客户端,后者应该保存在服务端。

当你使用搜索引擎时,你的搜索请求和当前页码就属于应用状态。这些状态是因客户端而异的,可能你在浏览“水母”搜索结果的第3页,而我在浏览“老鼠”搜索结果的第1页。 由于我们使用搜索引擎的轨迹不同,所以我们的搜索请求与当前页码不一样。 我们各自的客户端分别保存着各自的应用状态(application state)。

一个Web服务,仅当实际收到你的请求时才关心你的应用状态,其他时刻,你的存在对它没有意义。 这就是说,每当客户端向服务器发请求时,请求里必须包含服务器处理该请求所需的所有应用状态。服务器会向客户端返回一些含有链接(这些链接代表客户端可以继续发出的请求)的页面,然后服务器与客户端的本次交互就结束了。当客户端向服务器发出下一个请求时,将建立另一次交互——我说Web服务应该是“无状态的(stateless)”,指的就是这个意思。 各个客户端应当自己管理自己的应用状态。

资源状态(resource state)对于每个客户端都是相同的,它应当保存在服务端。 当你向Flickr上传一个图片时,你就为它创建了一个新资源:这个新建的图片拥有自己的URI,你可以向这个URI发出请求。 你可以通过HTTP来获取、修改或删除这个“图片”资源。 它是所有人的资源,我也可以获取它。 该图片是一个资源状态,它一直保存在服务器上(直到被删除)。

应用状态有时会被用于非你所期望的场合。 许多Web服务会让你注册一个称为“API key”或“应用key”的唯一字符串,并要求你给每个请求都附上这个key,这样,服务器就可以通过这个key来限定你一天最多发送多少次请求。 例如,Google SOAP搜索服务(Google已弃用该服务)的API key,一天可以用于1000个请求。 一个key一天发出请求的数量就是客户端的状态,每个客户端都不一样。 一旦你的请求数量超出限制,服务对请求的处理将发生显著变化:你的第1000个请求可以成功获取所需的数据,但你的第1001个请求将得到错误信息。

与此同时,我才发出第402个请求,所以服务器仍然为我服务。
显然,服务器不能依靠客户端自己报告“请求计数”这一应用状态(application state),因为客户端有可能故意报错数字,以欺骗服务器。 但假如服务器自己保存该应用状态的话,就会违反无状态性(statelessness)原则。 这里的API key如同前面那个Rails应用里的_session_id cookie,它是用于获取保存在服务器端的客户端会话(有效期为1天)的键(key)。 这在一定程度上没什么问题,不过将导致可伸缩性(scalability)问题:假如该服务被分布于多台机器上,那么集群中的每台机器都得知道“你发出的是第1001个请求,而我发出的是第402个请求”(用行话讲,这叫会话复制(session replication))才行,只有这样才能让每台机器都知道应该“拒绝你的请求,允许我的请求”。 另一种方案是负载均衡系统要确保:对于你的每个请求,每次都让集群里的同一台机器来处理(用行话讲,这叫会话亲缘性(session affinity))。 无状态性将免除这些要求。 所以作为一名服务设计者,只有当你的资源状态需要被划分到不同机器上时,你才需要考虑数据复制(data replication)的问题。


关于本书更多信息,请看本书官方网站:http://restfulwebservices.cn/


--  作者:hjx_221
--  发布时间:7/4/2008 10:02:00 PM

--  
慢慢看看
谢谢了
--  作者:hanling911
--  发布时间:7/7/2008 2:42:00 PM

--  
很好,很强大
--  作者:supercreateboy
--  发布时间:7/26/2008 9:36:00 AM

--  
感谢分享
--  作者:jacken
--  发布时间:8/23/2008 7:47:00 PM

--  
好书 不错 谢谢!!
--  作者:dicolor
--  发布时间:9/16/2008 6:01:00 PM

--  
好长的文章,慢慢看了
--  作者:naonao
--  发布时间:10/10/2008 11:29:00 AM

--  
很好
--  作者:zhxiaomiao
--  发布时间:10/14/2008 5:16:00 PM

--  
没事了 就来看看
W 3 C h i n a ( since 2003 ) 旗 下 站 点
苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
1,148.438ms