常见的三种Web服务架构
相互竞争的服务架构
The Competing Architectures
我们已经给出了“不同Web服务会有不同做法”的两个主要问题,现在要据此对不同风格的Web服务进行分类了。根据我的研究,常见的Web服务架构主要有三种:REST式架构、RPC式架构和REST-RPC混合架构。下面依次对它们进行介绍。
REST式、面向资源的架构
RESTful, Resource-Oriented Architectures
本书的主题是符合REST风格的Web服务架构——按照Roy Fielding博士论文里的评判标准,它们可以获得很高的得分。现在,虽然许多架构从技术上说是REST式的(注3),但我希望关注那些最适合Web服务的架构;所以,当我谈及REST式Web服务时,我指的是那些具备Web特征的服务——称它们为面向资源的(resource-oriented)。将在第3章通过一个真实的Web服务——Amazon S3(Simple Storage Service)来介绍面向资源的REST的基本概念,然后在第4章,向你逐个介绍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里,那么服务就不是面向资源的。虽然并非只有这两条要求,但它们是很好的经验。
l 提供Atom发布协议(http://www.ietf.org/html.char ters/atompub-charter.html)及其变型的服务,例如GData(http://code.google.com/apis/gdata/)
l Amazon S3(Simple Storage Service)(http://aws.amazon.com/s3)
l Yahoo!提供的大部分Web服务(http://developer.yahoo.com/)
l 许多其他未采用SOAP的、只读的Web服务
l 静态网站
l 很多Web应用(尤其是像搜索引擎这种只读的)
每当谈到非REST式架构或非面向资源的架构时,我都是有一定目的的。这一章将在Programmable Web的背景之下,对REST式Web服务加以全面考察。在第2章会提到一些真实的Web服务,并指出:无论一个服务是否正好符合我所推荐的架构,你都可以采用同样的客户端工具访问它。在第10章,会就“应如何设计programmable web”这一久远的话题发表观点。
RPC式架构
RPC-Style Architectures
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,它对应于示例1-13中的lookupUPC),作用域信息包含在URI里。这个假想的服务暴露的URI不只一个,每个UPC代码都有与之对应的URI。与示例1-13不同的是,这里的HTTP信封是空的——它是一个没有实体主体的HTTP GET请求。
另一个RPC式服务的例子可以参见示例1-8:Google SOAP API是一个采用SOAP作为信封格式的RPC式服务。
较多采用或只采用HTTP POST的服务,多半是RPC式服务。虽然这不是绝对的,但至少可以说明该服务没有把HTTP方法用于表达方法信息。如果一个REST式服务过多地采用HTTP POST,那么它就容易演变为REST-RPC混合架构。
下面是一些知名的RPC式Web服务的例子:
l 所有采用XML-RPC的服务
l 几乎所有的SOAP服务(这一点是有争议的,本章后面的“Programmable Web涉及的技术”一节对此进行了探讨)
l 少部分Web应用(通常是没设计好的)
REST-RPC混合架构
REST-RPC Hybrid Architectures
这一术语是我创造的,它用于形容那些介于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:一个向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)数据,如我在第5章所讲。Flickr Web API是一种REST-RPC混合架构:当客户端通过GET方法获取数据时,它是REST式的;当客户端通过GET方法修改数据时,它是RPC式的。
一些知名的REST-RPC混合Web服务包括:
l del.icio.us API
l Flickr Web API
l 许多被说成是REST式架构的Web服务
l 大部分Web应用
从设计的角度来看,我认为不会有人特意把服务设计为REST-RPC混合架构。由于HTTP工作方式的原因,任何采用普通HTTP并暴露多个URI的RPC式服务,往往最终成为REST式架构或混合架构。许多程序员按他们设计Web应用的方式来设计Web服务,并最终形成混合架构的服务。
混合架构的存在已经造成了不少混乱。从事Web应用设计的人容易设计出REST-RPC混合架构,而且他们常常声称这种混合架构是REST式架构——他们仍在以设计human web的方式来设计Web服务。已经有不少功夫花费在分辨REST式架构与其他架构上了。我把“其他架构”称为REST-RPC混合架构,这是众多新词中的一个,我认为这个新词是看待这些常见而令人困惑的服务最准确有效的方式。假如你知道它们的其他称呼(在写本书的时候,“HTTP+POX”是最流行的),不妨继续往下读,后面我会用我自己的语言来解释这些其他术语。