SlideShare a Scribd company logo
1 of 30
Download to read offline
免费试读章节
                   (非印刷免费在线版)

                   如果你喜欢本书,请去

         China-pub、第二书店、卓越网、当当网

          购买印刷版以支持作者和InfoQ中文站

        向博文视点出版公司以及译者徐涵等致谢




本图书节选由InfoQ中文站免费发放,如果你从其它渠道获取此摘选,

         请注册InfoQ中文站以支持作者和出版商

                        本摘选主页为

    http://infoq.com/cn/articles/restlet-for-restful-services

      《RESTful Web Services 中文版》官方网站为

                 http://restfulwebservices.cn/
第3章

       REST 式服务有什么特别不同?
       What Makes RESTful Services
                        Different?

前面的例子只是为了引起你的兴趣,它们不全是 REST 式架构的,现在该看看正确的做法
了。尽管本书是关于 REST 式 Web 服务的,   但我前面向你展示的大部分服务都是 REST-RPC
混合服务(比如 del.icio.us API)——这些服务的工作方式跟 Web 上的其他应用不太一样。
这是因为目前跟 Web 理念保持一致的、知名的 REST 式服务还不太多。在前两章,目的
是列举一些你也许知道的真实服务,所以我只有选择那些例子。

del.icio.us API 和 Flickr API 都是典型的 REST-RPC 混合服务的例子:它们在获取数据时工
作方式跟 Web 一样,但在修改数据时就变成 RPC 式服务了。Yahoo!提供的各种搜索服务
都是纯 REST 式的,但它们太过简单,不是很好的例子。Amazon 的电子商务服务(见示
例 1-2)也太过简单,而且在一些重要的细节上呈现出 RPC 风格。

这些服务都是有用的服务。虽然认为 RPC 式 Web 服务不可取,但假如某个 RPC 式 Web
服务有用,我还是会编写 RPC 式客户端访问它的。不过,我仍不能在这里拿 Flickr API
或 del.icio.us API 作为如何设计 REST 式 Web 服务的示例,所以在上一章向你介绍了它们,
因为上一章的目的就是介绍 programmable web 上现有的一些服务、以及如何编写 HTTP
客户端。由于后面就要步入设计方面的章节了,所以我需要向你展示一下 REST 式和面向
资源的服务是什么样的。



介绍 Simple Storage Service
Introducing the Simple Storage Service
有两个流行的 Web 服务能满足这一目的:               Atom 发布协议(Atom Publishing Protocol,APP)
和 Amazon S3(Simple Storage Service,简单存储服务)(附录 A 给出了一些现有的、公
                                            。
开的 REST 式 Web 服务,那里有很多估计你都没听说过。               )由于 APP 只是一组关于构建服
务的指导,还称不上是实际的服务,所以我选择 S3 作为本章的示例,毕竟 S3 是 Web 上




                                                                         49
实际存在的。我会在第 9 章讨论 APP、Atom 及相关话题(例如 Google 的 GData 等)。本
章其余部分将主要探讨 S3。

你可以通过 S3 存储任何结构化的数据。                你的数据可以是私密的,也可以是能被任何人  (通
过 Web 浏览器或 BitTorrent 客户端)访问的。Amazon 为你提供存储空间和带宽,而你为
所占用的存储空间和产生的流量按千兆字节(GB)付费。要运行本章的 S3 示例代码,你
需要先到 http://aws.amazon.com/s3 注册一下。S3 的技术文档可以从这里获得:http://docs.
amazonwebservices.com/AmazonS3/2006-03-01/。

S3 主要被用作两种用途。

备份服务器
    你通过 S3 来保存自己的数据,他人访问不了你的数据。你不是自己购买备份盘,而
    是租用 Amazon 的磁盘空间。

数据寄存
    你把数据保存在 S3 上,并允许他人访问这些数据。Amazon 通过 HTTP 或 BitTorrent
    提供这些数据。你的流量费不是向 ISP 支付,而是向 Amazon 支付。根据你的流量情
    况,也许这样可以节省不少流量费。现在有不少创业公司都用 S3 来供应数据文件。

跟前面展示过的那些服务不同的是,S3 没有与之相应的网站。del.icio.us API 是基于
del.icio.us 网站的,Yahoo!搜索服务也是基于相应网站的,但是你在 amazon.com 上却找不
到给 S3 上传文件的 HTML 表单(form)   。S3 是只供程序使用的。(当然,如果你把 S3 用
作数据寄存目的,人们将能通过 Web 浏览器来使用它,但他们并不知道自己访问的是一
个 Web 服务。在他们看来,他们访问的是一个普通的网站。         )

Amazon 提供了 Ruby、Python、Java、C#和 Perl 语言的示例库(参见 http://developer.
amazonwebservices.com/connect/kbcategory.jspa?categoryID=47)。除了 Amazon 的官方库,
还有一些第三方库,比如用于 Ruby 的 AWS::S3(http://amazon.rubyforge.org/)——s3sh
命令行解释器(见示例 1-4)就出自这个库。



S3 的面向对象设计
Object-Oriented Design of S3
S3 基于两个概念:S3“桶(bucket)              ”和 S3“对象(object)。一个对象(object)就是一
                                                    ”
则附有元数据的具名的(named)数据。一个桶(bucket)就是一个用于容纳对象(object)
的具名的(named)容器。桶就好比硬盘上的文件系统,对象就好比该文件系统里的一个
文件。把桶(bucket)比喻为文件系统里的目录(directory)是一个误区,因为文件系统
里的目录可以嵌套,而桶不行。假如你希望桶具有层次结构,你只有通过给对象
“directory/subdirectory/file-object”式的命名来模拟。



50 │ 第 3 章:REST 式服务有什么特别不同?
关于桶
A Faw Words About Buckets
桶(bucket)有一则与之关联的信息:即名称(name)      。桶名(bucket name)可以包含以
下字符:英文大小写字母(a-z,A-Z)     、阿拉伯数字(0-9)“_”、  (下划线)“.”
                                                 、 (句点)
及“-”(短横线)  。我建议不要在桶名中使用大写字母。正如前面所说的,一个桶不能包
含另一个桶   (即桶是不能嵌套的) 桶只能包含对象。
                     ,           每个 S3 用户只能创建最多 100 个桶,
而且你的桶不能跟其他用户的桶重名。我建议你要么把所有对象都放在一个桶里,要么用
你自己的项目名称(projects names)或域名(domain names)来给桶命名。

关于对象
A Few Words About Objects
对象有四个相关部分:
   对象所在桶的引用。
   对象里的数据(S3 术语为“值(value)”)。
   对象的名称(S3 术语为“键(key)”)。
   与对象关联的一组元数据键-值对(metadata key-value pairs)。它们主要是自定义的
   元数据,不过也可以包含 ContentType 和 Content-Disposition 等标准 HTTP 报头
   的值。
如果我想把 O’Reilly 网站寄存在 S3 上的话,我会创建一个名为“oreilly.com”的桶,然后
在其中放入一些键(key)分别为“”      (空串)“catalog”“catalog/9780596529260”等的
                                、           、
对象。这些对象分别对应于 http://oreilly.com/、http://oreilly.com/catalog 等 URI。这些对象
的值将是 O’Reilly 网站的网页内容。这些 S3 对象的 Content-Type 元数据值将被设为
text/html——这样,  当这些对象被人们浏览时,       会被作为 HTML 文档           (而不是作为 XML
或纯文本)来处理。

假如 S3 是一个独立的面向对象库
What If S3 Was a Standalone Library?
假如把 S3 实现为一个面向对象代码库(而不是 Web 服务)的话,那么将会有 S3Bucket
和 S3Object 这两个类,它们各自均有用于数据成员读写的方法(如 S3Bucket#name、
S3Object.value=及 S3Bucket#addObject 等)S3Bucket 类将会有一个名为 S3Bucket#
                                      。
getObjects 的实例方法(返回 S3Object 实例的列表)和名为 S3Bucket.getBuckets 的
类方法(返回所有桶)       。示例 3-1 是这个类对应的 Ruby 代码。
示例 3-1:把 S3 实现为 Ruby 库
   class S3Bucket
     # 这个类方法用于获取所有桶
     def self.getBuckets
     end

     # 这个实例方法用于获取桶里的对象




                                                   S3 的面向对象设计 │ 51
def getObjects
     end
     ...
   end

   class S3Object
     # 获取与对象关联的数据
     def data
     end

     # 设置与对象关联的数据
     def data=(new_value)
     end
     ...
   end



资源
Resources
Amazon S3 提供两种 Web 服务:基于普通 HTTP 信封的 REST 式服务(RESTful service)
和基于 SOAP 信封的 RPC 式服务(RPC-style service)  。RPC 式 S3 服务暴露的功能(如
ListAllMyBuckets、CreateBucket 等)跟示例 3-1 里列出的方法比较相像。实际上,许
多 RPC 式 Web 服务都是由它们内部的实现方法(implementation methods)自动生成的,
它们暴露的服务接口跟它们在内部调用的编程语言接口是一样的。                    因为大多数现代编程语
言(包括面向对象的)都是过程式的(procedural)         ,所以可以这么做。

REST 式 S3 服务跟 RPC 式 S3 服务的功能一样,只不过它暴露的不是自己命名的函数,
而是暴露标准的 HTTP 对象(称为资源)      。资源(resource)响应的不是像 getObjects 这
样自己命名的方法,而是响应 GET、HEAD、POST、PUT、DELETE 和 OPTIONS 这些标
准的 HTTP 方法。

REST 式 S3 服务提供三种资源,它们(及相应的 URI)分别是:

   桶列表(https://s3.amazonaws.com/),这种类型的资源只有一个;

   一个特定的桶(https://s3.amazonaws.com/{name-of-bucket}/),这种类型的
   资源最多可以有 100 个;

   某个桶里的一个特定的 S3 对象 https://s3.amazonaws.com/{name-of-bucket}/
                  (
   {name-of-object}),这种类型的资源数量不限。

前面那个假想的面向对象 S3 库里的每个方法,都可以转化为上述三种资源与六种标准方
法的某种组合。例如,读方法 S3Object#name 对应于对“S3 对象”资源做 GET 请求;
写方法 S3Object#value=对应于对
                      “S3 对象”
                            资源做 PUT 请求。工厂方法(factory method)




52 │ 第 3 章:REST 式服务有什么特别不同?
(如 S3Bucket.getBuckets)和关系方法(relational method)如 S3Bucket#getObjects)
                                              (                      ,
分别对应于对“桶列表”资源和“桶”资源做 GET 请求。

每个资源都暴露同样的接口,并以同样的方式工作。如果要获取一个对象的值(value)        ,
就向该对象的 URI 发送 GET 请求;如果只要获取一个对象的元数据(metadata) ,就向该
对象的 URI 发送 HEAD 请求;如果要创建一个桶,就自己构造一个含有桶名(即你为将
创建的桶起的名称)的 URI,然后向该 URI 发送 PUT 请求;如果要往一个桶里添加对象,
就向含有桶名(即新建对象所在桶的名称)和对象名(即你为将创建的对象起的名称)的
URI 发送 PUT 请求;如果要删除一个桶或对象,就向其 URI 发送 DELETE 请求。

这些并非 S3 设计者的发明,实际上,根据 HTTP 标准,这些正是 GET、HEAD、PUT 和
DELETE 本来的用途。这四个方法(连同 S3 没有用到的 POST 和 OPTIONS 方法)足以
描述所有与 Web 资源的交互。要把程序作为 Web 服务发布的话,不需要发明新词汇,或
者在 URI 里给出自己的方法名称,你唯一须要做的就是仔细考虑资源的设计。所有 REST
式 Web 服务(无论多复杂)都支持同样一组基本操作,它们的复杂性都在资源上。

表 3-1 显示了当你向一个 S3 资源的 URI 发送 HTTP 请求时将发生什么。

表 3-1:S3 资源及其方法
                       GET            HEAD      PUT         DELETE
 桶列表(/)                列出所有桶          -         -           -

 一个桶(/{bucket})        列出桶里的对象        -         创建桶         删除桶
 一个对象                 获取对象的值及         获取对象      设置对象的
                                                            删除对象
 (/{bucket}/{object}) 元数据             的元数据      值及元数据

这个表格看起来有点奇怪。那我为什么还要让它占用这里的宝贵篇幅呢?因为这些方法的
作用都名副其实。在一个经良好设计的 REST 式服务里,每个方法的作用都名副其实。

仅凭目前的论述,可能你还难以信服。S3 是一个比较通用的服务。假如你所要实现的只
是在具名槽(named slots)里存放数据,那么你只要用 GET 和 PUT 就行了(分别用于读
和写)  。在第 5 章和第 6 章,我会告诉你如何把任何动作映射到统一接口上。为了让你相
信这一点,请注意,其实只要定义一个只响应 GET 方法的“桶列表”资源,就可以代替
S3Bucket.getBuckets 方法了;同样地,在这样的资源设计下,S3Bucket#addObject
(它要求每个对象与一些桶对应)也不需要了。

我们来跟 S3 的 RPC 式 SOAP 接口作个比较。如果采用 SOAP 接口的话,获取桶列表,要
用 ListAllMyBuckets 方法;获取桶里的内容,要用 ListBucket 方法。而采用 REST 式




                                                                资源 │ 53
接口的话,这些操作全部用 GET 方法。在 REST 式服务里,对象(面向对象意义上的)
由 URI 来标识,而方法名都是标准的。这些标准的方法,在不同的资源与服务上具有相
同的工作方式。



HTTP 响应代码
HTTP Response Codes
利用 HTTP 响应代码是 REST 式架构的另一个标志特征。如果发给 S3 一个请求,而且 S3
成功处理了这个请求,那么你得到的 HTTP 响应代码将是 200(          “OK”。你的浏览器在成
                                               )
功获取网页时,得到的也是这个响应代码。如果出错的话,响应代码将在 3xx、4xx、5xx
的范围内,比如 500(“Internal Server Error”。错误的响应代码告诉客户端:请勿把本次
                                   )
响应的元数据与实体主体当成对请求的响应。这个响应并不是客户端所要的,而是服务器
试图告诉客户端“出错了”    。由于响应代码不是放在元数据或实体主体里的,所以客户端
只要看 HTTP 响应的前三个字节就能知道有没有出错。

示例 3-2 是一个错误的响应的例子。我向一个不存在的对象(https://s3.amazonaws.
com/crummy.com/nonexistent/object)发送了 HTTP 请求,得到的响应代码是 404“Not
                                                         (
Found”。
      )

示例 3-2:一个 S3 的错误响应的例子
   404 Not Found
   Content-Type: application/xml
   Date: Fri, 10 Nov 2006 20:04:45 GMT
   Server: AmazonS3
   Transfer-Encoding: chunked
   X-amz-id-2: /sBIPQxHJCsyRXJwGWNzxuL5P+K96/Wvx4FhvVACbjRfNbhbDyBH5RC511sIz0w0
   X-amz-request-id: ED2168503ABB7BF4

   <?xml version=quot;1.0quot; encoding=quot;UTF-8quot;?>
   <Error>
    <Code>NoSuchKey</Code>
    <Message>The specified key does not exist.</Message>
    <Key>nonexistent/object</Key>
    <RequestId>ED2168503ABB7BF4</RequestId>
    <HostId>/sBIPQxHJCsyRXJwGWNzxuL5P+K96/Wvx4FhvVACbjRfNbhbDyBH5RC511sIz0w0</HostId>
   </Error>

HTTP 响应代码在 human web 上未得到充分利用。当你请求网页时,你的浏览器不会把
HTTP 响应代码向你显示出来,因为既然人们通过更友好的方式得知有没有出错,谁还愿
意去看无聊的数字代码呢?大多数 Web 应用在出错时都会返回 200(  “OK”,以及一个人
                                        )
类可读的错误描述文档。人们一般不会把错误描述文档误认为是他们请求的文档。

在 programmable web 上,情况刚好相反。计算机程序善于根据数字变量值的不同而采取
不同处理,但对于揣摩文档是什么“含义”则很不擅长。如果没有事先制定好规则,程序



54 │ 第 3 章:REST 式服务有什么特别不同?
将无法判断一个 XML 文档里包含的是数据还是错误描述。HTTP 响应代码正好能够充当
这个规则,它给出了关于客户端应当如何处理 HTTP 响应的一个大致的规则。因为响应代
码不是放在元数据或实体主体里的,所以即便客户端不知如何读取它们,也能了解发生了
什么情况。

除 200(“OK”   )和 404( Not Found”
                     “          )以外,S3 还使用了许多其他响应代码。最常见的
可能是 403( “Forbidden”  )了,它表示客户端发出的请求未包含正确的证书。S3 还使用其
他一些响应代码,如 400(         “Bad Request”(表明服务器无法理解来自客户端的数据)和
                                    )
409(“Conflict”(表明客户端要删除的桶是非空的)
              )                         。完整的列表请参阅 S3 技术文档的
“The REST Error Response”部分。我将在附录 B 对每个 HTTP 响应代码作逐一讲解(主
要关注它们在 Web 服务上的应用)           。官方的 HTTP 响应代码有 41 个,不过在日常使用中,
最重要的只有 10 个左右。



一个 S3 客户端
An S3 Client
因为已经有 Amazon 的示例库和一些第三方库(如 AWS::S3 等)了,所以一般我们是不需
要自己定制 S3 客户端库的。  但我介绍 S3 的目的,并不是告诉你存在这样一个多么有用的
Web 服务,而是想通过 S3 来举例说明 REST 背后的理论。所以,我将用 Ruby 来写一个
自己的 S3 客户端,并在编写过程中来逐步剖析它。

为了展示这是可行的,            我的库将在 S3 服务之上实现  (implement)一个面向对象的接口(就
像示例 3-1 中的那样)         。最终结果将类似于 ActiveRecord 或其他对象关系型数据映射组件
(object-relational mapper,ORM),不过在内部,它并不是发出 SQL 请求来往数据库里存
储数据,而是发出 HTTP 请求来通过 S3 服务存储数据。我在给我的方法起名时,将尽量
采用能够反映下层 REST 式接口的名称(比如 get、put 等)            ,而不是给出针对资源的名称
(如 getBuckets 和 getObjects 等)   。

我首先需要一个接口来处理 Amazon 特有的 Web 服务认证机制。        不过这并不如直接对 Web
服务进行实际考察更有意思,所以我准备暂时掠过这部分。我将先创建一个非常简单的
Ruby 模块 S3::Authorized,这样其他 S3 类就可以包含(include)它了。最后我会回到
这里,并补上有关细节。

示例 3-3 显示了一段初始代码。

示例 3-3:用 Ruby 编写 S3 客户端:初始代码
   #!/usr/bin/ruby -w
   # S3lib.rb

   # 发送 HTTP 请求和解析响应所需的库
   require 'rubygems'
   require 'rest-open-uri'




                                               一个 S3 客户端 │ 55
require 'rexml/document'

   # 对请求进行签名所需的库
   require 'openssl'
   require 'digest/sha1'
   require 'base64'
   require 'uri'

   module S3 # 一个容纳所有代码的模块的开头。

   module Authorized
     # 输入你的公共标识符(Amazon 称之为“Access Key ID” )和
     # 你的密钥(Amazon 称之为“Secret Access Key”。
                                         )
     # 这样,你对你发出的 S3 请求进行签名后,Amazon 就知道该向谁收费了。
     @@access_key_id = ''
     @@secret_access_key = ''

    if @@public_key.empty? or @@private_key.empty?
      raise quot;You need to set your S3 keys.quot;
    end

     # 你不应修改这里,除非你想使用 S3 的克隆(比如 Park Place)。
     HOST = 'https://s3.amazonaws.com/'
   end

这段骨架代码里,唯一值得关注的是:应在哪里填写“跟你的 Amazon Web 服务账户相关
联”的两个密钥。发出的每个 S3 请求都包含你的公共标识符(public identifier,Amazon
称之为“Access Key ID”,这样 Amazon 就可以识别出请求来自于你。发出的每个请求都
                 )
必须用你的密钥(Amazon 称之为“Secret Access Key”)进行签名,这样 Amazon 就知道
请求的确来自于你。该密钥只有你和 Amazon 知道,不应把它告诉任何其他人。如果让别
人知道了,那么他就可以用你的密钥发 S3 请求,令 Amazon 向你收费。


桶列表
The Bucket List
示例 3-4 是我为桶列表实现的面向对象类,它是我实现的第一个资源。我称这个类为 S3::
BucketList。

示例 3-4:用 Ruby 编写 S3 客户端:S3::BucketList 类

   # 桶列表
   class BucketList
     include Authorized

    # 获取该用户的所有桶
    def get
      buckets = []




56 │ 第 3 章:REST 式服务有什么特别不同?
# 向桶列表的 URI 发送 GET 请求,并读取返回的 XML 文档。
      doc = REXML::Document.new(open(HOST).read)

       # 对于每个桶...
       REXML::XPath.each(doc, quot;//Bucket/Namequot;) do |e|
         # ...创建一个 Bucket 对象,并把它添加到列表中。
         buckets << Bucket.new(e.text) if e.text
       end
       return buckets
     end
   end


                                XPath 讲解
以从右往左的顺序来读//Bucket/Name 这个 XPath 表达式,它的意思是:

    寻找所有 Name 标签                            Name
    哪里的 Name 标签?直接在 Bucket 标签下的             Bucket/
    哪里的 Bucket 标签?任何地方的                     //


现在 ,它是一个真正的 Web 服务客户端了。调 用 S3::BucketList#get ,就等于向
https://s3.amazonaws.com/(“桶列表”资源的 URI)发送一个加密的 HTTP GET 请求。S3
服 务 会 返 回 一 个 像 示 例 3-5 那 样 的 XML 文 档 —— “ 桶 列 表 ” 资 源 的 一 个 表 示
(representation) (我将在下一章介绍“表示”这个概念)      。该 XML 文档给出了关于“桶
列表”     资源的当前状态的一些信息:Owner 标签表明这个桶列表的所有者是谁             (我的 AWS
账户名是“leonardr28”;Buckets 标签里包含一些 Bucket 标签,这些 Bucket 标签是对
                       )
我的桶的描述(在本例中,因为只有一个桶,所以这里出现一个 Bucket 标签)                 。

示例 3-5:一个“桶列表”的例子
   <?xml version='1.0' encoding='UTF-8'?>
   <ListAllMyBucketsResult xmlns='http://s3.amazonaws.com/doc/2006-03-01/'>
    <Owner>
     <ID>c0363f7260f2f5fcf38d48039f4fb5cab21b060577817310be5170e7774aad70</ID>
     <DisplayName>leonardr28</DisplayName>
    </Owner>
    <Buckets>
     <Bucket>
      <Name>crummy.com</Name>
      <CreationDate>2006-10-26T18:46:45.000Z</CreationDate>
     </Bucket>
    </Buckets>
   </ListAllMyBucketsResult>

在这个简单的客户端应用里,我只关心桶的名称。XPath 表达式//Bucket/Name 将给出每
个桶的桶名,我在创建 Bucket 对象时需要这个信息。




                                                             一个 S3 客户端 │ 57
正如我们所看到的,这个 XML 文档缺少的一样东西,即链接(link)              。该 XML 文档给出
了每个桶的名称,但是没有给出这些桶的 URIs。从 REST 设计角度来看,这是 Amazon S3
的主要不足。好在根据一个桶的桶名(bucket name)得出 URI 并不难,参照我前面给出
的规则就行了:htps://s3.amazonaws.com/{name-of-bucket}/。


桶
The Bucket
现在,我们来编写 S3::Bucket 类(见示例 3-6)。这样,S3::BucketList.get 就能实例
化一些桶了。

示例 3-6:用 Ruby 编写 S3 客户端:S3::Bucket 类

   # 一个 S3 桶
   class Bucket
     include Authorized
     attr_accessor :name

    def initialize(name)
      @name = name
    end

    # 桶的 URI 等于服务的根 URI 加上桶名。
    def uri
      HOST + URI.escape(name)
    end

    # 在 S3 上保存这个桶。
    # 类似于在数据库里保存对象的 ActiveRecord::Base#save。
    # 关于 acl_policy,请看下面正文中的讨论。
    def put(acl_policy=nil)
      # 设置 HTTP 方法,作为 open()的参数。
      # 同时为该桶设置 S3 访问策略(如果有提供的话)
      args = {:method => :put}
      args[quot;x-amz-aclquot;] = acl_policy if acl_policy

      # 向该桶的 URI 发送 PUT 请求
      open(uri, args)
      return self
    end

    # 删除该桶。
    # 如果桶不为空的话,该删除操作将失败,
    # 并返回 HTTP 响应代码 409(   “Conflict”。
                                    )
    def delete
      # 向该桶的 URI 发送 DELETE 请求
      open(uri, :method => :delete)
    end

这里又实现了两个 Web 服务方法:S3::Bucket#put 和 S3::Bucket#delete。因为一个
桶的 URI 唯一标识了该桶,所以删除操作很简单:向该桶的 URI 发送一个 DELETE 请求


58 │ 第 3 章:REST 式服务有什么特别不同?
即可。因为桶的 URI 里包含了桶的名称,而且一个桶没有其他可设置的属性,所以创建
一个桶也很简单:向该 URI 发送一个 PUT 请求就行了。正如我将在编写 S3::Object 代
码时向你展示的,假如不是所有数据都放在 URL 里,PUT 请求的实现将会比较复杂。

虽然我刚才把 S3:: 类比作 ActiveRecord 类,不过 S3::Bucket#put 跟 ActiveRecord 实
现的 save 还是有点区别的。在一个由 ActiveRecord 控制的数据库表里,每行记录都有一
个唯一的数字 ID。如果你要修改一个 ID 为 23 的 ActiveRecord 对象的名称,那么你的修
改将体现为对一条 ID 为 23 的数据库记录的修改:
   SET name=quot;newnamequot; WHERE id=23

对于一个 S3 桶来说,它的 URI 就是它的永久 ID,桶的名称包含在 URI 里。假如你在调
用 put 时把桶的名称(name)换掉,那么客户端将做的,并不是在 S3 里修改桶的名称,
而是为新的 URI(含有你设置的新桶名)创建一个新的空桶。这是由 S3 程序员的设计造
成的。其实完全可以避免这样。Ruby on Rails 框架采用了与此不同的设计:当它通过一个
REST 式 Web 服务来暴露数据库记录时,每条记录的 URI 都含有该记录的数字 ID。假设
S3 是一个 Rails 服务的话,桶的 URI 将像这样/buckets/23——这样,修改桶名称时,就
不会改变其 URI 了。

现在来看 S3::Bucket 的最后一个方法 get。 S3::BucketList.get 一样,
                            跟                      这个方法(见
示例 3-7)向一个资源(这里是一个“桶”     )的 URI 发送 GET 请求,以获取一个 XML 文
档,然后根据该 XML 文档生成 S3::Object 类实例。这个方法支持以各种方式来过滤 S3
桶的内容,比方说,假如你只要获取那些“键(key)以某字符串开头”的对象,那么可
以采用:Prefix,等等。我就不对这些过滤选项作详细介绍了,如果你感兴趣,可以参阅
S3 技术文档的“Listing Keys”部分。

示例 3-7:用 Ruby 编写 S3 客户端:S3::Bucket 类(结束)

   # 获取桶里的(全部或部分)对象。
   #
   # 如果 S3 决定不返回整个桶(或子集)    ,那么
   # 第二个返回值将被设为 true。要获取其余对象,
   # 你需要调整子集选项(subset option) (本书未作介绍)   。
   #
   # 子集选项包括::Prefix、:Marker、:Delimiter、:MaxKeys。
   # 有关详情,请参阅 S3 技术文档的“Listing Keys”部分。
   def get(options={})
      # 获取该桶的基准 URI(base URI),并把子集选项(假如有的话)
      # 附加到查询字符串(query string)上。
      uri = uri()
      suffix = '?'

     # 对于用户提供的每个选项...
     options.each do |param, value|
        # ...如果属于某个 S3 子集选项...
        if [:Prefix, :Marker, :Delimiter, :MaxKeys].member? :param
          # ...把它附加到 URI 上
          uri << suffix << param.to_s << '=' << URI.escape(value)



                                                       一个 S3 客户端 │ 59
suffix = '&'
       end
     end

     # 现在我们已经构造好了 URI。向该 URI 发送 GET 请求,
     # 并读取含有 S3 对象信息的 XML 文档。
     doc = REXML::Document.new(open(uri).read)
     there_are_more = REXML::XPath.first(doc, quot;//IsTruncatedquot;).text == quot;truequot;

      # 构建一个 S3::Object 对象的列表
      objects = []
      # 对于桶里的每个 S3 对象...
      REXML::XPath.each(doc, quot;//Contents/Keyquot;) do |e|
        # ...构造一个 S3::Object 对象,并把它添加到列表中。
        objects << Object.new(self, e.text) if e.text
      end
      return objects, there_are_more
    end
  end


                              XPath 讲解
以从右往左的顺序来读//IsTruncated 这个 XPath 表达式,它的意思是:

   寻找所有 IsTruncated 标签                IsTruncated
   哪里的 IsTruncated 标签?任何地方的           //


向应用的根 URI 发送 GET 请求,你可以得到“桶列表(bucket list)  ”这个资源(resource)
的一个表示(representation)。向一个“桶”资源的 URI 发送 GET 请求,你可以得到该“桶
(bucket)”的一个表示,即一个像示例 3-8 那样的 XML 文档,其中的 Contents 元素包
含该桶的一些信息。

示例 3-8:一个“桶”的表示
  <?xml version='1.0' encoding='UTF-8'?>
  <ListBucketResult xmlns=quot;http://s3.amazonaws.com/doc/2006-03-01/quot;>
   <Name>crummy.com</Name>
   <Prefix></Prefix>
   <Marker></Marker>
   <MaxKeys>1000</MaxKeys>
   <IsTruncated>false</IsTruncated>
   <Contents>
    <Key>mydocument</Key>
    <LastModified>2006-10-27T16:01:19.000Z</LastModified>
    <ETag>quot;93bede57fd3818f93eedce0def329cc7quot;</ETag>
    <Size>22</Size>
    <Owner>
     <ID>
      c0363f7260f2f5fcf38d48039f4fb5cab21b060577817310be5170e7774aad70</ID>
      <DisplayName>leonardr28</DisplayName>
    </Owner>



60 │ 第 3 章:REST 式服务有什么特别不同?
<StorageClass>STANDARD</StorageClass>
    </Contents>
   </ListBucketResult>

在本例中,    该文档值得关注的部分是该桶的对象列表。       一个对象(object)是由它的键(key)
标识的,所以我用 XPath 表达式“//Contents/Key”来获取此信息。另外,有一个布尔值
( /IsTruncated”
 “/            )也值得关注,它表示文档里是否包括了桶中所有对象的键(key)       ,S3
有没有因为太多了放不下而截短了。
同样地,这个表示里主要缺少的就是链接(link)           。该文档给出了许多有关对象的信息,但
它没有给出对象的 URI。S3 假定客户端知道如何根据对象名称构造对象的 URI。好在构
造一个对象的 URI 并不难,       参照我前面给出的规则就行了:   https://s3.amazonaws.com/
{name-of-bucket}/{name-of-object}。



S3 对象
The S3 Object
现在我们要为 S3 服务的核心——S3 对象——实现接口了。记住,一个 S3 对象(object)
只是一个具有名称(键)和一组元数据键-值对(如 Content-Type=quot;text/htmlquot;)的数
据串。当你向桶列表(bucket list)或桶(bucket)发送 GET 请求时,S3 会返回一个 XML
文档。当你向一个对象发送 GET 请求时, 会逐个字节地返回你先前向该对象 PUT 的数
                         S3
据串。

示例 3-9 给出了 S3::Object 类的开头部分,现在你对它应该已经不陌生了。

示例 3-9:用 Ruby 编写 S3 客户端:S3::Object 类

   # 跟某个桶关联的一个具有值和元数据的 S3 对象。
   class Object
     include Authorized

    # 客户端可以知道对象在哪个桶里
    attr_reader :bucket

    # 客户端可以读写对象的名称
    attr_accessor :name

    # 客户端可以写对象的元数据和值
    # 稍后我将定义相应的“读”方法
    attr_writer :metadata, :value

    def initialize(bucket, name, value=nil, metadata=nil)
      @bucket, @name, @value, @metadata = bucket, name, value, metadata
    end

    # 对象的 URI 等于所在桶的 URI 加上该对象的名称
    def uri
      @bucket.uri + '/' + URI.escape(name)
    end



                                                       一个 S3 客户端 │ 61
下面是我实现的第一个 HTTP HEAD 请求。它用于获取一个 S3 对象的元数据键-值对
(metadata key-value pairs) 并填充元数据 hash store_metadata 的实现在本类的最后)
                          ,          (                          。
由于我是用 rest-open-uri 发送请求的,所以发送 HEAD 请求的代码看上去跟发送其他
HTTP 请求的代码差不多(见示例 3-10)         。

示例 3-10:用 Ruby 编写 S3 客户端:S3::Object#metadata 方法

    # 获取对象的元数据 hash
    def metadata
      # 如果没有元数据...
      unless @metadata
        # 向对象的 URI 发送一个 HEAD 请求,并从响应的 HTTP 报头里读取元数据。
        begin
          store_metadata(open(uri, :method => :head).meta)
        rescue OpenURI::HTTPError => e
          if e.io.status == [quot;404quot;, quot;Not Foundquot;]
            # 假如没有元数据是因为对象不存在,这不算错误。
            @metadata = {}
          else
            # 其他情况,作错误处理。
            raise e
          end
        end

      end
      return @metadata
    end

这段代码的作用是获取一个对象的元数据,而不必获取该对象本身。这就跟下载一则电影
评论跟下载该电影本身的区别一样;而且,如果你需要为流量付费的话,就更能体会到二
者的差别了。元数据(metadata)与表示(representation)的区别并非只在 S3 中有,它是
所有面向资源的 Web 服务都有的。HEAD 方法令任何客户端可以获取任一资源的元数据,
而不必获取其表示(可能不只一个)的方法。

当然,有时你确实要下载电影本身,那么就需要 GET 请求了。在示例 3-11 中,我在存取
方法 (accessor method) S3::Object#value 里使用了 GET 请求。这个方法的结构跟
S3::Object#metadata 的差不多。

示例 3-11:用 Ruby 编写 S3 客户端:S3::Object#value 方法

    # 获取对象的值和元数据
    def value
      # 没有如果没有值...
      unless @value
        # 向对象的 URI 发送 GET 请求
        response = open(uri)




62 │ 第 3 章:REST 式服务有什么特别不同?
# 从响应的 HTTP 报头里读取元数据
        store_metadata(response.meta) unless @metadata
        # 从实体主体里读取值
        @value = response.read
      end
      return @value
    end

在 S3 服务上创建 S3 对象跟创建 S3 桶的原理一样:向一个 URI 发送 PUT 请求即可。对
于 S3 桶(bucket)的创建,PUT 请求是比较简单的,因为一个桶除了名称(已包含在 PUT
请求的 URI 里)以外,没有其他属性。而对于 S3 对象(object)的创建,PUT 请求要复
杂一些,因为 HTTP 客户端要在 PUT 请求里指定对象的元数据(比如 Content-Type)和
值(我们之后可以用 HEAD 或 GET 请求来获取这些信息)   。

好在构造一个创建 S3 对象的 PUT 请求不是十分复杂,因为客户端可以决定对象的值,而
且不需要把对象的值包装为 XML 文档或其他形式,            只要把它按原状放入 PUT 请求的实体
主体(entity-body)里,并按照 metadta hash 里的各项元数据设置好 HTTP 报头(headers)
就行了(见示例 3-12)   。

示例 3-12:用 Ruby 编写 S3 客户端:S3::Object#put 方法

    # 在 S3 上保存对象
    def put(acl_policy=nil)

      # 以原始元数据的副本开始,或者,
      # 如果没有元数据的话,就以空 hash 开始。
      args = @metadata ? @metadata.clone : {}

      # 设置 HTTP 方法、实体主体及一些另外的 HTTP 报头
      args[:method] = :put
      args[quot;x-amz-aclquot;] = acl_policy if acl_policy
      if @value
        args[quot;Content-Lengthquot;] = @value.size.to_s
        args[:body] = @value
      end

      # 向对象的 URI 发送 PUT 请求
      open(uri, args)
      return self
    end

S3::Object#delete 的实现(见示例 3-13)跟 S3::Bucket#delete 一模一样。

示例 3-13:用 Ruby 编写 S3 客户端:S3::Object#delete 方法

    # 删除对象
    def delete
      # 向对象的 URI 发送 DELETE 请求
      open(uri, :method => :delete)
    end




                                                         一个 S3 客户端 │ 63
示例 3-14 显示的是一个用于“根据 HTTP 响应报头生成 S3 对象元数据”的方法。你应该
为你设置的所有元数据报头(除 Content-Type 以外)添加前缀“x-amz-meta-”,否则它
们被发给 S3 服务器后, 就不会再回到 Web 服务客户端——S3 服务器会认为它们是因为客
户端软件故障产生的,并丢弃它们。

示例 3-14:用 Ruby 编写 S3 客户端:S3::Object#store_metadata 方法
    private

     # 给定一个包含 HTTP 响应报头的 hash,
     # 选取那些跟 S3 对象相关的报头,然后把它们保存在实例变量@metadata 里。
     def store_metadata(new_metadata)
       @metadata = {}
       new_metadata.each do |h,v|
         if RELEVANT_HEADERS.member?(h) || h.index('x-amz-meta') == 0
           @metadata[h] = v
         end
       end
     end
     RELEVANT_HEADERS = ['content-type', 'content-disposition', 'content-range',
                           'x-amz-missing-meta']
   end



对请求进行签名及访问控制
Request Signing and Access Control
我已经把关于 S3 认证的话题尽量延后了,现在是时候对它作一下讲解了。假如你主要关
心的是 REST 式服务的概况,可以略过本节,直接跳到“使用 S3 客户端库”一节。不过
假如你对 S3 的内部工作原理有兴趣的话,请继续阅读。

用我前面展示的代码是可以发出 HTTP 请求,不过这些请求到达 S3 后会被拒绝处理,因
为这些请求里没有包含关键的 Authorization 报头——这样,S3 就无法证实你是不是这
些桶的主人。别忘了,你是要为你在 S3 服务上存放的数据及产生的流量向 Amazon 付费
的。假如 S3 对操作桶的请求不加以认证就执行的话,那么你可能要为别人在你的桶里存
放的数据埋单。

大多数需要认证的 Web 服务,都采用标准的 HTTP 机制来核实你是否的确是自称的那个
人。但 S3 的需求比较复杂。在大多数 Web 服务里,你绝不希望自己的数据被他人使用。
但是 S3 的用途之一就是用于寄存服务。你也许会把一部电影寄存在 S3 上,供人们用
BitTorrent 来下载(当然你要为此付费)。

或者,你也许想出售对存放在 S3 上的电影文件的访问权。你的电子商务网站在收到一位
客户的付款后,把 S3 上的文件的 URI 告诉他。这意味着,你在授权他人“作特定 Web
服务调用(GET 请求)”的权利,而由你来付费。




64 │ 第 3 章:REST 式服务有什么特别不同?
标准的 HTTP 认证机制无法为此种应用提供安全性支持。因为一般来说,标准的 HTTP 认
证机制需要让发送 HTTP 请求的人知道实际的密码。你可以防止别人窃取密码,但不能对
另一个人说“这是我的密码,但你必须保证你只能用它来请求这个 URI。 ”

在 S3 里,这个问题是用消息认证代码(Message Authentication Code,MAC)解决的。你
每次发送 S3 请求时,都用你的密钥(只有你和 Amazon 知道)来对请求中的重要部分(比
如:URI、HTTP 方法,以及一些 HTTP 报头)进行签名。因为只有知道密钥的人才能够
为请求生成正确的签名,所以 Amazon 由此可以确信应该向你收费。在对一个请求签名以
后,可以把该签名发给第三方(不必告诉他你的密钥)         ;该第三方由于拥有你的签名,所
以他可以发送签名的那个请求,   并令 Amazon 向你收费。   总之,    其他人可以在一定时间内,
像你一样发出特定的请求,而不必知道你的密钥。

要为你的 S3 对象开通匿名访问权限,有一种较为简单的方法(后面我会谈到)                。但是对自
己的请求进行签名是无法避免的,所以即使是像这样的一个简单的库,也必须支持对请求
进行签名才行。我将对 S3::Authorized 模块进行重新编写,为它增加一项功能:截取对
open 方法的调用,并在发出 HTTP 请求前对它进行签名。因为 S3::BucketList 、
S3::Bucket 和 S3::Object 都已经包含(include)了这个模块,所以它们将自动继承该
功能。假如不为 S3::Authorized 模块添加这个功能,那么我前面在各个类里定义的所有
open 调用,都将发送未签名的 HTTP 请求——S3 将对这些请求返回响应代码 403
(“Forbidden”。 S3::Authorized 模块添加这个功能后,
            )为                           你就可以生成已签名的 HTTP
请求,这样就可以通过 S3 的安全策略了(并让你支付费用)             。示例 3-15 及后面一些示例
中的代码都相当依赖于 Amazon 自己的示例 S3 库。

示例 3-15:用 Ruby 编写 S3 客户端:S3::Authorized 模块
   module Authorized
     # 这些是 S3 认为跟签名请求相关的标准 HTTP 报头
     INTERESTING_HEADERS = ['content-type', 'content-md5', 'date']

    # 这些前缀用于自定义元数据报头。
    # 所有自定义元数据报头都被认为跟签名请求相关
    AMAZON_HEADER_PREFIX = 'x-amz-'

    # 为用 rest-open-uri 实现的 open()方法针对 S3 作的一个封装。
    # 该实现在发送请求之前进行一些 HTTP 报头的设置,
    # 其中最重要的是 Authorization 报头,Amazon 将据此决定该向谁收费。
    def open(uri, headers_and_options={}, *args, &block)
      headers_and_options = headers_and_options.dup
      headers_and_options['Date'] ||= Time.now.httpdate
      headers_and_options['Content-Type'] ||= ''




                                              对请求进行签名及访问控制 │ 65
signed = signature(uri, headers_and_options[:method] || :get,
                       headers_and_options)
      headers_and_options['Authorization'] = quot;AWS #{@@public_key}:#{signed}quot;
      Kernel::open(uri, headers_and_options, *args, &block)
    end

现在的一项艰巨任务是实现 signature 方法。这个方法的作用是构建一个加密字符串。
该加密字符串将被放入请求的 Authorization 报头里,以令 S3 服务相信请求确实是你发
的,或者是你授权别人发的(见示例 3-16)  。

示例 3-16:用 Ruby 编写 S3 客户端:Authorized#signature 模块

    # 为 HTTP 请求构造加密签名。
    # 这是(用你的密钥)对一个“规范化字符串”的签名,
    # 其中含有跟该请求相关的所有信息。
    def signature(uri, method=:get, headers={}, expires=nil)
      # URI 或者是个字符串,或者是个 Ruby URI 对象。
      if uri.respond_to? :path
        path = uri.path
      else
        uri = URI.parse(uri)
        path = uri.path + (uri.query ? quot;?quot; + query : quot;quot;)
      end

      # 构造规范化字符串,并对它进行签名。
      signed_string = sign(canonical_string(method, path, headers, expires))
    end

注意,本方法是通过对 canonical_string 的返回值调用 sign 来构造规范化字符串的。
我们来看一下这两个方法。先看 canonical_string,它把一个 HTTP 请求转换成一个如
示例 3-17 所示的字符串,该字符串具有特定的格式,其中含有(从 S3 的角度来看)跟
HTTP 请求相关的所有信息。这些相关信息包括:HTTP 方法(PUT) Content-type   、
( ext/plain”、日期、其他一些 HTTP 报头(
 “t        )                     “x-amz-metadata”,以及 URI 里的路径部
                                                 )
分( /crummy.com/myobject”。sign 方法将对这个字符串进行签名。任何人都知道如何
  “                     )
创建上述字符串,但只有 S3 注册用户和 Amazon 自己知道如何生成正确的签名。

示例 3-17:一个请求的规范化字符串
   PUT

   text/plain
   Fri, 27 Oct 2006 21:22:41 GMT
   x-amz-metadata:Here's some metadata for the myobject object.
   /crummy.com/myobject

Amazon 服务器收到你的 HTTP 请求后,它会根据请求也生成一个规范化字符串,并对它
进行签名(别忘了,Amazon 也知道你的密钥)   ,然后 Amazon 服务器将比较两个签名,看
是否匹配。S3 的身份认证就是这样实现的。如果签名匹配,你的请求将被执行;否则,
你将得到响应代码 403( “Forbidden”。
                         )



66 │ 第 3 章:REST 式服务有什么特别不同?
示例 3-18 显示的是用于生成规范化字符串的代码。

示例 3-18:用 Ruby 编写 S3 客户端:Authorized#canonical_string 方法

    # 根据 HTTP 请求的各个部分生成一个用于签名的字符串。
    def canonical_string(method, path, headers, expires=nil)

      # 为所有相关报头设置默认值
      sign_headers = {}
      INTERESTING_HEADERS.each { |header| sign_headers[header] = '' }

      # 把实际的值(包括用于自定义 S3 报头的值)复制进来
      headers.each do |header, value|
        if header.respond_to? :to_str
          header = header.downcase
          # 如果是一个自定义报头或 S3 认为相关的报头...
          if INTERESTING_HEADERS.member?(header) ||
              header.index(AMAZON_HEADER_PREFIX) == 0
            # 把它加入到报头里
            sign_headers[header] = value.to_s.strip
          end
        end
      end

      # 这个库不需要 Amazon 定义的 x-amz-date 报头,不过可能会有人设置这个报头。
      # 假如设置了这个报头的话,我们就不采用 HTTP 的标准 Date 报头。
      sign_headers['date'] = '' if sign_headers.has_key? 'x-amz-date'

      # 如果提供了过期时间的话,那么它优先于其他 Date 报头。
      # 这个签名将在过期时间内都有效。
      sign_headers['date'] = expires.to_s if expires

      # 现在,我们开始为该请求构造规范化字符串。我们从 HTTP 方法开始。
      canonical = method.to_s.upcase + quot;nquot;

      # 把所有报头按名称排序,然后把这些报头(或仅仅是值)添加到将被签名的字符串里。
      sign_headers.sort_by { |h| h[0] }.each do |header, value|
        canonical << header << quot;:quot; if header.index(AMAZON_HEADER_PREFIX) == 0
        canonical << value << quot;nquot;
      end

      # 最后对 URI 路径进行签名。我们去掉查询字符串(query string)        ,
      # 并附上一个专门的 S3 查询参数:       ‘acl’‘torrent’或‘logging’
                                     、                 。
      canonical << path.gsub(/?.*$/, '')

      for param in ['acl', 'torrent', 'logging']
        if path =~ Regexp.new(quot;[&?]#{param}($|&|=)quot;)
          canonical << quot;?quot; << param




                                                 对请求进行签名及访问控制 │ 67
break
         end
       end
       return canonical
     end

在 sign 的实现里,实际起作用的是 Ruby 的标准加密与编码接口(见示例 3-19)。

示例 3-19:用 Ruby 编写 S3 客户端:Authorized#sign 方法

     # 用客户端的 Secret Access Key(即密钥)对一个字符串进行签名,
     # 并用 base64 把签名后得到的二进制串编码为纯 ASCII 码。
     def sign(str)
       digest_generator = OpenSSL::Digest::Digest.new('sha1')
       digest = OpenSSL::HMAC.digest(digest_generator, @@private_key, str)
       return Base64.encode64(digest).strip
     end



对 URI 进行签名
Signing a URI
我的 S3 库还有一个功能要实现。我已多次提到,S3 允许你在对一个 HTTP 请求签名后,
把 URI 给其他人,这样他们就可以像你一样发送请求了。       实现这一目标需要用到 signed_
uri 这个方法(见示例 3-20)
                 。不是用 open 来发送 HTTP 请求,而是把 open 的参数传给
这个方法,然后它会给你一个签了名的 URI(别人可以像你一样用这个 URI)          。为防止滥
用, 个签了名的 URI 只在一定时间内有效。
   一                       你可以通过为 :expires 参数传进一个 Time
对象来设置这个时间。

示例 3-20:用 Ruby 编写 S3 客户端:Authorized#signed_uri 方法

     # 根据一个 HTTP 请求的信息,返回一个你可以给别人用的 URI,
     # 令他们可以像你一样发出特定的 HTTP 请求。
     # 该 URI 将在你所指定的时间(默认为 15 分钟)内有效。
     def signed_uri(headers_and_options={})
       expires = headers_and_options[:expires] || (Time.now.to_i + (15 * 60))
       expires = expires.to_i if expires.respond_to? :to_i
       headers_and_options.delete(:expires)
       signature = URI.escape(signature(uri, headers_and_options[:method],
                                           headers_and_options, nil))
       q = (uri.index(quot;?quot;)) ? quot;&quot; : quot;?quot;
       quot;#{uri}#{q}Signature=#{signature}&Expires=#{expires}&AWSAccessKeyId=#{@@public_key}quot;
     end
   end

   end # 还记得那个容纳所有代码的 S3 模块的开头吗?这里是它的结尾。

它的工作原理是这样:假设我想给一个客户访问我保存在 https://s3.amazonaws.com/
BobProductions/KomodoDragon.avi 的文件的权限,可以运行如示例 3-21 所示的代码为他
生成一个 URI。



68 │ 第 3 章:REST 式服务有什么特别不同?
示例 3-21:生成一个签了名的 URI
   #!/usr/bin/ruby1.9
   # s3-signed-uri.rb
   require 'S3lib'

   bucket = S3::Bucket.new(quot;BobProductionsquot;)
   object = S3::Object.new(bucket, quot;KomodoDragon.aviquot;)
   puts object.signed_uri
   # quot;https://s3.amazonaws.com/BobProductions/KomodoDragon.avi
   # ?Signature=J%2Fu6kxT3j0zHaFXjsLbowgpzExQ%3D
   # &Expires=1162156499&AWSAccessKeyId=0F9DBXKB5274JKTJ8DG2quot;

这个 URI 将在 15 分钟内有效  (默认值) 该 URI 中含有我的公共标识符 AWSAccessKeyId)
                          。               (              、
过期时间(Expires)及加密的签名(Signature)    。我的客户可以访问这个 URI,并下载
电影文件 KomodoDragon.avi——由此产生的流量费用将由我向 Amazon 支付。  假如某个客
户对 URI 里的任何部分作修改的话      (比如他想下载另一部电影) S3 服务会拒绝他的要求。
                                        ,
也许有的客户会把这个 URI 发给其他人用,不过 15 分钟后这个 URI 就失效了。

你也许已经发现这里的一个问题了。规范化字符串里通常含有 Date 报头的值,如此一来,
客户访问这个签过名的 URI 时,它们的 Web 浏览器发出的日期报头的值肯定跟签名里的
不一样。这就是为什么在生成规范化字符串时,你要设置一个过期日期(expiration)而不
是请求日期的原因。回顾一下示例 3-18 中 canonical_string 的实现可以看到,过期日
期(假如有的话)是优先于 Date 报头的。


设置访问策略
Setting Access Policy
假如我想令一个对象可被公开访问的话,该怎么做呢?我想把一些文件对外界开放,并让
Amazon 来处理烦人的服务器管理问题。嗯,其实我可以把过期日期设为一个很久远的日
子,并公开发布所有已签名的 URI。不过,还有一种更简单的实现办法:即允许匿名访问。
你可以为桶(bucket)或对象(object)设置访问策略(access policy),告诉 S3 可以响应
未签名的请求。你可以在发送用于创建桶或对象的 PUT 请求时,附上 x-amz-acl 报头来
设置访问策略。

这就是 Bucket#put 和 Object#put 方法的 acl_policy 参数的作用。如果你想令一个桶
或对象成为公开可读或公开可写的,你就为 acl_policy 参数设置适当的参数值就行了。
我的客户端是把该值作为自定义 HTTP 请求报头 x-amz-acl 的一部分来发送的。Amazon
S3 会读取这个请求报头,并为桶或对象设置相应的访问规则。

示例 3-22 所示的客户端创建了一个 S3 对象,任何人均可通过其 URI https://s3.
amazonaws.com/BobProductions/KomodoDragon-Trailer.avi 来读取它。这里,我并
不是出售该电影文件,我只是把 Amazon 作为寄存服务使用,省去了自己做网站来存放这
些文件的麻烦。



                                              对请求进行签名及访问控制 │ 69
示例 3-22:创建一个可公开读取的对象
   #!/usr/bin/ruby -w
   # s3-public-object.rb
   require 'S3lib'

   bucket = S3::Bucket.new(quot;BobProductionsquot;)
   object = S3::Object.new(bucket, quot;KomodoDragon-Trailer.aviquot;)
   object.put(quot;public-readquot;)

S3 支持下列四种访问策略。
private
    这是默认的。只有用你的密钥签过名的请求才被接受。
public-read
    可以接受未签名的 GET 请求。这意味着,任何人都可以下载一个对象或列出一个桶
    里的内容。
public-write
    可以接受未签名的 GET 和 PUT 请求。这意味着,任何人都可以修改一个对象或向桶
    里添加对象。
authenticated-read
    未签名的请求将被拒绝,但是用别的 S3 用户的密钥签名的读请求也将被接受。这就
    是说,任何拥有 S3 账户的人都可以下载你的对象或列出桶里的内容。

给桶或对象设置权限,还有细粒度的方法。我没有介绍这部分,假如你有兴趣,可以参阅
S3 技术文档的“Setting Access Policy with REST”部分。那里将向你介绍一个平行的附属
资源空间:    每个桶 /{name-of-bucket} 有一个影子资源      (shadow resource) /{name-of-
bucket}?acl,该影子资源对应于该桶的访问控制规则;同样地,每个对象 /{name-of-
bucket}/{name-of-object} 也 有 一 个 影 子 资 源 /{name-of-bucket}/{name-of-
object}?acl。你可以通过向这些 URIs 发送 PUT 请求(在实体主体里给出 XML 格式的
访问控制列表)     ,为特定的桶或对象设置特定的权限,或针对特定 S3 用户设定访问权限。



使用 S3 客户端库
Using the S3 Client Library
到目前为止,我的 Ruby 客户端库已经可以访问差不多 S3 服务的全部功能了。当然,一
个库要有用户,才是有用的库。在上一节,我通过一些简单的客户端向你展示了安全方面
的要点,现在我想展示一些更重要的东西。

示例 3-23 是一个简单的命令行 S3 客户端,它将创建一个桶和一个对象,然后列出桶里的
内容。该示例可以让你从较高的层次上理解 S3 资源是如何相互协作的。我在注释里标出
了触发 HTTP 请求各行。



70 │ 第 3 章:REST 式服务有什么特别不同?
示例 3-23:一个 S3 客户端的示例
  #!/usr/bin/ruby -w
  # s3-sample-client.rb
  require 'S3lib'

  # 收集命令行参数
  bucket_name, object_name, object_value = ARGV
  unless bucket_name
    puts quot;Usage: #{$0} [bucket name] [object name] [object value]quot;
    exit
  end

  # 找到或创建桶
  buckets = S3::BucketList.new.get      # GET /
  bucket = buckets.detect { |b| b.name == bucket_name }
  if bucket
    puts quot;Found bucket #{bucket_name}.quot;
  else
    puts quot;Could not find bucket #{bucket_name}, creating it.quot;
    bucket = S3::Bucket.new(bucket_name)
    bucket.put                          # PUT /{bucket}
  end

  # 创建对象
  object = S3::Object.new(bucket, object_name)
  object.metadata['content-type'] = 'text/plain'
  object.value = object_value
  object.put                            # PUT /{bucket}/{object}

  # 对于桶里的每个对象...
  bucket.get[0].each do |o|             # GET /{bucket}
    # ...打印出有关该对象的信息
    puts quot;Name: #{o.name}quot;
    puts quot;Value: #{o.value}quot;            # GET /{bucket}/{object}
    puts quot;Metadata hash: #{o.metadata.inspect}quot;
    puts
  end


用 ActiveResource 创建透明的客户端
Clients Made Transparent with ActiveResource
既然所有 REST 式 Web 服务暴露的都是基本相同的简单接口,那么定制一个适用于所有
Web 服务的客户端并不是难事——不过这有点浪费了。你另外有两个方案可选:              (1)用
WADL 文档(上一章介绍过,第 9 章将详细介绍)来描述服务,然后通过一个通用 WADL
客户端来访问它;  (2)有一个名为 ActiveResource 的 Ruby 库,它使得为某些种类的 Web
服务编写客户端变得轻而易举。
ActiveResource 用于访问那些暴露出关系数据库里的记录与数据的 Web 服务。   WADL 可用
于描述几乎各类 Web 服务, ActiveResource 只能用于符合一定规则的 Web 服务。
                    但                             Ruby on




                                     用 ActiveResource 创建透明的客户端 │ 71
Rails 是目前唯一符合这些规则的框架。不过,一个服务只要通过跟 Rails 一样的 REST
式接口暴露出数据库,就能响应来自 ActiveResource 客户端的请求了。

在本书编写之时,可用 ActiveResource 客户端调用的公共 Web 服务还不多(我在附录 A
中列出了一些)。下面,我来创建一个简单的 Rails Web 服务。我将能够用 ActiveResource
来调用这个服务,而不必为此编写任何 HTTP 客户端或 XML 解析代码。


创建一个简单的服务
Creating a Simple Service
我要创建的是一个简单的笔记本 (notebook)Web 服务:它能保存带时间戳的笔记(notes)。
因为我的计算机上已经安装了 Rails 1.2,所以可以像下面这样来创建这个笔记本服务:
   $ rails notebook
   $ cd notebook

我在自己的计算机上创建了一个名为 notebook_development 的数据库,然后编辑 Rails
文件 notebook/config/database.yml,把连接这个数据库所需的信息提供给 Rails。任何 Rails
的一般性指南都会对这些初始步骤有较详细的介绍。
现在,  我已经创建了一个 Rails 应用,但是它还做不了任何事。我将用 scaffold_resource
生成器为一个简单的 REST 式 Web 服务生成代码。希望我的笔记(note)包含一个时间戳
(timestamp)和一段文本(text)
                     ,所以运行如下命令:
   $ ruby   script/generate scaffold_resource note date:date body:text
   create    app/views/notes
   create    app/views/notes/index.rhtml
   create    app/views/notes/show.rhtml
   create    app/views/notes/new.rhtml
   create    app/views/notes/edit.rhtml
   create    app/views/layouts/notes.rhtml
   create    public/stylesheets/scaffold.css
   create    app/models/note.rb
   create    app/controllers/notes_controller.rb
   create    test/functional/notes_controller_test.rb
   create    app/helpers/notes_helper.rb
   create    test/unit/note_test.rb
   create    test/fixtures/notes.yml
   create    db/migrate
   create    db/migrate/001_create_notes.rb
   route    map.resources :notes

Rails 已经为我的“笔记(note)
                   ”对象生成了 Web 服务代码的各个部分——模型(model)            、
视图(view)和控制器(controller)。在 db/migrate/001_create_notes.rb 里有一段
代码,它用于创建一个名为 notes 并具有以下字段的数据库表:一个唯一 ID、一个日期
(date)和一段文本(body)  。

app/models/note.rb 里的模型部分   (model)提供了一个用于访问数据库表的 ActiveResource
接口;  app/controllers/notes_controller.rb 里的控制器部分(controller)通过 HTTP
                                                           ,
把那个接口暴露给外界;app/views/notes 里的视图部分(view)定义了用户界面。



72 │ 第 3 章:REST 式服务有什么特别不同?
一个 REST 式 Web 服务构建好了——它虽然功能不强,但用作示范足够了。

在启动该服务之前,我需要先初始化数据库:
   $ rake db:migrate
   == CreateNotes: migrating ===========================================
   -- create_table(:notes)
   -> 0.0119s
   == CreateNotes: migrated (0.0142s) ==================================

现在,我可以启动笔记本应用,并开始使用我的服务了:
   $ script/server
   => Booting WEBrick...
   => Rails application started on http://0.0.0.0:3000
   => Ctrl-C to shutdown server; call with --help for options



一个 ActiveResource 客户端
An ActiveResource Client
我刚刚创建的应用作为一个示范,                 功能并不强大, 但它展示了一些印象深刻的特性。             首先,
它既是一个 Web 服务,            也是一个 Web 应用。我可以用 Web 浏览器访问 http://localhost:3000/
notes,然后通过 Web 接口来创建笔记。图 3-1 显示的是经过我的一系列操作之后,
http://localhost:3000/notes 所呈现出的视图。




图 3-1:包含一些笔记的笔记本 Web 应用

假如你曾经编写过 Rails 应用,或者看过 Rails 的 demo,那么你对这个例子应该比较熟悉。
不过在 Rails 1.2 里,生成的模型和控制器也可以作为一个 REST 式 Web 服务——你可以
用程序编写一个客户端,像 Web 浏览器那样简单地访问它。

可惜 ActiveResource 客户端本身没有随同 Rails 1.2 一起发布。在本书编写之时,它还在
Rails 的开发树(development tree)中被开发。要获取其代码,需要从 Subversion 版本控
制库里把代码 check out 出来:
   $ svn co http://dev.rubyonrails.org/svn/rails/trunk activeresource_client
   $ cd activeresource_client



                                        用 ActiveResource 创建透明的客户端 │ 73
现在,准备工作已经完毕,我要开始为我的笔记本 Web 服务编写 ActiveResource 客户端
了。示例 3-24 是一个客户端,它先创建一则笔记,然后修改它,接着列出已有笔记,最
后删除这则笔记。

示例 3-24:一个用于笔记本服务的 ActiveResource 客户端
   #!/usr/bin/ruby -w
   # activeresource-notebook-manipulation.rb

   require 'activesupport/lib/active_support'
   require 'activeresource/lib/active_resource'

   # 为网站暴露的对象定义一个模型
   class Note < ActiveResource::Base
     self.site = 'http://localhost:3000/'
   end

   def show_notes
     notes = Note.find :all                    # GET /notes.xml
     puts quot;I see #{notes.size} note(s):quot;
     notes.each do |note|
       puts quot; #{note.date}: #{note.body}quot;
     end
   end

   new_note = Note.new(:date => Time.now, :body => quot;A test notequot;)
   new_note.save                             # POST /notes.xml

   new_note.body = quot;This note has been modified.quot;
   new_note.save                             # PUT /notes/{id}.xml

   show_notes

   new_note.destroy                            # DELETE /notes/{id}.xml

   puts
   show_notes

示例 3-25 显示的是运行该程序产生的输出。

示例 3-25:activeresource-notebook-manipulation.rb 的一次运行
   I see 3 note(s):
     2006-06-05: What if I wrote a book about REST?
     2006-12-18: Pasta for lunch maybe?
     2006-12-18: This note has been modified.

   I see 2 note(s):
     2006-06-05: What if I wrote a book about REST?
     2006-12-18: Pasta for lunch maybe?

如果你熟悉 ActiveRecord(用于连接 Rails 和数据库的对象关系型数据映射组件(object-
relational mapper))的话,你会注意到 ActiveResource 的接口看上去跟 ActiveRecord 差不
多。它们均为各种暴露统一接口的对象提供了一个面向对象的接口。对于 ActiveRecord,


74 │ 第 3 章:REST 式服务有什么特别不同?
对象在数据库里,通过 SQL(采用 SELECT、INSERT、UPDATE 和 DELETE 等)来访问
这些对象;而对于 ActiveResource,对象在 Rails 应用里,通过 HTTP(采用 GET、POST、
PUT 和 DELETE)来访问这些对象。

示例 3-26 是从 Rails 服务器日志里摘录的一段与运行我的 ActiveResource 客户端相关的记
录。其中的 GET、POST、PUT 和 DELETE 请求分别与示例 3-24 里被注释的代码行对应。

示例 3-26:activeresource-notebook-manipulation.rb 发出的 HTTP 请求
   quot;POST /notes.xml HTTP/1.1quot; 201
   quot;PUT /notes/5.xml HTTP/1.1quot; 200
   quot;GET /notes.xml HTTP/1.1quot; 200
   quot;DELETE /notes/5.xml HTTP/1.1quot; 200
   quot;GET /notes.xml HTTP/1.1quot; 200

这些请求做了什么?跟发给 S3 的请求一样,通过 HTTP 统一接口进行资源访问。我的笔
记本服务暴露了两种资源(resource)
                    :

   笔记列表(/notes.xml),相当于 S3 里的桶(它是一个对象列表);

   一则笔记(/notes/{id}.xml),相当于 S3 里的对象。

跟 S3 资源一样,这些资源也暴露 GET、PUT 和 DELETE。笔记列表还支持用 POST 来创
建一则新笔记——这跟 S3 有点不同(在 S3 里,对象是用 PUT 方法创建的) 过这是符
                                          ,不
合 REST 风格的。

当客户端运行时,客户端与服务器之间的 XML 文档传递是透明的。这些 XML 文档是对
下层数据库记录的简单描绘,就像示例 3-27 或示例 3-28 那样。

示例 3-27:一个响应实体主体(对发给 /notes.xml 的 GET 请求的响应)
   <?xml version=quot;1.0quot; encoding=quot;UTF-8quot;?>
   <notes>
    <note>
     <body>What if I wrote a book about REST?</body>
     <date type=quot;datequot;>2006-06-05</date>
     <id type=quot;integerquot;>2</id>
    </note>
    <note>
     <body>Pasta for lunch maybe?</body>
     <date type=quot;datequot;>2006-12-18</date>
     <id type=quot;integerquot;>3</id>
    </note>
   </notes>

示例 3-28:一个请求实体主体(发给 /notes/5.xml 的 PUT 请求)
   <?xml version=quot;1.0quot; encoding=quot;UTF-8quot;?>
   <note>
     <body>This note has been modified.</body>
   </note>



                                        用 ActiveResource 创建透明的客户端 │ 75
访问 ActiveResource 服务的 Python 客户端
A Python Client for the Simple Service
目前,Ruby 的 ActiveResource 库是唯一的 ActiveResource 客户端库,Rails 是唯一能暴露
跟 ActiveResource 兼容的服务的框架。  不过它只是发送一些传递 XML 文档的 HTTP 请求、
并获取返回的 XML 文档而已,用其他语言编写的客户端应该也能发送那些 XML 文档,
用其他框架应该也能暴露同样的 URI。
在示例 3-29 中,我用 Python 实现了示例 3-24 所示的客户端程序。这个程序没有基于
ActiveResource,所以它要比示例 3-24 的 Ruby 实现长一些。在这个 Python 实现里,它必
须自己来构造 XML 文档和发送 HTTP 请求,       但是其结构跟示例 3-24 所示的客户端程序是
基本一样的。

示例 3-29:用 Python 实现一个 ActiveResource 服务的客户端
   #!/usr/bin/python
   # activeresource-notebook-manipulation.py

   from elementtree.ElementTree import Element, SubElement, tostring
   from elementtree import ElementTree
   import httplib2
   import time

   BASE = quot;http://localhost:3000/quot;
   client = httplib2.Http(quot;.cachequot;)

   def showNotes():
      headers, xml = client.request(BASE + quot;notes.xmlquot;)
      doc = ElementTree.fromstring(xml)
      for note in doc.findall('note'):
          print quot;%s: %squot; % (note.find('date').text, note.find('body').text)

   newNote = Element(quot;notequot;)
   date = SubElement(newNote, quot;datequot;)
   date.attrib['type'] = quot;datequot;
   date.text = time.strftime(quot;%Y-%m-%dquot;, time.localtime())
   body = SubElement(newNote, quot;bodyquot;)
   body.text = quot;A test notequot;

   headers, ignore = client.request(BASE + quot;notes.xmlquot;, quot;POSTquot;,
                               body= tostring(newNote),
                               headers={'content-type' : 'application/xml'})
   newURI = headers['location']

   modifiedBody = Element(quot;notequot;)
   body = SubElement(modifiedBody, quot;bodyquot;)
   body.text = quot;This note has been modifiedquot;

   client.request(newURI, quot;PUTquot;,
                body=tostring(modifiedBody),
                headers={'content-type' : 'application/xml'})

   showNotes()



76 │ 第 3 章:REST 式服务有什么特别不同?
client.request(newURI, quot;DELETEquot;)

   print
   showNotes()



最后的话
Parting Words
因为REST式Web服务具有简单和良定的(well-defined)接口,所以可以容易地做到克隆
一 个 REST 式 Web 服 务 , 或 者 为 一 个 REST 式 Web 服 务 替 换 实 现 。 Park Place ( http://
code.whytheluckystiff.net/parkplace)是一个具有跟S3 一样的接口的Ruby应用。   你可以用Park
Place来提供自己的S3 服务。S3 的库和客户端程序同样可用于访问Park Place的服务器,就
好比在访问https://s3.amazonaws.com/一样。

克隆 ActiveResource 也是可以的。虽然还没人这么做,但是为 Python 或其他动态语言编
写一个通用的 ActiveResource 客户端并不难。另一方面,为一个兼容 ActiveResource 的服
务编写一次性客户端,跟为其他 REST 式服务编写客户端同样简单。

现在,你应该对编写 REST 式或 REST-RPC 混合服务(无论它提供 XML、HTML、JSON,
还是某种混合)的客户端感到轻松了——它们就是 HTTP 请求和文档解析而已。

同时,   你对 REST 式 Web 服务 (如 S3 和 Yahoo!搜索服务) RPC 式和混合服务
                                          跟          (如 Flickr
和 del.icio.us APIs)的区别也应该有所了解了。这不是对服务内容的评判,而是对其架构
的判断。在对木材进行加工时,应顺应木材的纹理进行得当的加工。Web 也有“纹理”                    ,
而 REST 式 Web 服务正是一种吻合 Web 纹理的设计。

在接下来的章节中,我将教你如何创建像 S3(而不是像 del.icio.us API)一样的 REST 式
Web 服务。第 7 章之前,我都将围绕这一主题。在第 7 章,我们会把 del.icio.us 重新作为
REST 式 Web 服务来设计。




                                                                最后的话 │ 77

More Related Content

What's hot

P101 B04
P101 B04P101 B04
P101 B04jingger
 
資工所考試密技說明會
資工所考試密技說明會資工所考試密技說明會
資工所考試密技說明會Mu Chun Wang
 
入門啟示錄Ch05簡報
入門啟示錄Ch05簡報入門啟示錄Ch05簡報
入門啟示錄Ch05簡報Chiou WeiHao
 
Hr 017 社會新鮮人生涯規劃
Hr 017 社會新鮮人生涯規劃Hr 017 社會新鮮人生涯規劃
Hr 017 社會新鮮人生涯規劃handbook
 
Bloggers Survival 제안서 불로고수
Bloggers Survival 제안서 불로고수Bloggers Survival 제안서 불로고수
Bloggers Survival 제안서 불로고수JIAQI NIE
 
Cre 020 創意與創新行銷
Cre 020 創意與創新行銷Cre 020 創意與創新行銷
Cre 020 創意與創新行銷handbook
 
DS-033-裕隆日產汽車知識管理之路
DS-033-裕隆日產汽車知識管理之路DS-033-裕隆日產汽車知識管理之路
DS-033-裕隆日產汽車知識管理之路handbook
 
Perlで圧縮
Perlで圧縮Perlで圧縮
Perlで圧縮Naoya Ito
 
Authoring Tools Comparision in Detail
Authoring Tools Comparision in DetailAuthoring Tools Comparision in Detail
Authoring Tools Comparision in DetailTim Lu
 
中小企業E化最新趨勢與效益分析
中小企業E化最新趨勢與效益分析中小企業E化最新趨勢與效益分析
中小企業E化最新趨勢與效益分析Alex Lee
 
CRE-024-製商整合科技與產業創新
CRE-024-製商整合科技與產業創新CRE-024-製商整合科技與產業創新
CRE-024-製商整合科技與產業創新handbook
 
馬英九、蕭萬長原住民政策
馬英九、蕭萬長原住民政策馬英九、蕭萬長原住民政策
馬英九、蕭萬長原住民政策ma19
 
入門啟示錄Ch07簡報
入門啟示錄Ch07簡報入門啟示錄Ch07簡報
入門啟示錄Ch07簡報Chiou WeiHao
 
インフラエンジニアになろう!
インフラエンジニアになろう!インフラエンジニアになろう!
インフラエンジニアになろう!Toshiaki Baba
 
はてなブックマークのシステムについて
はてなブックマークのシステムについてはてなブックマークのシステムについて
はてなブックマークのシステムについてNaoya Ito
 
eComing Club簡介200802
eComing Club簡介200802eComing Club簡介200802
eComing Club簡介200802Robin Chen
 
DS-032-間歇運動機構
DS-032-間歇運動機構DS-032-間歇運動機構
DS-032-間歇運動機構handbook
 
【13-B-3】 企業システムをマッシュアップ型に変えるには
【13-B-3】 企業システムをマッシュアップ型に変えるには【13-B-3】 企業システムをマッシュアップ型に変えるには
【13-B-3】 企業システムをマッシュアップ型に変えるにはdevsumi2009
 

What's hot (20)

P101 B04
P101 B04P101 B04
P101 B04
 
資工所考試密技說明會
資工所考試密技說明會資工所考試密技說明會
資工所考試密技說明會
 
入門啟示錄Ch05簡報
入門啟示錄Ch05簡報入門啟示錄Ch05簡報
入門啟示錄Ch05簡報
 
Hr 017 社會新鮮人生涯規劃
Hr 017 社會新鮮人生涯規劃Hr 017 社會新鮮人生涯規劃
Hr 017 社會新鮮人生涯規劃
 
中国网络审查
中国网络审查中国网络审查
中国网络审查
 
Bloggers Survival 제안서 불로고수
Bloggers Survival 제안서 불로고수Bloggers Survival 제안서 불로고수
Bloggers Survival 제안서 불로고수
 
Cre 020 創意與創新行銷
Cre 020 創意與創新行銷Cre 020 創意與創新行銷
Cre 020 創意與創新行銷
 
DS-033-裕隆日產汽車知識管理之路
DS-033-裕隆日產汽車知識管理之路DS-033-裕隆日產汽車知識管理之路
DS-033-裕隆日產汽車知識管理之路
 
Perlで圧縮
Perlで圧縮Perlで圧縮
Perlで圧縮
 
Authoring Tools Comparision in Detail
Authoring Tools Comparision in DetailAuthoring Tools Comparision in Detail
Authoring Tools Comparision in Detail
 
中小企業E化最新趨勢與效益分析
中小企業E化最新趨勢與效益分析中小企業E化最新趨勢與效益分析
中小企業E化最新趨勢與效益分析
 
CRE-024-製商整合科技與產業創新
CRE-024-製商整合科技與產業創新CRE-024-製商整合科技與產業創新
CRE-024-製商整合科技與產業創新
 
馬英九、蕭萬長原住民政策
馬英九、蕭萬長原住民政策馬英九、蕭萬長原住民政策
馬英九、蕭萬長原住民政策
 
入門啟示錄Ch07簡報
入門啟示錄Ch07簡報入門啟示錄Ch07簡報
入門啟示錄Ch07簡報
 
インフラエンジニアになろう!
インフラエンジニアになろう!インフラエンジニアになろう!
インフラエンジニアになろう!
 
はてなブックマークのシステムについて
はてなブックマークのシステムについてはてなブックマークのシステムについて
はてなブックマークのシステムについて
 
eComing Club簡介200802
eComing Club簡介200802eComing Club簡介200802
eComing Club簡介200802
 
DS-032-間歇運動機構
DS-032-間歇運動機構DS-032-間歇運動機構
DS-032-間歇運動機構
 
【13-B-3】 企業システムをマッシュアップ型に変えるには
【13-B-3】 企業システムをマッシュアップ型に変えるには【13-B-3】 企業システムをマッシュアップ型に変えるには
【13-B-3】 企業システムをマッシュアップ型に変えるには
 
plan
planplan
plan
 

Viewers also liked

dynamics-of-wikipedia-1196670708664566-3
dynamics-of-wikipedia-1196670708664566-3dynamics-of-wikipedia-1196670708664566-3
dynamics-of-wikipedia-1196670708664566-351 lecture
 
Measuring the effectiveness of the promotional program
Measuring the effectiveness of the promotional programMeasuring the effectiveness of the promotional program
Measuring the effectiveness of the promotional programAgung Setiawan
 
Chapter 19 measuring the effectiveness of the promotional program
Chapter 19   measuring the effectiveness of the promotional programChapter 19   measuring the effectiveness of the promotional program
Chapter 19 measuring the effectiveness of the promotional programPujarini Ghosh
 
Chap19 Measuring The Effectiveness Of The Promotional Program
Chap19 Measuring The Effectiveness Of The Promotional ProgramChap19 Measuring The Effectiveness Of The Promotional Program
Chap19 Measuring The Effectiveness Of The Promotional ProgramPhoenix media & event
 

Viewers also liked (6)

Aô @go
Aô @goAô @go
Aô @go
 
dynamics-of-wikipedia-1196670708664566-3
dynamics-of-wikipedia-1196670708664566-3dynamics-of-wikipedia-1196670708664566-3
dynamics-of-wikipedia-1196670708664566-3
 
Measuring the effectiveness of the promotional program
Measuring the effectiveness of the promotional programMeasuring the effectiveness of the promotional program
Measuring the effectiveness of the promotional program
 
Chapter 19 measuring the effectiveness of the promotional program
Chapter 19   measuring the effectiveness of the promotional programChapter 19   measuring the effectiveness of the promotional program
Chapter 19 measuring the effectiveness of the promotional program
 
Chap19 Measuring The Effectiveness Of The Promotional Program
Chap19 Measuring The Effectiveness Of The Promotional ProgramChap19 Measuring The Effectiveness Of The Promotional Program
Chap19 Measuring The Effectiveness Of The Promotional Program
 
Funny Photos
Funny PhotosFunny Photos
Funny Photos
 

More from 51 lecture

1244600439API2 upload
1244600439API2 upload1244600439API2 upload
1244600439API2 upload51 lecture
 
1242982374API2 upload
1242982374API2 upload1242982374API2 upload
1242982374API2 upload51 lecture
 
1242626441API2 upload
1242626441API2 upload1242626441API2 upload
1242626441API2 upload51 lecture
 
1242625986my upload
1242625986my upload1242625986my upload
1242625986my upload51 lecture
 
1242361147my upload ${file.name}
1242361147my upload ${file.name}1242361147my upload ${file.name}
1242361147my upload ${file.name}51 lecture
 
this is ruby test
this is ruby testthis is ruby test
this is ruby test51 lecture
 
this is ruby test
this is ruby testthis is ruby test
this is ruby test51 lecture
 
this is ruby test
this is ruby testthis is ruby test
this is ruby test51 lecture
 
this is ruby test
this is ruby testthis is ruby test
this is ruby test51 lecture
 
this is ruby test
this is ruby testthis is ruby test
this is ruby test51 lecture
 
this is test api2
this is test api2this is test api2
this is test api251 lecture
 
My cool new Slideshow!
My cool new Slideshow!My cool new Slideshow!
My cool new Slideshow!51 lecture
 
Stress Management
Stress Management Stress Management
Stress Management 51 lecture
 
Iim A Managment
Iim A ManagmentIim A Managment
Iim A Managment51 lecture
 
Time Management
Time ManagementTime Management
Time Management51 lecture
 
Conversation By Design
Conversation By DesignConversation By Design
Conversation By Design51 lecture
 
Tech_Implementation of Complex ITIM Workflows
Tech_Implementation of Complex ITIM WorkflowsTech_Implementation of Complex ITIM Workflows
Tech_Implementation of Complex ITIM Workflows51 lecture
 
javascript reference
javascript referencejavascript reference
javascript reference51 lecture
 

More from 51 lecture (20)

1244600439API2 upload
1244600439API2 upload1244600439API2 upload
1244600439API2 upload
 
1242982374API2 upload
1242982374API2 upload1242982374API2 upload
1242982374API2 upload
 
1242626441API2 upload
1242626441API2 upload1242626441API2 upload
1242626441API2 upload
 
1242625986my upload
1242625986my upload1242625986my upload
1242625986my upload
 
1242361147my upload ${file.name}
1242361147my upload ${file.name}1242361147my upload ${file.name}
1242361147my upload ${file.name}
 
this is ruby test
this is ruby testthis is ruby test
this is ruby test
 
this is ruby test
this is ruby testthis is ruby test
this is ruby test
 
this is ruby test
this is ruby testthis is ruby test
this is ruby test
 
this is ruby test
this is ruby testthis is ruby test
this is ruby test
 
this is ruby test
this is ruby testthis is ruby test
this is ruby test
 
this is test api2
this is test api2this is test api2
this is test api2
 
My cool new Slideshow!
My cool new Slideshow!My cool new Slideshow!
My cool new Slideshow!
 
Stress Management
Stress Management Stress Management
Stress Management
 
Iim A Managment
Iim A ManagmentIim A Managment
Iim A Managment
 
Time Management
Time ManagementTime Management
Time Management
 
Conversation By Design
Conversation By DesignConversation By Design
Conversation By Design
 
Web 2.0
Web 2.0Web 2.0
Web 2.0
 
Tech_Implementation of Complex ITIM Workflows
Tech_Implementation of Complex ITIM WorkflowsTech_Implementation of Complex ITIM Workflows
Tech_Implementation of Complex ITIM Workflows
 
javascript reference
javascript referencejavascript reference
javascript reference
 
Esb
EsbEsb
Esb
 

1242982622API2 upload

  • 1. 免费试读章节 (非印刷免费在线版) 如果你喜欢本书,请去 China-pub、第二书店、卓越网、当当网 购买印刷版以支持作者和InfoQ中文站 向博文视点出版公司以及译者徐涵等致谢 本图书节选由InfoQ中文站免费发放,如果你从其它渠道获取此摘选, 请注册InfoQ中文站以支持作者和出版商 本摘选主页为 http://infoq.com/cn/articles/restlet-for-restful-services 《RESTful Web Services 中文版》官方网站为 http://restfulwebservices.cn/
  • 2. 第3章 REST 式服务有什么特别不同? What Makes RESTful Services Different? 前面的例子只是为了引起你的兴趣,它们不全是 REST 式架构的,现在该看看正确的做法 了。尽管本书是关于 REST 式 Web 服务的, 但我前面向你展示的大部分服务都是 REST-RPC 混合服务(比如 del.icio.us API)——这些服务的工作方式跟 Web 上的其他应用不太一样。 这是因为目前跟 Web 理念保持一致的、知名的 REST 式服务还不太多。在前两章,目的 是列举一些你也许知道的真实服务,所以我只有选择那些例子。 del.icio.us API 和 Flickr API 都是典型的 REST-RPC 混合服务的例子:它们在获取数据时工 作方式跟 Web 一样,但在修改数据时就变成 RPC 式服务了。Yahoo!提供的各种搜索服务 都是纯 REST 式的,但它们太过简单,不是很好的例子。Amazon 的电子商务服务(见示 例 1-2)也太过简单,而且在一些重要的细节上呈现出 RPC 风格。 这些服务都是有用的服务。虽然认为 RPC 式 Web 服务不可取,但假如某个 RPC 式 Web 服务有用,我还是会编写 RPC 式客户端访问它的。不过,我仍不能在这里拿 Flickr API 或 del.icio.us API 作为如何设计 REST 式 Web 服务的示例,所以在上一章向你介绍了它们, 因为上一章的目的就是介绍 programmable web 上现有的一些服务、以及如何编写 HTTP 客户端。由于后面就要步入设计方面的章节了,所以我需要向你展示一下 REST 式和面向 资源的服务是什么样的。 介绍 Simple Storage Service Introducing the Simple Storage Service 有两个流行的 Web 服务能满足这一目的: Atom 发布协议(Atom Publishing Protocol,APP) 和 Amazon S3(Simple Storage Service,简单存储服务)(附录 A 给出了一些现有的、公 。 开的 REST 式 Web 服务,那里有很多估计你都没听说过。 )由于 APP 只是一组关于构建服 务的指导,还称不上是实际的服务,所以我选择 S3 作为本章的示例,毕竟 S3 是 Web 上 49
  • 3. 实际存在的。我会在第 9 章讨论 APP、Atom 及相关话题(例如 Google 的 GData 等)。本 章其余部分将主要探讨 S3。 你可以通过 S3 存储任何结构化的数据。 你的数据可以是私密的,也可以是能被任何人 (通 过 Web 浏览器或 BitTorrent 客户端)访问的。Amazon 为你提供存储空间和带宽,而你为 所占用的存储空间和产生的流量按千兆字节(GB)付费。要运行本章的 S3 示例代码,你 需要先到 http://aws.amazon.com/s3 注册一下。S3 的技术文档可以从这里获得:http://docs. amazonwebservices.com/AmazonS3/2006-03-01/。 S3 主要被用作两种用途。 备份服务器 你通过 S3 来保存自己的数据,他人访问不了你的数据。你不是自己购买备份盘,而 是租用 Amazon 的磁盘空间。 数据寄存 你把数据保存在 S3 上,并允许他人访问这些数据。Amazon 通过 HTTP 或 BitTorrent 提供这些数据。你的流量费不是向 ISP 支付,而是向 Amazon 支付。根据你的流量情 况,也许这样可以节省不少流量费。现在有不少创业公司都用 S3 来供应数据文件。 跟前面展示过的那些服务不同的是,S3 没有与之相应的网站。del.icio.us API 是基于 del.icio.us 网站的,Yahoo!搜索服务也是基于相应网站的,但是你在 amazon.com 上却找不 到给 S3 上传文件的 HTML 表单(form) 。S3 是只供程序使用的。(当然,如果你把 S3 用 作数据寄存目的,人们将能通过 Web 浏览器来使用它,但他们并不知道自己访问的是一 个 Web 服务。在他们看来,他们访问的是一个普通的网站。 ) Amazon 提供了 Ruby、Python、Java、C#和 Perl 语言的示例库(参见 http://developer. amazonwebservices.com/connect/kbcategory.jspa?categoryID=47)。除了 Amazon 的官方库, 还有一些第三方库,比如用于 Ruby 的 AWS::S3(http://amazon.rubyforge.org/)——s3sh 命令行解释器(见示例 1-4)就出自这个库。 S3 的面向对象设计 Object-Oriented Design of S3 S3 基于两个概念:S3“桶(bucket) ”和 S3“对象(object)。一个对象(object)就是一 ” 则附有元数据的具名的(named)数据。一个桶(bucket)就是一个用于容纳对象(object) 的具名的(named)容器。桶就好比硬盘上的文件系统,对象就好比该文件系统里的一个 文件。把桶(bucket)比喻为文件系统里的目录(directory)是一个误区,因为文件系统 里的目录可以嵌套,而桶不行。假如你希望桶具有层次结构,你只有通过给对象 “directory/subdirectory/file-object”式的命名来模拟。 50 │ 第 3 章:REST 式服务有什么特别不同?
  • 4. 关于桶 A Faw Words About Buckets 桶(bucket)有一则与之关联的信息:即名称(name) 。桶名(bucket name)可以包含以 下字符:英文大小写字母(a-z,A-Z) 、阿拉伯数字(0-9)“_”、 (下划线)“.” 、 (句点) 及“-”(短横线) 。我建议不要在桶名中使用大写字母。正如前面所说的,一个桶不能包 含另一个桶 (即桶是不能嵌套的) 桶只能包含对象。 , 每个 S3 用户只能创建最多 100 个桶, 而且你的桶不能跟其他用户的桶重名。我建议你要么把所有对象都放在一个桶里,要么用 你自己的项目名称(projects names)或域名(domain names)来给桶命名。 关于对象 A Few Words About Objects 对象有四个相关部分: 对象所在桶的引用。 对象里的数据(S3 术语为“值(value)”)。 对象的名称(S3 术语为“键(key)”)。 与对象关联的一组元数据键-值对(metadata key-value pairs)。它们主要是自定义的 元数据,不过也可以包含 ContentType 和 Content-Disposition 等标准 HTTP 报头 的值。 如果我想把 O’Reilly 网站寄存在 S3 上的话,我会创建一个名为“oreilly.com”的桶,然后 在其中放入一些键(key)分别为“” (空串)“catalog”“catalog/9780596529260”等的 、 、 对象。这些对象分别对应于 http://oreilly.com/、http://oreilly.com/catalog 等 URI。这些对象 的值将是 O’Reilly 网站的网页内容。这些 S3 对象的 Content-Type 元数据值将被设为 text/html——这样, 当这些对象被人们浏览时, 会被作为 HTML 文档 (而不是作为 XML 或纯文本)来处理。 假如 S3 是一个独立的面向对象库 What If S3 Was a Standalone Library? 假如把 S3 实现为一个面向对象代码库(而不是 Web 服务)的话,那么将会有 S3Bucket 和 S3Object 这两个类,它们各自均有用于数据成员读写的方法(如 S3Bucket#name、 S3Object.value=及 S3Bucket#addObject 等)S3Bucket 类将会有一个名为 S3Bucket# 。 getObjects 的实例方法(返回 S3Object 实例的列表)和名为 S3Bucket.getBuckets 的 类方法(返回所有桶) 。示例 3-1 是这个类对应的 Ruby 代码。 示例 3-1:把 S3 实现为 Ruby 库 class S3Bucket # 这个类方法用于获取所有桶 def self.getBuckets end # 这个实例方法用于获取桶里的对象 S3 的面向对象设计 │ 51
  • 5. def getObjects end ... end class S3Object # 获取与对象关联的数据 def data end # 设置与对象关联的数据 def data=(new_value) end ... end 资源 Resources Amazon S3 提供两种 Web 服务:基于普通 HTTP 信封的 REST 式服务(RESTful service) 和基于 SOAP 信封的 RPC 式服务(RPC-style service) 。RPC 式 S3 服务暴露的功能(如 ListAllMyBuckets、CreateBucket 等)跟示例 3-1 里列出的方法比较相像。实际上,许 多 RPC 式 Web 服务都是由它们内部的实现方法(implementation methods)自动生成的, 它们暴露的服务接口跟它们在内部调用的编程语言接口是一样的。 因为大多数现代编程语 言(包括面向对象的)都是过程式的(procedural) ,所以可以这么做。 REST 式 S3 服务跟 RPC 式 S3 服务的功能一样,只不过它暴露的不是自己命名的函数, 而是暴露标准的 HTTP 对象(称为资源) 。资源(resource)响应的不是像 getObjects 这 样自己命名的方法,而是响应 GET、HEAD、POST、PUT、DELETE 和 OPTIONS 这些标 准的 HTTP 方法。 REST 式 S3 服务提供三种资源,它们(及相应的 URI)分别是: 桶列表(https://s3.amazonaws.com/),这种类型的资源只有一个; 一个特定的桶(https://s3.amazonaws.com/{name-of-bucket}/),这种类型的 资源最多可以有 100 个; 某个桶里的一个特定的 S3 对象 https://s3.amazonaws.com/{name-of-bucket}/ ( {name-of-object}),这种类型的资源数量不限。 前面那个假想的面向对象 S3 库里的每个方法,都可以转化为上述三种资源与六种标准方 法的某种组合。例如,读方法 S3Object#name 对应于对“S3 对象”资源做 GET 请求; 写方法 S3Object#value=对应于对 “S3 对象” 资源做 PUT 请求。工厂方法(factory method) 52 │ 第 3 章:REST 式服务有什么特别不同?
  • 6. (如 S3Bucket.getBuckets)和关系方法(relational method)如 S3Bucket#getObjects) ( , 分别对应于对“桶列表”资源和“桶”资源做 GET 请求。 每个资源都暴露同样的接口,并以同样的方式工作。如果要获取一个对象的值(value) , 就向该对象的 URI 发送 GET 请求;如果只要获取一个对象的元数据(metadata) ,就向该 对象的 URI 发送 HEAD 请求;如果要创建一个桶,就自己构造一个含有桶名(即你为将 创建的桶起的名称)的 URI,然后向该 URI 发送 PUT 请求;如果要往一个桶里添加对象, 就向含有桶名(即新建对象所在桶的名称)和对象名(即你为将创建的对象起的名称)的 URI 发送 PUT 请求;如果要删除一个桶或对象,就向其 URI 发送 DELETE 请求。 这些并非 S3 设计者的发明,实际上,根据 HTTP 标准,这些正是 GET、HEAD、PUT 和 DELETE 本来的用途。这四个方法(连同 S3 没有用到的 POST 和 OPTIONS 方法)足以 描述所有与 Web 资源的交互。要把程序作为 Web 服务发布的话,不需要发明新词汇,或 者在 URI 里给出自己的方法名称,你唯一须要做的就是仔细考虑资源的设计。所有 REST 式 Web 服务(无论多复杂)都支持同样一组基本操作,它们的复杂性都在资源上。 表 3-1 显示了当你向一个 S3 资源的 URI 发送 HTTP 请求时将发生什么。 表 3-1:S3 资源及其方法 GET HEAD PUT DELETE 桶列表(/) 列出所有桶 - - - 一个桶(/{bucket}) 列出桶里的对象 - 创建桶 删除桶 一个对象 获取对象的值及 获取对象 设置对象的 删除对象 (/{bucket}/{object}) 元数据 的元数据 值及元数据 这个表格看起来有点奇怪。那我为什么还要让它占用这里的宝贵篇幅呢?因为这些方法的 作用都名副其实。在一个经良好设计的 REST 式服务里,每个方法的作用都名副其实。 仅凭目前的论述,可能你还难以信服。S3 是一个比较通用的服务。假如你所要实现的只 是在具名槽(named slots)里存放数据,那么你只要用 GET 和 PUT 就行了(分别用于读 和写) 。在第 5 章和第 6 章,我会告诉你如何把任何动作映射到统一接口上。为了让你相 信这一点,请注意,其实只要定义一个只响应 GET 方法的“桶列表”资源,就可以代替 S3Bucket.getBuckets 方法了;同样地,在这样的资源设计下,S3Bucket#addObject (它要求每个对象与一些桶对应)也不需要了。 我们来跟 S3 的 RPC 式 SOAP 接口作个比较。如果采用 SOAP 接口的话,获取桶列表,要 用 ListAllMyBuckets 方法;获取桶里的内容,要用 ListBucket 方法。而采用 REST 式 资源 │ 53
  • 7. 接口的话,这些操作全部用 GET 方法。在 REST 式服务里,对象(面向对象意义上的) 由 URI 来标识,而方法名都是标准的。这些标准的方法,在不同的资源与服务上具有相 同的工作方式。 HTTP 响应代码 HTTP Response Codes 利用 HTTP 响应代码是 REST 式架构的另一个标志特征。如果发给 S3 一个请求,而且 S3 成功处理了这个请求,那么你得到的 HTTP 响应代码将是 200( “OK”。你的浏览器在成 ) 功获取网页时,得到的也是这个响应代码。如果出错的话,响应代码将在 3xx、4xx、5xx 的范围内,比如 500(“Internal Server Error”。错误的响应代码告诉客户端:请勿把本次 ) 响应的元数据与实体主体当成对请求的响应。这个响应并不是客户端所要的,而是服务器 试图告诉客户端“出错了” 。由于响应代码不是放在元数据或实体主体里的,所以客户端 只要看 HTTP 响应的前三个字节就能知道有没有出错。 示例 3-2 是一个错误的响应的例子。我向一个不存在的对象(https://s3.amazonaws. com/crummy.com/nonexistent/object)发送了 HTTP 请求,得到的响应代码是 404“Not ( Found”。 ) 示例 3-2:一个 S3 的错误响应的例子 404 Not Found Content-Type: application/xml Date: Fri, 10 Nov 2006 20:04:45 GMT Server: AmazonS3 Transfer-Encoding: chunked X-amz-id-2: /sBIPQxHJCsyRXJwGWNzxuL5P+K96/Wvx4FhvVACbjRfNbhbDyBH5RC511sIz0w0 X-amz-request-id: ED2168503ABB7BF4 <?xml version=quot;1.0quot; encoding=quot;UTF-8quot;?> <Error> <Code>NoSuchKey</Code> <Message>The specified key does not exist.</Message> <Key>nonexistent/object</Key> <RequestId>ED2168503ABB7BF4</RequestId> <HostId>/sBIPQxHJCsyRXJwGWNzxuL5P+K96/Wvx4FhvVACbjRfNbhbDyBH5RC511sIz0w0</HostId> </Error> HTTP 响应代码在 human web 上未得到充分利用。当你请求网页时,你的浏览器不会把 HTTP 响应代码向你显示出来,因为既然人们通过更友好的方式得知有没有出错,谁还愿 意去看无聊的数字代码呢?大多数 Web 应用在出错时都会返回 200( “OK”,以及一个人 ) 类可读的错误描述文档。人们一般不会把错误描述文档误认为是他们请求的文档。 在 programmable web 上,情况刚好相反。计算机程序善于根据数字变量值的不同而采取 不同处理,但对于揣摩文档是什么“含义”则很不擅长。如果没有事先制定好规则,程序 54 │ 第 3 章:REST 式服务有什么特别不同?
  • 8. 将无法判断一个 XML 文档里包含的是数据还是错误描述。HTTP 响应代码正好能够充当 这个规则,它给出了关于客户端应当如何处理 HTTP 响应的一个大致的规则。因为响应代 码不是放在元数据或实体主体里的,所以即便客户端不知如何读取它们,也能了解发生了 什么情况。 除 200(“OK” )和 404( Not Found” “ )以外,S3 还使用了许多其他响应代码。最常见的 可能是 403( “Forbidden” )了,它表示客户端发出的请求未包含正确的证书。S3 还使用其 他一些响应代码,如 400( “Bad Request”(表明服务器无法理解来自客户端的数据)和 ) 409(“Conflict”(表明客户端要删除的桶是非空的) ) 。完整的列表请参阅 S3 技术文档的 “The REST Error Response”部分。我将在附录 B 对每个 HTTP 响应代码作逐一讲解(主 要关注它们在 Web 服务上的应用) 。官方的 HTTP 响应代码有 41 个,不过在日常使用中, 最重要的只有 10 个左右。 一个 S3 客户端 An S3 Client 因为已经有 Amazon 的示例库和一些第三方库(如 AWS::S3 等)了,所以一般我们是不需 要自己定制 S3 客户端库的。 但我介绍 S3 的目的,并不是告诉你存在这样一个多么有用的 Web 服务,而是想通过 S3 来举例说明 REST 背后的理论。所以,我将用 Ruby 来写一个 自己的 S3 客户端,并在编写过程中来逐步剖析它。 为了展示这是可行的, 我的库将在 S3 服务之上实现 (implement)一个面向对象的接口(就 像示例 3-1 中的那样) 。最终结果将类似于 ActiveRecord 或其他对象关系型数据映射组件 (object-relational mapper,ORM),不过在内部,它并不是发出 SQL 请求来往数据库里存 储数据,而是发出 HTTP 请求来通过 S3 服务存储数据。我在给我的方法起名时,将尽量 采用能够反映下层 REST 式接口的名称(比如 get、put 等) ,而不是给出针对资源的名称 (如 getBuckets 和 getObjects 等) 。 我首先需要一个接口来处理 Amazon 特有的 Web 服务认证机制。 不过这并不如直接对 Web 服务进行实际考察更有意思,所以我准备暂时掠过这部分。我将先创建一个非常简单的 Ruby 模块 S3::Authorized,这样其他 S3 类就可以包含(include)它了。最后我会回到 这里,并补上有关细节。 示例 3-3 显示了一段初始代码。 示例 3-3:用 Ruby 编写 S3 客户端:初始代码 #!/usr/bin/ruby -w # S3lib.rb # 发送 HTTP 请求和解析响应所需的库 require 'rubygems' require 'rest-open-uri' 一个 S3 客户端 │ 55
  • 9. require 'rexml/document' # 对请求进行签名所需的库 require 'openssl' require 'digest/sha1' require 'base64' require 'uri' module S3 # 一个容纳所有代码的模块的开头。 module Authorized # 输入你的公共标识符(Amazon 称之为“Access Key ID” )和 # 你的密钥(Amazon 称之为“Secret Access Key”。 ) # 这样,你对你发出的 S3 请求进行签名后,Amazon 就知道该向谁收费了。 @@access_key_id = '' @@secret_access_key = '' if @@public_key.empty? or @@private_key.empty? raise quot;You need to set your S3 keys.quot; end # 你不应修改这里,除非你想使用 S3 的克隆(比如 Park Place)。 HOST = 'https://s3.amazonaws.com/' end 这段骨架代码里,唯一值得关注的是:应在哪里填写“跟你的 Amazon Web 服务账户相关 联”的两个密钥。发出的每个 S3 请求都包含你的公共标识符(public identifier,Amazon 称之为“Access Key ID”,这样 Amazon 就可以识别出请求来自于你。发出的每个请求都 ) 必须用你的密钥(Amazon 称之为“Secret Access Key”)进行签名,这样 Amazon 就知道 请求的确来自于你。该密钥只有你和 Amazon 知道,不应把它告诉任何其他人。如果让别 人知道了,那么他就可以用你的密钥发 S3 请求,令 Amazon 向你收费。 桶列表 The Bucket List 示例 3-4 是我为桶列表实现的面向对象类,它是我实现的第一个资源。我称这个类为 S3:: BucketList。 示例 3-4:用 Ruby 编写 S3 客户端:S3::BucketList 类 # 桶列表 class BucketList include Authorized # 获取该用户的所有桶 def get buckets = [] 56 │ 第 3 章:REST 式服务有什么特别不同?
  • 10. # 向桶列表的 URI 发送 GET 请求,并读取返回的 XML 文档。 doc = REXML::Document.new(open(HOST).read) # 对于每个桶... REXML::XPath.each(doc, quot;//Bucket/Namequot;) do |e| # ...创建一个 Bucket 对象,并把它添加到列表中。 buckets << Bucket.new(e.text) if e.text end return buckets end end XPath 讲解 以从右往左的顺序来读//Bucket/Name 这个 XPath 表达式,它的意思是: 寻找所有 Name 标签 Name 哪里的 Name 标签?直接在 Bucket 标签下的 Bucket/ 哪里的 Bucket 标签?任何地方的 // 现在 ,它是一个真正的 Web 服务客户端了。调 用 S3::BucketList#get ,就等于向 https://s3.amazonaws.com/(“桶列表”资源的 URI)发送一个加密的 HTTP GET 请求。S3 服 务 会 返 回 一 个 像 示 例 3-5 那 样 的 XML 文 档 —— “ 桶 列 表 ” 资 源 的 一 个 表 示 (representation) (我将在下一章介绍“表示”这个概念) 。该 XML 文档给出了关于“桶 列表” 资源的当前状态的一些信息:Owner 标签表明这个桶列表的所有者是谁 (我的 AWS 账户名是“leonardr28”;Buckets 标签里包含一些 Bucket 标签,这些 Bucket 标签是对 ) 我的桶的描述(在本例中,因为只有一个桶,所以这里出现一个 Bucket 标签) 。 示例 3-5:一个“桶列表”的例子 <?xml version='1.0' encoding='UTF-8'?> <ListAllMyBucketsResult xmlns='http://s3.amazonaws.com/doc/2006-03-01/'> <Owner> <ID>c0363f7260f2f5fcf38d48039f4fb5cab21b060577817310be5170e7774aad70</ID> <DisplayName>leonardr28</DisplayName> </Owner> <Buckets> <Bucket> <Name>crummy.com</Name> <CreationDate>2006-10-26T18:46:45.000Z</CreationDate> </Bucket> </Buckets> </ListAllMyBucketsResult> 在这个简单的客户端应用里,我只关心桶的名称。XPath 表达式//Bucket/Name 将给出每 个桶的桶名,我在创建 Bucket 对象时需要这个信息。 一个 S3 客户端 │ 57
  • 11. 正如我们所看到的,这个 XML 文档缺少的一样东西,即链接(link) 。该 XML 文档给出 了每个桶的名称,但是没有给出这些桶的 URIs。从 REST 设计角度来看,这是 Amazon S3 的主要不足。好在根据一个桶的桶名(bucket name)得出 URI 并不难,参照我前面给出 的规则就行了:htps://s3.amazonaws.com/{name-of-bucket}/。 桶 The Bucket 现在,我们来编写 S3::Bucket 类(见示例 3-6)。这样,S3::BucketList.get 就能实例 化一些桶了。 示例 3-6:用 Ruby 编写 S3 客户端:S3::Bucket 类 # 一个 S3 桶 class Bucket include Authorized attr_accessor :name def initialize(name) @name = name end # 桶的 URI 等于服务的根 URI 加上桶名。 def uri HOST + URI.escape(name) end # 在 S3 上保存这个桶。 # 类似于在数据库里保存对象的 ActiveRecord::Base#save。 # 关于 acl_policy,请看下面正文中的讨论。 def put(acl_policy=nil) # 设置 HTTP 方法,作为 open()的参数。 # 同时为该桶设置 S3 访问策略(如果有提供的话) args = {:method => :put} args[quot;x-amz-aclquot;] = acl_policy if acl_policy # 向该桶的 URI 发送 PUT 请求 open(uri, args) return self end # 删除该桶。 # 如果桶不为空的话,该删除操作将失败, # 并返回 HTTP 响应代码 409( “Conflict”。 ) def delete # 向该桶的 URI 发送 DELETE 请求 open(uri, :method => :delete) end 这里又实现了两个 Web 服务方法:S3::Bucket#put 和 S3::Bucket#delete。因为一个 桶的 URI 唯一标识了该桶,所以删除操作很简单:向该桶的 URI 发送一个 DELETE 请求 58 │ 第 3 章:REST 式服务有什么特别不同?
  • 12. 即可。因为桶的 URI 里包含了桶的名称,而且一个桶没有其他可设置的属性,所以创建 一个桶也很简单:向该 URI 发送一个 PUT 请求就行了。正如我将在编写 S3::Object 代 码时向你展示的,假如不是所有数据都放在 URL 里,PUT 请求的实现将会比较复杂。 虽然我刚才把 S3:: 类比作 ActiveRecord 类,不过 S3::Bucket#put 跟 ActiveRecord 实 现的 save 还是有点区别的。在一个由 ActiveRecord 控制的数据库表里,每行记录都有一 个唯一的数字 ID。如果你要修改一个 ID 为 23 的 ActiveRecord 对象的名称,那么你的修 改将体现为对一条 ID 为 23 的数据库记录的修改: SET name=quot;newnamequot; WHERE id=23 对于一个 S3 桶来说,它的 URI 就是它的永久 ID,桶的名称包含在 URI 里。假如你在调 用 put 时把桶的名称(name)换掉,那么客户端将做的,并不是在 S3 里修改桶的名称, 而是为新的 URI(含有你设置的新桶名)创建一个新的空桶。这是由 S3 程序员的设计造 成的。其实完全可以避免这样。Ruby on Rails 框架采用了与此不同的设计:当它通过一个 REST 式 Web 服务来暴露数据库记录时,每条记录的 URI 都含有该记录的数字 ID。假设 S3 是一个 Rails 服务的话,桶的 URI 将像这样/buckets/23——这样,修改桶名称时,就 不会改变其 URI 了。 现在来看 S3::Bucket 的最后一个方法 get。 S3::BucketList.get 一样, 跟 这个方法(见 示例 3-7)向一个资源(这里是一个“桶” )的 URI 发送 GET 请求,以获取一个 XML 文 档,然后根据该 XML 文档生成 S3::Object 类实例。这个方法支持以各种方式来过滤 S3 桶的内容,比方说,假如你只要获取那些“键(key)以某字符串开头”的对象,那么可 以采用:Prefix,等等。我就不对这些过滤选项作详细介绍了,如果你感兴趣,可以参阅 S3 技术文档的“Listing Keys”部分。 示例 3-7:用 Ruby 编写 S3 客户端:S3::Bucket 类(结束) # 获取桶里的(全部或部分)对象。 # # 如果 S3 决定不返回整个桶(或子集) ,那么 # 第二个返回值将被设为 true。要获取其余对象, # 你需要调整子集选项(subset option) (本书未作介绍) 。 # # 子集选项包括::Prefix、:Marker、:Delimiter、:MaxKeys。 # 有关详情,请参阅 S3 技术文档的“Listing Keys”部分。 def get(options={}) # 获取该桶的基准 URI(base URI),并把子集选项(假如有的话) # 附加到查询字符串(query string)上。 uri = uri() suffix = '?' # 对于用户提供的每个选项... options.each do |param, value| # ...如果属于某个 S3 子集选项... if [:Prefix, :Marker, :Delimiter, :MaxKeys].member? :param # ...把它附加到 URI 上 uri << suffix << param.to_s << '=' << URI.escape(value) 一个 S3 客户端 │ 59
  • 13. suffix = '&' end end # 现在我们已经构造好了 URI。向该 URI 发送 GET 请求, # 并读取含有 S3 对象信息的 XML 文档。 doc = REXML::Document.new(open(uri).read) there_are_more = REXML::XPath.first(doc, quot;//IsTruncatedquot;).text == quot;truequot; # 构建一个 S3::Object 对象的列表 objects = [] # 对于桶里的每个 S3 对象... REXML::XPath.each(doc, quot;//Contents/Keyquot;) do |e| # ...构造一个 S3::Object 对象,并把它添加到列表中。 objects << Object.new(self, e.text) if e.text end return objects, there_are_more end end XPath 讲解 以从右往左的顺序来读//IsTruncated 这个 XPath 表达式,它的意思是: 寻找所有 IsTruncated 标签 IsTruncated 哪里的 IsTruncated 标签?任何地方的 // 向应用的根 URI 发送 GET 请求,你可以得到“桶列表(bucket list) ”这个资源(resource) 的一个表示(representation)。向一个“桶”资源的 URI 发送 GET 请求,你可以得到该“桶 (bucket)”的一个表示,即一个像示例 3-8 那样的 XML 文档,其中的 Contents 元素包 含该桶的一些信息。 示例 3-8:一个“桶”的表示 <?xml version='1.0' encoding='UTF-8'?> <ListBucketResult xmlns=quot;http://s3.amazonaws.com/doc/2006-03-01/quot;> <Name>crummy.com</Name> <Prefix></Prefix> <Marker></Marker> <MaxKeys>1000</MaxKeys> <IsTruncated>false</IsTruncated> <Contents> <Key>mydocument</Key> <LastModified>2006-10-27T16:01:19.000Z</LastModified> <ETag>quot;93bede57fd3818f93eedce0def329cc7quot;</ETag> <Size>22</Size> <Owner> <ID> c0363f7260f2f5fcf38d48039f4fb5cab21b060577817310be5170e7774aad70</ID> <DisplayName>leonardr28</DisplayName> </Owner> 60 │ 第 3 章:REST 式服务有什么特别不同?
  • 14. <StorageClass>STANDARD</StorageClass> </Contents> </ListBucketResult> 在本例中, 该文档值得关注的部分是该桶的对象列表。 一个对象(object)是由它的键(key) 标识的,所以我用 XPath 表达式“//Contents/Key”来获取此信息。另外,有一个布尔值 ( /IsTruncated” “/ )也值得关注,它表示文档里是否包括了桶中所有对象的键(key) ,S3 有没有因为太多了放不下而截短了。 同样地,这个表示里主要缺少的就是链接(link) 。该文档给出了许多有关对象的信息,但 它没有给出对象的 URI。S3 假定客户端知道如何根据对象名称构造对象的 URI。好在构 造一个对象的 URI 并不难, 参照我前面给出的规则就行了: https://s3.amazonaws.com/ {name-of-bucket}/{name-of-object}。 S3 对象 The S3 Object 现在我们要为 S3 服务的核心——S3 对象——实现接口了。记住,一个 S3 对象(object) 只是一个具有名称(键)和一组元数据键-值对(如 Content-Type=quot;text/htmlquot;)的数 据串。当你向桶列表(bucket list)或桶(bucket)发送 GET 请求时,S3 会返回一个 XML 文档。当你向一个对象发送 GET 请求时, 会逐个字节地返回你先前向该对象 PUT 的数 S3 据串。 示例 3-9 给出了 S3::Object 类的开头部分,现在你对它应该已经不陌生了。 示例 3-9:用 Ruby 编写 S3 客户端:S3::Object 类 # 跟某个桶关联的一个具有值和元数据的 S3 对象。 class Object include Authorized # 客户端可以知道对象在哪个桶里 attr_reader :bucket # 客户端可以读写对象的名称 attr_accessor :name # 客户端可以写对象的元数据和值 # 稍后我将定义相应的“读”方法 attr_writer :metadata, :value def initialize(bucket, name, value=nil, metadata=nil) @bucket, @name, @value, @metadata = bucket, name, value, metadata end # 对象的 URI 等于所在桶的 URI 加上该对象的名称 def uri @bucket.uri + '/' + URI.escape(name) end 一个 S3 客户端 │ 61
  • 15. 下面是我实现的第一个 HTTP HEAD 请求。它用于获取一个 S3 对象的元数据键-值对 (metadata key-value pairs) 并填充元数据 hash store_metadata 的实现在本类的最后) , ( 。 由于我是用 rest-open-uri 发送请求的,所以发送 HEAD 请求的代码看上去跟发送其他 HTTP 请求的代码差不多(见示例 3-10) 。 示例 3-10:用 Ruby 编写 S3 客户端:S3::Object#metadata 方法 # 获取对象的元数据 hash def metadata # 如果没有元数据... unless @metadata # 向对象的 URI 发送一个 HEAD 请求,并从响应的 HTTP 报头里读取元数据。 begin store_metadata(open(uri, :method => :head).meta) rescue OpenURI::HTTPError => e if e.io.status == [quot;404quot;, quot;Not Foundquot;] # 假如没有元数据是因为对象不存在,这不算错误。 @metadata = {} else # 其他情况,作错误处理。 raise e end end end return @metadata end 这段代码的作用是获取一个对象的元数据,而不必获取该对象本身。这就跟下载一则电影 评论跟下载该电影本身的区别一样;而且,如果你需要为流量付费的话,就更能体会到二 者的差别了。元数据(metadata)与表示(representation)的区别并非只在 S3 中有,它是 所有面向资源的 Web 服务都有的。HEAD 方法令任何客户端可以获取任一资源的元数据, 而不必获取其表示(可能不只一个)的方法。 当然,有时你确实要下载电影本身,那么就需要 GET 请求了。在示例 3-11 中,我在存取 方法 (accessor method) S3::Object#value 里使用了 GET 请求。这个方法的结构跟 S3::Object#metadata 的差不多。 示例 3-11:用 Ruby 编写 S3 客户端:S3::Object#value 方法 # 获取对象的值和元数据 def value # 没有如果没有值... unless @value # 向对象的 URI 发送 GET 请求 response = open(uri) 62 │ 第 3 章:REST 式服务有什么特别不同?
  • 16. # 从响应的 HTTP 报头里读取元数据 store_metadata(response.meta) unless @metadata # 从实体主体里读取值 @value = response.read end return @value end 在 S3 服务上创建 S3 对象跟创建 S3 桶的原理一样:向一个 URI 发送 PUT 请求即可。对 于 S3 桶(bucket)的创建,PUT 请求是比较简单的,因为一个桶除了名称(已包含在 PUT 请求的 URI 里)以外,没有其他属性。而对于 S3 对象(object)的创建,PUT 请求要复 杂一些,因为 HTTP 客户端要在 PUT 请求里指定对象的元数据(比如 Content-Type)和 值(我们之后可以用 HEAD 或 GET 请求来获取这些信息) 。 好在构造一个创建 S3 对象的 PUT 请求不是十分复杂,因为客户端可以决定对象的值,而 且不需要把对象的值包装为 XML 文档或其他形式, 只要把它按原状放入 PUT 请求的实体 主体(entity-body)里,并按照 metadta hash 里的各项元数据设置好 HTTP 报头(headers) 就行了(见示例 3-12) 。 示例 3-12:用 Ruby 编写 S3 客户端:S3::Object#put 方法 # 在 S3 上保存对象 def put(acl_policy=nil) # 以原始元数据的副本开始,或者, # 如果没有元数据的话,就以空 hash 开始。 args = @metadata ? @metadata.clone : {} # 设置 HTTP 方法、实体主体及一些另外的 HTTP 报头 args[:method] = :put args[quot;x-amz-aclquot;] = acl_policy if acl_policy if @value args[quot;Content-Lengthquot;] = @value.size.to_s args[:body] = @value end # 向对象的 URI 发送 PUT 请求 open(uri, args) return self end S3::Object#delete 的实现(见示例 3-13)跟 S3::Bucket#delete 一模一样。 示例 3-13:用 Ruby 编写 S3 客户端:S3::Object#delete 方法 # 删除对象 def delete # 向对象的 URI 发送 DELETE 请求 open(uri, :method => :delete) end 一个 S3 客户端 │ 63
  • 17. 示例 3-14 显示的是一个用于“根据 HTTP 响应报头生成 S3 对象元数据”的方法。你应该 为你设置的所有元数据报头(除 Content-Type 以外)添加前缀“x-amz-meta-”,否则它 们被发给 S3 服务器后, 就不会再回到 Web 服务客户端——S3 服务器会认为它们是因为客 户端软件故障产生的,并丢弃它们。 示例 3-14:用 Ruby 编写 S3 客户端:S3::Object#store_metadata 方法 private # 给定一个包含 HTTP 响应报头的 hash, # 选取那些跟 S3 对象相关的报头,然后把它们保存在实例变量@metadata 里。 def store_metadata(new_metadata) @metadata = {} new_metadata.each do |h,v| if RELEVANT_HEADERS.member?(h) || h.index('x-amz-meta') == 0 @metadata[h] = v end end end RELEVANT_HEADERS = ['content-type', 'content-disposition', 'content-range', 'x-amz-missing-meta'] end 对请求进行签名及访问控制 Request Signing and Access Control 我已经把关于 S3 认证的话题尽量延后了,现在是时候对它作一下讲解了。假如你主要关 心的是 REST 式服务的概况,可以略过本节,直接跳到“使用 S3 客户端库”一节。不过 假如你对 S3 的内部工作原理有兴趣的话,请继续阅读。 用我前面展示的代码是可以发出 HTTP 请求,不过这些请求到达 S3 后会被拒绝处理,因 为这些请求里没有包含关键的 Authorization 报头——这样,S3 就无法证实你是不是这 些桶的主人。别忘了,你是要为你在 S3 服务上存放的数据及产生的流量向 Amazon 付费 的。假如 S3 对操作桶的请求不加以认证就执行的话,那么你可能要为别人在你的桶里存 放的数据埋单。 大多数需要认证的 Web 服务,都采用标准的 HTTP 机制来核实你是否的确是自称的那个 人。但 S3 的需求比较复杂。在大多数 Web 服务里,你绝不希望自己的数据被他人使用。 但是 S3 的用途之一就是用于寄存服务。你也许会把一部电影寄存在 S3 上,供人们用 BitTorrent 来下载(当然你要为此付费)。 或者,你也许想出售对存放在 S3 上的电影文件的访问权。你的电子商务网站在收到一位 客户的付款后,把 S3 上的文件的 URI 告诉他。这意味着,你在授权他人“作特定 Web 服务调用(GET 请求)”的权利,而由你来付费。 64 │ 第 3 章:REST 式服务有什么特别不同?
  • 18. 标准的 HTTP 认证机制无法为此种应用提供安全性支持。因为一般来说,标准的 HTTP 认 证机制需要让发送 HTTP 请求的人知道实际的密码。你可以防止别人窃取密码,但不能对 另一个人说“这是我的密码,但你必须保证你只能用它来请求这个 URI。 ” 在 S3 里,这个问题是用消息认证代码(Message Authentication Code,MAC)解决的。你 每次发送 S3 请求时,都用你的密钥(只有你和 Amazon 知道)来对请求中的重要部分(比 如:URI、HTTP 方法,以及一些 HTTP 报头)进行签名。因为只有知道密钥的人才能够 为请求生成正确的签名,所以 Amazon 由此可以确信应该向你收费。在对一个请求签名以 后,可以把该签名发给第三方(不必告诉他你的密钥) ;该第三方由于拥有你的签名,所 以他可以发送签名的那个请求, 并令 Amazon 向你收费。 总之, 其他人可以在一定时间内, 像你一样发出特定的请求,而不必知道你的密钥。 要为你的 S3 对象开通匿名访问权限,有一种较为简单的方法(后面我会谈到) 。但是对自 己的请求进行签名是无法避免的,所以即使是像这样的一个简单的库,也必须支持对请求 进行签名才行。我将对 S3::Authorized 模块进行重新编写,为它增加一项功能:截取对 open 方法的调用,并在发出 HTTP 请求前对它进行签名。因为 S3::BucketList 、 S3::Bucket 和 S3::Object 都已经包含(include)了这个模块,所以它们将自动继承该 功能。假如不为 S3::Authorized 模块添加这个功能,那么我前面在各个类里定义的所有 open 调用,都将发送未签名的 HTTP 请求——S3 将对这些请求返回响应代码 403 (“Forbidden”。 S3::Authorized 模块添加这个功能后, )为 你就可以生成已签名的 HTTP 请求,这样就可以通过 S3 的安全策略了(并让你支付费用) 。示例 3-15 及后面一些示例 中的代码都相当依赖于 Amazon 自己的示例 S3 库。 示例 3-15:用 Ruby 编写 S3 客户端:S3::Authorized 模块 module Authorized # 这些是 S3 认为跟签名请求相关的标准 HTTP 报头 INTERESTING_HEADERS = ['content-type', 'content-md5', 'date'] # 这些前缀用于自定义元数据报头。 # 所有自定义元数据报头都被认为跟签名请求相关 AMAZON_HEADER_PREFIX = 'x-amz-' # 为用 rest-open-uri 实现的 open()方法针对 S3 作的一个封装。 # 该实现在发送请求之前进行一些 HTTP 报头的设置, # 其中最重要的是 Authorization 报头,Amazon 将据此决定该向谁收费。 def open(uri, headers_and_options={}, *args, &block) headers_and_options = headers_and_options.dup headers_and_options['Date'] ||= Time.now.httpdate headers_and_options['Content-Type'] ||= '' 对请求进行签名及访问控制 │ 65
  • 19. signed = signature(uri, headers_and_options[:method] || :get, headers_and_options) headers_and_options['Authorization'] = quot;AWS #{@@public_key}:#{signed}quot; Kernel::open(uri, headers_and_options, *args, &block) end 现在的一项艰巨任务是实现 signature 方法。这个方法的作用是构建一个加密字符串。 该加密字符串将被放入请求的 Authorization 报头里,以令 S3 服务相信请求确实是你发 的,或者是你授权别人发的(见示例 3-16) 。 示例 3-16:用 Ruby 编写 S3 客户端:Authorized#signature 模块 # 为 HTTP 请求构造加密签名。 # 这是(用你的密钥)对一个“规范化字符串”的签名, # 其中含有跟该请求相关的所有信息。 def signature(uri, method=:get, headers={}, expires=nil) # URI 或者是个字符串,或者是个 Ruby URI 对象。 if uri.respond_to? :path path = uri.path else uri = URI.parse(uri) path = uri.path + (uri.query ? quot;?quot; + query : quot;quot;) end # 构造规范化字符串,并对它进行签名。 signed_string = sign(canonical_string(method, path, headers, expires)) end 注意,本方法是通过对 canonical_string 的返回值调用 sign 来构造规范化字符串的。 我们来看一下这两个方法。先看 canonical_string,它把一个 HTTP 请求转换成一个如 示例 3-17 所示的字符串,该字符串具有特定的格式,其中含有(从 S3 的角度来看)跟 HTTP 请求相关的所有信息。这些相关信息包括:HTTP 方法(PUT) Content-type 、 ( ext/plain”、日期、其他一些 HTTP 报头( “t ) “x-amz-metadata”,以及 URI 里的路径部 ) 分( /crummy.com/myobject”。sign 方法将对这个字符串进行签名。任何人都知道如何 “ ) 创建上述字符串,但只有 S3 注册用户和 Amazon 自己知道如何生成正确的签名。 示例 3-17:一个请求的规范化字符串 PUT text/plain Fri, 27 Oct 2006 21:22:41 GMT x-amz-metadata:Here's some metadata for the myobject object. /crummy.com/myobject Amazon 服务器收到你的 HTTP 请求后,它会根据请求也生成一个规范化字符串,并对它 进行签名(别忘了,Amazon 也知道你的密钥) ,然后 Amazon 服务器将比较两个签名,看 是否匹配。S3 的身份认证就是这样实现的。如果签名匹配,你的请求将被执行;否则, 你将得到响应代码 403( “Forbidden”。 ) 66 │ 第 3 章:REST 式服务有什么特别不同?
  • 20. 示例 3-18 显示的是用于生成规范化字符串的代码。 示例 3-18:用 Ruby 编写 S3 客户端:Authorized#canonical_string 方法 # 根据 HTTP 请求的各个部分生成一个用于签名的字符串。 def canonical_string(method, path, headers, expires=nil) # 为所有相关报头设置默认值 sign_headers = {} INTERESTING_HEADERS.each { |header| sign_headers[header] = '' } # 把实际的值(包括用于自定义 S3 报头的值)复制进来 headers.each do |header, value| if header.respond_to? :to_str header = header.downcase # 如果是一个自定义报头或 S3 认为相关的报头... if INTERESTING_HEADERS.member?(header) || header.index(AMAZON_HEADER_PREFIX) == 0 # 把它加入到报头里 sign_headers[header] = value.to_s.strip end end end # 这个库不需要 Amazon 定义的 x-amz-date 报头,不过可能会有人设置这个报头。 # 假如设置了这个报头的话,我们就不采用 HTTP 的标准 Date 报头。 sign_headers['date'] = '' if sign_headers.has_key? 'x-amz-date' # 如果提供了过期时间的话,那么它优先于其他 Date 报头。 # 这个签名将在过期时间内都有效。 sign_headers['date'] = expires.to_s if expires # 现在,我们开始为该请求构造规范化字符串。我们从 HTTP 方法开始。 canonical = method.to_s.upcase + quot;nquot; # 把所有报头按名称排序,然后把这些报头(或仅仅是值)添加到将被签名的字符串里。 sign_headers.sort_by { |h| h[0] }.each do |header, value| canonical << header << quot;:quot; if header.index(AMAZON_HEADER_PREFIX) == 0 canonical << value << quot;nquot; end # 最后对 URI 路径进行签名。我们去掉查询字符串(query string) , # 并附上一个专门的 S3 查询参数: ‘acl’‘torrent’或‘logging’ 、 。 canonical << path.gsub(/?.*$/, '') for param in ['acl', 'torrent', 'logging'] if path =~ Regexp.new(quot;[&?]#{param}($|&|=)quot;) canonical << quot;?quot; << param 对请求进行签名及访问控制 │ 67
  • 21. break end end return canonical end 在 sign 的实现里,实际起作用的是 Ruby 的标准加密与编码接口(见示例 3-19)。 示例 3-19:用 Ruby 编写 S3 客户端:Authorized#sign 方法 # 用客户端的 Secret Access Key(即密钥)对一个字符串进行签名, # 并用 base64 把签名后得到的二进制串编码为纯 ASCII 码。 def sign(str) digest_generator = OpenSSL::Digest::Digest.new('sha1') digest = OpenSSL::HMAC.digest(digest_generator, @@private_key, str) return Base64.encode64(digest).strip end 对 URI 进行签名 Signing a URI 我的 S3 库还有一个功能要实现。我已多次提到,S3 允许你在对一个 HTTP 请求签名后, 把 URI 给其他人,这样他们就可以像你一样发送请求了。 实现这一目标需要用到 signed_ uri 这个方法(见示例 3-20) 。不是用 open 来发送 HTTP 请求,而是把 open 的参数传给 这个方法,然后它会给你一个签了名的 URI(别人可以像你一样用这个 URI) 。为防止滥 用, 个签了名的 URI 只在一定时间内有效。 一 你可以通过为 :expires 参数传进一个 Time 对象来设置这个时间。 示例 3-20:用 Ruby 编写 S3 客户端:Authorized#signed_uri 方法 # 根据一个 HTTP 请求的信息,返回一个你可以给别人用的 URI, # 令他们可以像你一样发出特定的 HTTP 请求。 # 该 URI 将在你所指定的时间(默认为 15 分钟)内有效。 def signed_uri(headers_and_options={}) expires = headers_and_options[:expires] || (Time.now.to_i + (15 * 60)) expires = expires.to_i if expires.respond_to? :to_i headers_and_options.delete(:expires) signature = URI.escape(signature(uri, headers_and_options[:method], headers_and_options, nil)) q = (uri.index(quot;?quot;)) ? quot;&quot; : quot;?quot; quot;#{uri}#{q}Signature=#{signature}&Expires=#{expires}&AWSAccessKeyId=#{@@public_key}quot; end end end # 还记得那个容纳所有代码的 S3 模块的开头吗?这里是它的结尾。 它的工作原理是这样:假设我想给一个客户访问我保存在 https://s3.amazonaws.com/ BobProductions/KomodoDragon.avi 的文件的权限,可以运行如示例 3-21 所示的代码为他 生成一个 URI。 68 │ 第 3 章:REST 式服务有什么特别不同?
  • 22. 示例 3-21:生成一个签了名的 URI #!/usr/bin/ruby1.9 # s3-signed-uri.rb require 'S3lib' bucket = S3::Bucket.new(quot;BobProductionsquot;) object = S3::Object.new(bucket, quot;KomodoDragon.aviquot;) puts object.signed_uri # quot;https://s3.amazonaws.com/BobProductions/KomodoDragon.avi # ?Signature=J%2Fu6kxT3j0zHaFXjsLbowgpzExQ%3D # &Expires=1162156499&AWSAccessKeyId=0F9DBXKB5274JKTJ8DG2quot; 这个 URI 将在 15 分钟内有效 (默认值) 该 URI 中含有我的公共标识符 AWSAccessKeyId) 。 ( 、 过期时间(Expires)及加密的签名(Signature) 。我的客户可以访问这个 URI,并下载 电影文件 KomodoDragon.avi——由此产生的流量费用将由我向 Amazon 支付。 假如某个客 户对 URI 里的任何部分作修改的话 (比如他想下载另一部电影) S3 服务会拒绝他的要求。 , 也许有的客户会把这个 URI 发给其他人用,不过 15 分钟后这个 URI 就失效了。 你也许已经发现这里的一个问题了。规范化字符串里通常含有 Date 报头的值,如此一来, 客户访问这个签过名的 URI 时,它们的 Web 浏览器发出的日期报头的值肯定跟签名里的 不一样。这就是为什么在生成规范化字符串时,你要设置一个过期日期(expiration)而不 是请求日期的原因。回顾一下示例 3-18 中 canonical_string 的实现可以看到,过期日 期(假如有的话)是优先于 Date 报头的。 设置访问策略 Setting Access Policy 假如我想令一个对象可被公开访问的话,该怎么做呢?我想把一些文件对外界开放,并让 Amazon 来处理烦人的服务器管理问题。嗯,其实我可以把过期日期设为一个很久远的日 子,并公开发布所有已签名的 URI。不过,还有一种更简单的实现办法:即允许匿名访问。 你可以为桶(bucket)或对象(object)设置访问策略(access policy),告诉 S3 可以响应 未签名的请求。你可以在发送用于创建桶或对象的 PUT 请求时,附上 x-amz-acl 报头来 设置访问策略。 这就是 Bucket#put 和 Object#put 方法的 acl_policy 参数的作用。如果你想令一个桶 或对象成为公开可读或公开可写的,你就为 acl_policy 参数设置适当的参数值就行了。 我的客户端是把该值作为自定义 HTTP 请求报头 x-amz-acl 的一部分来发送的。Amazon S3 会读取这个请求报头,并为桶或对象设置相应的访问规则。 示例 3-22 所示的客户端创建了一个 S3 对象,任何人均可通过其 URI https://s3. amazonaws.com/BobProductions/KomodoDragon-Trailer.avi 来读取它。这里,我并 不是出售该电影文件,我只是把 Amazon 作为寄存服务使用,省去了自己做网站来存放这 些文件的麻烦。 对请求进行签名及访问控制 │ 69
  • 23. 示例 3-22:创建一个可公开读取的对象 #!/usr/bin/ruby -w # s3-public-object.rb require 'S3lib' bucket = S3::Bucket.new(quot;BobProductionsquot;) object = S3::Object.new(bucket, quot;KomodoDragon-Trailer.aviquot;) object.put(quot;public-readquot;) S3 支持下列四种访问策略。 private 这是默认的。只有用你的密钥签过名的请求才被接受。 public-read 可以接受未签名的 GET 请求。这意味着,任何人都可以下载一个对象或列出一个桶 里的内容。 public-write 可以接受未签名的 GET 和 PUT 请求。这意味着,任何人都可以修改一个对象或向桶 里添加对象。 authenticated-read 未签名的请求将被拒绝,但是用别的 S3 用户的密钥签名的读请求也将被接受。这就 是说,任何拥有 S3 账户的人都可以下载你的对象或列出桶里的内容。 给桶或对象设置权限,还有细粒度的方法。我没有介绍这部分,假如你有兴趣,可以参阅 S3 技术文档的“Setting Access Policy with REST”部分。那里将向你介绍一个平行的附属 资源空间: 每个桶 /{name-of-bucket} 有一个影子资源 (shadow resource) /{name-of- bucket}?acl,该影子资源对应于该桶的访问控制规则;同样地,每个对象 /{name-of- bucket}/{name-of-object} 也 有 一 个 影 子 资 源 /{name-of-bucket}/{name-of- object}?acl。你可以通过向这些 URIs 发送 PUT 请求(在实体主体里给出 XML 格式的 访问控制列表) ,为特定的桶或对象设置特定的权限,或针对特定 S3 用户设定访问权限。 使用 S3 客户端库 Using the S3 Client Library 到目前为止,我的 Ruby 客户端库已经可以访问差不多 S3 服务的全部功能了。当然,一 个库要有用户,才是有用的库。在上一节,我通过一些简单的客户端向你展示了安全方面 的要点,现在我想展示一些更重要的东西。 示例 3-23 是一个简单的命令行 S3 客户端,它将创建一个桶和一个对象,然后列出桶里的 内容。该示例可以让你从较高的层次上理解 S3 资源是如何相互协作的。我在注释里标出 了触发 HTTP 请求各行。 70 │ 第 3 章:REST 式服务有什么特别不同?
  • 24. 示例 3-23:一个 S3 客户端的示例 #!/usr/bin/ruby -w # s3-sample-client.rb require 'S3lib' # 收集命令行参数 bucket_name, object_name, object_value = ARGV unless bucket_name puts quot;Usage: #{$0} [bucket name] [object name] [object value]quot; exit end # 找到或创建桶 buckets = S3::BucketList.new.get # GET / bucket = buckets.detect { |b| b.name == bucket_name } if bucket puts quot;Found bucket #{bucket_name}.quot; else puts quot;Could not find bucket #{bucket_name}, creating it.quot; bucket = S3::Bucket.new(bucket_name) bucket.put # PUT /{bucket} end # 创建对象 object = S3::Object.new(bucket, object_name) object.metadata['content-type'] = 'text/plain' object.value = object_value object.put # PUT /{bucket}/{object} # 对于桶里的每个对象... bucket.get[0].each do |o| # GET /{bucket} # ...打印出有关该对象的信息 puts quot;Name: #{o.name}quot; puts quot;Value: #{o.value}quot; # GET /{bucket}/{object} puts quot;Metadata hash: #{o.metadata.inspect}quot; puts end 用 ActiveResource 创建透明的客户端 Clients Made Transparent with ActiveResource 既然所有 REST 式 Web 服务暴露的都是基本相同的简单接口,那么定制一个适用于所有 Web 服务的客户端并不是难事——不过这有点浪费了。你另外有两个方案可选: (1)用 WADL 文档(上一章介绍过,第 9 章将详细介绍)来描述服务,然后通过一个通用 WADL 客户端来访问它; (2)有一个名为 ActiveResource 的 Ruby 库,它使得为某些种类的 Web 服务编写客户端变得轻而易举。 ActiveResource 用于访问那些暴露出关系数据库里的记录与数据的 Web 服务。 WADL 可用 于描述几乎各类 Web 服务, ActiveResource 只能用于符合一定规则的 Web 服务。 但 Ruby on 用 ActiveResource 创建透明的客户端 │ 71
  • 25. Rails 是目前唯一符合这些规则的框架。不过,一个服务只要通过跟 Rails 一样的 REST 式接口暴露出数据库,就能响应来自 ActiveResource 客户端的请求了。 在本书编写之时,可用 ActiveResource 客户端调用的公共 Web 服务还不多(我在附录 A 中列出了一些)。下面,我来创建一个简单的 Rails Web 服务。我将能够用 ActiveResource 来调用这个服务,而不必为此编写任何 HTTP 客户端或 XML 解析代码。 创建一个简单的服务 Creating a Simple Service 我要创建的是一个简单的笔记本 (notebook)Web 服务:它能保存带时间戳的笔记(notes)。 因为我的计算机上已经安装了 Rails 1.2,所以可以像下面这样来创建这个笔记本服务: $ rails notebook $ cd notebook 我在自己的计算机上创建了一个名为 notebook_development 的数据库,然后编辑 Rails 文件 notebook/config/database.yml,把连接这个数据库所需的信息提供给 Rails。任何 Rails 的一般性指南都会对这些初始步骤有较详细的介绍。 现在, 我已经创建了一个 Rails 应用,但是它还做不了任何事。我将用 scaffold_resource 生成器为一个简单的 REST 式 Web 服务生成代码。希望我的笔记(note)包含一个时间戳 (timestamp)和一段文本(text) ,所以运行如下命令: $ ruby script/generate scaffold_resource note date:date body:text create app/views/notes create app/views/notes/index.rhtml create app/views/notes/show.rhtml create app/views/notes/new.rhtml create app/views/notes/edit.rhtml create app/views/layouts/notes.rhtml create public/stylesheets/scaffold.css create app/models/note.rb create app/controllers/notes_controller.rb create test/functional/notes_controller_test.rb create app/helpers/notes_helper.rb create test/unit/note_test.rb create test/fixtures/notes.yml create db/migrate create db/migrate/001_create_notes.rb route map.resources :notes Rails 已经为我的“笔记(note) ”对象生成了 Web 服务代码的各个部分——模型(model) 、 视图(view)和控制器(controller)。在 db/migrate/001_create_notes.rb 里有一段 代码,它用于创建一个名为 notes 并具有以下字段的数据库表:一个唯一 ID、一个日期 (date)和一段文本(body) 。 app/models/note.rb 里的模型部分 (model)提供了一个用于访问数据库表的 ActiveResource 接口; app/controllers/notes_controller.rb 里的控制器部分(controller)通过 HTTP , 把那个接口暴露给外界;app/views/notes 里的视图部分(view)定义了用户界面。 72 │ 第 3 章:REST 式服务有什么特别不同?
  • 26. 一个 REST 式 Web 服务构建好了——它虽然功能不强,但用作示范足够了。 在启动该服务之前,我需要先初始化数据库: $ rake db:migrate == CreateNotes: migrating =========================================== -- create_table(:notes) -> 0.0119s == CreateNotes: migrated (0.0142s) ================================== 现在,我可以启动笔记本应用,并开始使用我的服务了: $ script/server => Booting WEBrick... => Rails application started on http://0.0.0.0:3000 => Ctrl-C to shutdown server; call with --help for options 一个 ActiveResource 客户端 An ActiveResource Client 我刚刚创建的应用作为一个示范, 功能并不强大, 但它展示了一些印象深刻的特性。 首先, 它既是一个 Web 服务, 也是一个 Web 应用。我可以用 Web 浏览器访问 http://localhost:3000/ notes,然后通过 Web 接口来创建笔记。图 3-1 显示的是经过我的一系列操作之后, http://localhost:3000/notes 所呈现出的视图。 图 3-1:包含一些笔记的笔记本 Web 应用 假如你曾经编写过 Rails 应用,或者看过 Rails 的 demo,那么你对这个例子应该比较熟悉。 不过在 Rails 1.2 里,生成的模型和控制器也可以作为一个 REST 式 Web 服务——你可以 用程序编写一个客户端,像 Web 浏览器那样简单地访问它。 可惜 ActiveResource 客户端本身没有随同 Rails 1.2 一起发布。在本书编写之时,它还在 Rails 的开发树(development tree)中被开发。要获取其代码,需要从 Subversion 版本控 制库里把代码 check out 出来: $ svn co http://dev.rubyonrails.org/svn/rails/trunk activeresource_client $ cd activeresource_client 用 ActiveResource 创建透明的客户端 │ 73
  • 27. 现在,准备工作已经完毕,我要开始为我的笔记本 Web 服务编写 ActiveResource 客户端 了。示例 3-24 是一个客户端,它先创建一则笔记,然后修改它,接着列出已有笔记,最 后删除这则笔记。 示例 3-24:一个用于笔记本服务的 ActiveResource 客户端 #!/usr/bin/ruby -w # activeresource-notebook-manipulation.rb require 'activesupport/lib/active_support' require 'activeresource/lib/active_resource' # 为网站暴露的对象定义一个模型 class Note < ActiveResource::Base self.site = 'http://localhost:3000/' end def show_notes notes = Note.find :all # GET /notes.xml puts quot;I see #{notes.size} note(s):quot; notes.each do |note| puts quot; #{note.date}: #{note.body}quot; end end new_note = Note.new(:date => Time.now, :body => quot;A test notequot;) new_note.save # POST /notes.xml new_note.body = quot;This note has been modified.quot; new_note.save # PUT /notes/{id}.xml show_notes new_note.destroy # DELETE /notes/{id}.xml puts show_notes 示例 3-25 显示的是运行该程序产生的输出。 示例 3-25:activeresource-notebook-manipulation.rb 的一次运行 I see 3 note(s): 2006-06-05: What if I wrote a book about REST? 2006-12-18: Pasta for lunch maybe? 2006-12-18: This note has been modified. I see 2 note(s): 2006-06-05: What if I wrote a book about REST? 2006-12-18: Pasta for lunch maybe? 如果你熟悉 ActiveRecord(用于连接 Rails 和数据库的对象关系型数据映射组件(object- relational mapper))的话,你会注意到 ActiveResource 的接口看上去跟 ActiveRecord 差不 多。它们均为各种暴露统一接口的对象提供了一个面向对象的接口。对于 ActiveRecord, 74 │ 第 3 章:REST 式服务有什么特别不同?
  • 28. 对象在数据库里,通过 SQL(采用 SELECT、INSERT、UPDATE 和 DELETE 等)来访问 这些对象;而对于 ActiveResource,对象在 Rails 应用里,通过 HTTP(采用 GET、POST、 PUT 和 DELETE)来访问这些对象。 示例 3-26 是从 Rails 服务器日志里摘录的一段与运行我的 ActiveResource 客户端相关的记 录。其中的 GET、POST、PUT 和 DELETE 请求分别与示例 3-24 里被注释的代码行对应。 示例 3-26:activeresource-notebook-manipulation.rb 发出的 HTTP 请求 quot;POST /notes.xml HTTP/1.1quot; 201 quot;PUT /notes/5.xml HTTP/1.1quot; 200 quot;GET /notes.xml HTTP/1.1quot; 200 quot;DELETE /notes/5.xml HTTP/1.1quot; 200 quot;GET /notes.xml HTTP/1.1quot; 200 这些请求做了什么?跟发给 S3 的请求一样,通过 HTTP 统一接口进行资源访问。我的笔 记本服务暴露了两种资源(resource) : 笔记列表(/notes.xml),相当于 S3 里的桶(它是一个对象列表); 一则笔记(/notes/{id}.xml),相当于 S3 里的对象。 跟 S3 资源一样,这些资源也暴露 GET、PUT 和 DELETE。笔记列表还支持用 POST 来创 建一则新笔记——这跟 S3 有点不同(在 S3 里,对象是用 PUT 方法创建的) 过这是符 ,不 合 REST 风格的。 当客户端运行时,客户端与服务器之间的 XML 文档传递是透明的。这些 XML 文档是对 下层数据库记录的简单描绘,就像示例 3-27 或示例 3-28 那样。 示例 3-27:一个响应实体主体(对发给 /notes.xml 的 GET 请求的响应) <?xml version=quot;1.0quot; encoding=quot;UTF-8quot;?> <notes> <note> <body>What if I wrote a book about REST?</body> <date type=quot;datequot;>2006-06-05</date> <id type=quot;integerquot;>2</id> </note> <note> <body>Pasta for lunch maybe?</body> <date type=quot;datequot;>2006-12-18</date> <id type=quot;integerquot;>3</id> </note> </notes> 示例 3-28:一个请求实体主体(发给 /notes/5.xml 的 PUT 请求) <?xml version=quot;1.0quot; encoding=quot;UTF-8quot;?> <note> <body>This note has been modified.</body> </note> 用 ActiveResource 创建透明的客户端 │ 75
  • 29. 访问 ActiveResource 服务的 Python 客户端 A Python Client for the Simple Service 目前,Ruby 的 ActiveResource 库是唯一的 ActiveResource 客户端库,Rails 是唯一能暴露 跟 ActiveResource 兼容的服务的框架。 不过它只是发送一些传递 XML 文档的 HTTP 请求、 并获取返回的 XML 文档而已,用其他语言编写的客户端应该也能发送那些 XML 文档, 用其他框架应该也能暴露同样的 URI。 在示例 3-29 中,我用 Python 实现了示例 3-24 所示的客户端程序。这个程序没有基于 ActiveResource,所以它要比示例 3-24 的 Ruby 实现长一些。在这个 Python 实现里,它必 须自己来构造 XML 文档和发送 HTTP 请求, 但是其结构跟示例 3-24 所示的客户端程序是 基本一样的。 示例 3-29:用 Python 实现一个 ActiveResource 服务的客户端 #!/usr/bin/python # activeresource-notebook-manipulation.py from elementtree.ElementTree import Element, SubElement, tostring from elementtree import ElementTree import httplib2 import time BASE = quot;http://localhost:3000/quot; client = httplib2.Http(quot;.cachequot;) def showNotes(): headers, xml = client.request(BASE + quot;notes.xmlquot;) doc = ElementTree.fromstring(xml) for note in doc.findall('note'): print quot;%s: %squot; % (note.find('date').text, note.find('body').text) newNote = Element(quot;notequot;) date = SubElement(newNote, quot;datequot;) date.attrib['type'] = quot;datequot; date.text = time.strftime(quot;%Y-%m-%dquot;, time.localtime()) body = SubElement(newNote, quot;bodyquot;) body.text = quot;A test notequot; headers, ignore = client.request(BASE + quot;notes.xmlquot;, quot;POSTquot;, body= tostring(newNote), headers={'content-type' : 'application/xml'}) newURI = headers['location'] modifiedBody = Element(quot;notequot;) body = SubElement(modifiedBody, quot;bodyquot;) body.text = quot;This note has been modifiedquot; client.request(newURI, quot;PUTquot;, body=tostring(modifiedBody), headers={'content-type' : 'application/xml'}) showNotes() 76 │ 第 3 章:REST 式服务有什么特别不同?
  • 30. client.request(newURI, quot;DELETEquot;) print showNotes() 最后的话 Parting Words 因为REST式Web服务具有简单和良定的(well-defined)接口,所以可以容易地做到克隆 一 个 REST 式 Web 服 务 , 或 者 为 一 个 REST 式 Web 服 务 替 换 实 现 。 Park Place ( http:// code.whytheluckystiff.net/parkplace)是一个具有跟S3 一样的接口的Ruby应用。 你可以用Park Place来提供自己的S3 服务。S3 的库和客户端程序同样可用于访问Park Place的服务器,就 好比在访问https://s3.amazonaws.com/一样。 克隆 ActiveResource 也是可以的。虽然还没人这么做,但是为 Python 或其他动态语言编 写一个通用的 ActiveResource 客户端并不难。另一方面,为一个兼容 ActiveResource 的服 务编写一次性客户端,跟为其他 REST 式服务编写客户端同样简单。 现在,你应该对编写 REST 式或 REST-RPC 混合服务(无论它提供 XML、HTML、JSON, 还是某种混合)的客户端感到轻松了——它们就是 HTTP 请求和文档解析而已。 同时, 你对 REST 式 Web 服务 (如 S3 和 Yahoo!搜索服务) RPC 式和混合服务 跟 (如 Flickr 和 del.icio.us APIs)的区别也应该有所了解了。这不是对服务内容的评判,而是对其架构 的判断。在对木材进行加工时,应顺应木材的纹理进行得当的加工。Web 也有“纹理” , 而 REST 式 Web 服务正是一种吻合 Web 纹理的设计。 在接下来的章节中,我将教你如何创建像 S3(而不是像 del.icio.us API)一样的 REST 式 Web 服务。第 7 章之前,我都将围绕这一主题。在第 7 章,我们会把 del.icio.us 重新作为 REST 式 Web 服务来设计。 最后的话 │ 77