1242982622api2 upload

30
免费试读章节 (非印刷免费在线版) 如果你喜欢本书,请去 China-pub 第二书店 卓越网 当当网 购买印刷版以支持作者和InfoQ中文站 向博文视点出版公司以及译者徐涵等致谢 本图书节选由InfoQ中文站 免费发放,如果你从其它渠道获取此摘选, 请注册InfoQ中文站 以支持作者和出版商 本摘选主页为 http://infoq.com/cn/articles/restlet-for-restful-services RESTful Web Services 中文版》官方网站为 http://restfulwebservices.cn/

Upload: 51-lecture

Post on 30-Aug-2014

2.270 views

Category:

Documents


4 download

DESCRIPTION

 

TRANSCRIPT

Page 1: 1242982622API2 upload

免费试读章节

(非印刷免费在线版)

如果你喜欢本书,请去

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

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

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

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

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

本摘选主页为

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

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

http://restfulwebservices.cn/

Page 2: 1242982622API2 upload

49

第 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 上

Page 3: 1242982622API2 upload

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

实际存在的。我会在第 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

Page 4: 1242982622API2 upload

S3 的面向对象设计 │

关于桶 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 # 这个实例方法用于获取桶里的对象

51

Page 5: 1242982622API2 upload

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

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

Page 6: 1242982622API2 upload

资源 │

(如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})

元数据 取对象

的元数据 对象的

值及元数据 删除对象 获取对象的值 获 设置

这个表格看起来有点奇怪。那我为什么还要让它占用这里的宝贵篇幅呢?因为这些方法的

作 其实。在一个经良 服 个 名副其实。

仅 S3 是一个 服 所要实现的只

是在具名槽(named slots)里存放数据,那么你只要用 GET 和 PUT 就行了(分别用于读

用都名副

凭目前的论述,可能你还难

好设计的 REST 式

以信服。

务里,每

比较通用的

方法的作用都

务。假如你

和写)。在第 5 章和第 6 章,我会告诉你如何把任何动作映射到统一接口上。为了让你相

信这一点,请注意,其实只要定义一个只响应 GET 方法的“桶列表”资源,就可以代替

S3Bucket.getBuckets 方法了;同样地,在这样的资源设计下,S3Bucket#addObject

(它要求每个对象与一些桶对应)也不需要了。

我们来跟 S3 的 RPC 式 SOAP 接口作个比较。如果采用 SOAP 接口的话,获取桶列表,要

用 ListAllMyBuckets方法;获取桶里的内容,要用 ListBucket方法。而采用 REST 式

53

Page 7: 1242982622API2 upload

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

接口的话,这些操作全部用 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="1.0" encoding="UTF-8"?> <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

Page 8: 1242982622API2 upload

一个 S3 客户端 │

将无法判断一个 XML 文档里包含的是数据还是错误描述。HTTP 响应代码正好能够充当

这个规则,它给出了关于客户端应当如何处理 HTTP 响应的一个大致的规则。因为响应代

码不是放在元数据或实体主体里的,所以即便客户端不知如何读取它们,也能了解发生了

什么情况。

Not Found”)以外,S3 还使用了许多其他响应代码。 常见的

端发出的请求未包含正确的证书。S3 还使用其

(表明服务器无法理解来自客户端的数据)和

An S3 Client

S3 客户端库的。但我介绍 S3 的目的,并不是告诉你存在这样一个多么有用的

Web 服务,而是想通过 S3 来举例说明 REST 背后的理论。所以,我将用 Ruby 来写一个

步剖析它。

为了 之上实现(implement)一个面向对象的接口(就

像示 Record 或其他对象关系型数据映射组件

(ob 内部,它并不是发出 SQL 请求来往数据库里存

储数

采用

(如

我首 n 特有的 Web 服务认证机制。不过这并不如直接对 Web服务 一个非常简单的

Rub 后我会回到

这里

示例 段初始代码。

除 200(“OK”)和 404(“可能是 403(“Forbidden”)了,它表示客户

他一些响应代码,如 400(“Bad Request”)409(“Conflict”)(表明客户端要删除的桶是非空的)。完整的列表请参阅 S3 技术文档的

“The REST Error Response”部分。我将在附录 B 对每个 HTTP 响应代码作逐一讲解(主

要关注它们在 Web 服务上的应用)。官方的 HTTP 响应代码有 41 个,不过在日常使用中,

重要的只有 10 个左右。

一个 S3 客户端

因为已经有 Amazon 的示例库和一些第三方库(如 AWS::S3 等)了,所以一般我们是不需

要自己定制

自己的 S3 客户端,并在编写过程中来逐

展示这是可行的,我的库将在 S3 服务

例 3-1 中的那样)。 终结果将类似于 Activeject-relational mapper,ORM),不过在

据,而是发出 HTTP 请求来通过 S3 服务存储数据。我在给我的方法起名时,将尽量

et、put 等),而不是给出针对资源的名称能够反映下层 REST 式接口的名称(比如 ggetBuckets和 getObjects等)。

先需要一个接口来处理 Amazo进行实际考察更有意思,所以我准备暂时掠过这部分。我将先创建

类就可以包含(include)它了。y 模块 S3::Authorized,这样其他 S3,并补上有关细节。

3-3 显示了一

示例 3-3:用 Ruby 编写 S3 客户端:初始代码

#!/usr/bin/ruby -w # S3lib.rb # 发送 HTTP请求和解析响应所需的库 require 'rubygems' require 'rest-open-uri'

55

Page 9: 1242982622API2 upload

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

require 'rexml/document' # 对请求进行签名所需的库 require 'openssl' requirerequire 'base64'

'digest/sha1'

ey = '' if @@public_key.empty? or @@private_key.empty?

ed to set your S3 keys."

,除非你想使用 S3的克隆(比如 Park Place)。 HOST = 'https://s3.amazonaws.com/'

标识符(public identifier,Amazon称之为“Access Key ID”),这样 Amazon 就可以识别出请求来自于你。发出的每个请求都

端:S3::BucketList 类

的所有桶

require 'uri' module S3 # 一个容纳所有代码的模块的开头。 module Authorized # 输入你的公共标识符(Amazon称之为“Access Key ID”)和 # 你的密钥(Amazon称之为“Secret Access Key”)。 # 这样,你对你发出的 S3请求进行签名后,Amazon就知道该向谁收费了。 @@access_key_id = '' @@secret_access_k

raise "You ne end # 你不应修改这里

end

这段骨架代码里,唯一值得关注的是:应在哪里填写“跟你的 Amazon Web 服务账户相关

联”的两个密钥。发出的每个 S3 请求都包含你的公共

必须用你的密钥(Amazon 称之为“Secret Access Key”)进行签名,这样 Amazon 就知道

请求的确来自于你。该密钥只有你和 Amazon 知道,不应把它告诉任何其他人。如果让别

人知道了,那么他就可以用你的密钥发 S3 请求,令 Amazon 向你收费。

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

BucketList。

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

# 桶列表 class BucketList include Authorized # 获取该用户

def get buckets = []

56

Page 10: 1242982622API2 upload

一个 S3 客户端 │

# 向桶列表的 URI发送 GET请求,并读取返回的 XML文档。 doc = REXML::Document.new(open(HOST).read)

c, "//Bucket/Name") do |e| 对象,并把它添加到列表中。

Bucket.new(e.text) if e.text

# 对于每个桶... REXML::XPath.each(do # ... buckets <<

创建一个 Bucket end return buckets end end

XPath 讲解

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

寻找所有 标签 Name NameName标签?直接在 Bucket标签下的 Bucket/ 哪里的

Bucket标签?任何地方的 // 哪里的

现在 用 S3::BucketList#get,就等于向 https://s3.amazonaws.com/(“桶列表”资源的 URI)发送一个加密的 HTTP GET 请求。S3

AllMyBucketsResult xmlns='http://s3.amazonaws.com/doc/2006-03-01/'>

5fcf38d48039f4fb5cab21b060577817310be5170e7774aad70</ID> <DisplayName>leonardr28</DisplayName>

<Name>crummy.com</Name>

</Bucket> s>

Result>

在这个简 心桶的名称。XPath 表达式//Bucket/Name将给出每

个桶 对象时需要这个信息。

,它是一个真正的 Web 服务客户端了。调

服务会返回一个像示例 3-5 那样的 XML 文档——“桶列表”资源的一个表示

(representation)(我将在下一章介绍“表示”这个概念)。该 XML 文档给出了关于“桶

列表”资源的当前状态的一些信息:Owner标签表明这个桶列表的所有者是谁(我的 AWS账户名是“leonardr28”);Buckets标签里包含一些 Bucket标签,这些 Bucket标签是对

我的桶的描述(在本例中,因为只有一个桶,所以这里出现一个 Bucket标签)。

示例 3-5:一个“桶列表”的例子

<?xml version='1.0' encoding='UTF-8'?> <List <Owner> <ID>c0363f7260f2f

</Owner> <Buckets> <Bucket>

<CreationDate>2006-10-26T18:46:45.000Z</CreationDate>

</Bucket</ListAllMyBuckets

单的客户端应用里,我只关

的桶名,我在创建 Bucket

57

Page 11: 1242982622API2 upload

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

正如 k)。该 XML 文档给出

了每 角度来看,这是 Amazon S3的主 桶的桶名(bucket name)得出 URI 并不难,参照我前面给出

的规

桶 Th现在 来编写 S3::Bucket类(见示例 3-6)。这样,S3::BucketList.get就能实例

化一些桶了。

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

# 一个 S3桶 c def initialize(name)

中的讨论。

, args) elf

败, ”)。

end

这里又实现了两个 Web 服务方法:S3::Bucket#put 和 S3::Bucket#delete。因为一个

桶的 URI 唯一标识了该桶,所以删除操作很简单:向该桶的 URI 发送一个 DELETE 请求

我们所看到的,这个 XML 文档缺少的一样东西,即链接(lin桶的名称,但是没有给出这些桶的 URIs。从 REST 设计个

要不足。好在根据一个

则就行了:htps://s3.amazonaws.com/{name-of-bucket}/。

e Bucket ,我们

lass Bucket include Authorized attr_accessor :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访问策略(如果有提供的话)

{:method => :put} args = args["x-amz-acl"] = acl_policy if acl_policy

的 URI发送 PUT请求 # 向该桶 open(uri return s end

# 删除该桶。

# 该删除操作将失

# 并返回 HTTP响应代码 409(“Conflict如果桶不为空的话,

def delete # 向该桶的 URI发送 DELETE请求 open(uri, :method => :delete)

58

Page 12: 1242982622API2 upload

一个 S3 客户端 │

即可。因为桶的 URI 里包含了桶的名称,而且一个桶没有其他可设置的属性,所以创建

一个桶也很简单:向该 URI 发送一个 PUT 请求就行了。正如我将在编写 S3::Object代

码时向你展示的,假如不是所有数据都放在 URL 里,PUT 请求的实现将会比较复杂。

虽然我刚才把 S3:: 类比作 ActiveRecord 类,不过 S3::Bucket#put 跟 ActiveRecord 实

save还是有点区别的。在一个由 ActiveRecord 控制的数据库表里,每行记录都有一

如果你要修改一个 ID 为 23 的 ActiveRecord 对象的名称,那么你的修

ID 为 23 的数据库记录的修改:

"n RE id=23

对于一 S3 桶来说,它的 就是它的 , 的名称包含在 URI 里。假如你在调

的,并不是在 S3 里修改桶的名称,

而是 含有你设置的新桶名)创建一个新的空桶。这是由 S3 程序员的设计造

成的 免这样。Ruby on Rails 框架采用了与此不同的设计:当它通过一个

RES 记录时,每条记录的 URI 都含有该记录的数字 ID。假设

S3 是 URI 将像这样/buckets/23——这样,修改桶名称时,就

不会

现在 ::Bucket的 后一个方法 get。跟 S3::BucketList.get一样,这个方法(见

示例 URI 发送 GET 请求,以获取一个 XML 文

档, ML 文档生成 实例。这个方法支持以各种方式来过滤 S3桶的 些“键(key)以某字符串开头”的对象,那么可

以采 fix,等等。我就不对这些过滤选项作详细介绍了,如果你感兴趣,可以参阅

S3 技术文档的 部分。

示例

Marker、:Delimiter、:MaxKeys。 术文档的“Listing Keys”部分。

{}) # 该桶的基准 URI(base URI),并把子集选项(假如有的话)

符串(query string)上。

现的

个唯一的数字 ID。

改将体现为对一条

SET name= ewname" WHE

个 URI 永久 ID 桶

用 put时把桶的名称(name)换掉,那么客户端将做

为新的 URI(。其实完全可以避

T 式 Web 服务来暴露数据库

个 Rails 服务的话,桶的一

改变其 URI 了。

来看 S3

3-7)向一个资源(这里是一个“桶”)的

X S3::Object类然后根据该

内容,比方说,假如你只要获取那

:Pre用

“Listing Keys”

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

# 获取 (全 对象。 桶里的 部或部分)#

,那么 # 如果 S3决定不返回整个桶(或子集)

# 第二个返回值将被设为 true。要获取其余对象, 你需要调整子集选项(subset option)(本书未作介绍)。 #

# x、:# 子集选项包括::Prefi

S3技# 有关详情,请参阅t(options=def ge

获取 # 附加到查询字 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)

59

Page 13: 1242982622API2 upload

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

suffix = '&' end end

# 现在我们已经构造好了 URI。向该 URI发送 GET请求, # 并读取含有 S3对象信息的 XML文档。 doc = REXML::Document.new(open(uri).read) there_are_more = REXML::XPath.first(doc, "//IsTruncated").text == "true" # 构建一个 S3::Object对象的列表 objects = [] # 对于桶里的每个 S3对象... REXML::XPath.each(doc, "//Contents/Key") 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)T 请求,你可以得到该“桶

(bucket)”的一个表示,即一个像示例 3-8 那样的 XML 文档,其中的 Contents元素包

含该桶的

示例

006-03-01/">

</Key> 006-10-27T16:01:19.000Z</LastModified>

dce0def329cc7"</ETag>

/ID>

的一个表示(representation)。向一个“桶”资源的 URI 发送 GE

一些信息。

3-8:一个“桶”的表示

<?<Lixml version='1.0' encoding='UTF-8'?> stBucketResult xmlns="http://s3.amazonaws.com/doc/2

<Name>crummy.com</Name> <Prefix></Prefix> <Marker></Marker> <MaxKeys>1000</MaxKeys> <IsTruncated>false</IsTruncated> <Contents> <Key>mydocument<LastModified>2

<ETag>"93bede57fd3818f93ee <Size>22</Size> <Owner> <ID> c0363f7260f2f5fcf38d48039f4fb5cab21b060577817310be5170e7774aad70< <DisplayName>leonardr28</DisplayName> </Owner>

60

Page 14: 1242982622API2 upload

一个 S3 客户端 │

<StorageClass>STAN </Contents>

etResult>

DARD</StorageClass>

在本 档值得关注的部分是该桶的对象列表。一个对象(object)是由它的键(key)标识 息。另外,有一个布尔值

(“/ 否包括了桶中所有对象的键(key),S3有没

同样地, 表示里主 链接( )。该文档给出了许多有关对象的信息,但

它没 根据对象名称构造对象的 URI。好在构

造一 给出的规则就行了:https://s3.amazonaws.com/

{na

S3The S3 Object 现在我们要为 S3 服务的核心——S3 对象——实现接口了。记住,一个 S3 对象(object)只是一个具有名称(键)和一组元数 tent-Type="text/html")的数

据串。当你向桶列表(bucket list)或桶(bucket 送 GET 请求时,S3 会返回一个 XML文档。 PUT 的数

据串。

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

rized

个桶里

ue, metadata

I等于所在桶的 URI加上该对象的名称

</ListBuck

例中,该文

的,所以我用 XPath 表达式“//Contents/Key”来获取此信

/IsTruncated”)也值得关注,它表示文档里是

有因为太多了放不下而截短了。

这个 要缺少的就是 linkS3 假定客户端知道如何有给出对象的 URI。

个对象的 URI 并不难,参照我前面

me-of-bucket}/{name-of-object}。

对象

据键-值对(如 Con

)发

当你向一个对象发送 GET 请求时,S3 会逐个字节地返回你先前向该对象

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

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

# 客户端可以知道对象在哪 attr_reader :bucket # 客户端可以读写对象的名称

me attr_accessor :na # 客户端可以写对象的元数据和

# 稍后我将定义相应的“读”方法 r :metadata, :value attr_write

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

e, val @bucket, @name, @value, @metadata = bucket, nam end

UR # 对象的 def uri @bucket.uri + '/' + URI.escape(name) end

61

Page 15: 1242982622API2 upload

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

下面 取一个 S3 对象的元数据键-值对

(m 填充元数据 hash(store_metadata的实现在本类的 后)。

,并从响应的 HTTP begin store_metadata(open(uri, :method => :head).meta)

escue OpenURI::HTTPError => e if e.io.status == ["404", "Not Found"]

有元数据是因为对象不存在,这不算错误。 ta = {}

end @metadata

这段代码的作用是获取一个对象的元数据,而不必获取该对象本身。这就跟下载一则电影

要为流量付费的话,就更能体会到二

者的 on)的区别并非只在 S3 中有,它是

所有 的。HEAD 方法令任何客户端可以获取任一资源的元数据,

而不 个)的方法。

当然 身,那么就需要 GET 请求了。在示例 3-11 中,我在存取

方法 cessor method) 里使用了 GET 请求。这个方法的结构跟

S3:

示例 Object#value 方法

esponse = open(uri)

是我实现的第一个 HTTP HEAD 请求。它用于获

etadata key-value pairs),并由于我是用 rest-open-uri发送请求的,所以发送 HEAD 请求的代码看上去跟发送其他

HTTP 请求的代码差不多(见示例 3-10)。

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

# 获取对象的元数据 hash def metadata # 如果没有元数据... unless @metadata # 向对象的 URI发送一个 HEAD请求 报头里读取元数据。

r # 假如没 @metada else # 其他情况,作错误处理。 raise e end end return end

评论跟下载该电影本身的区别一样;而且,如果你需

差别了。元数据(metadata)与表示(representati面向资源的 Web 服务都有

获取其表示(可能不只一必

,有时你确实要下载电影本

(ac S3::Object#value

。:Object#metadata的差不多

3-11:用 Ruby 编写 S3 客户端:S3::

# 获取对象的值和元数据 def value

# 没有如果没有值... unless @value

向对象的 URI发送 GET请求 # r

62

Page 16: 1242982622API2 upload

一个 S3 客户端 │

# 从响应的 HTTP报头里读取元数据 store_metadata(response.meta) unless @metadata # 从实体主体里读取值 @value = response.read end return @value end

在 S S3 桶的原理一样:向一个 URI 发送 PUT 请求即可。对

于 S 建 较简单的,因为一个桶除了名称(已包含在 PUT请求 他属性。而对于 S3 对象(object)的创建,PUT 请求要复

杂一 e)和

值( 用 D 或 GE 来获取这些信息

好在 端可以决定对象的值,而

且不 状放入 PUT 请求的实体

主体 adta hash 里的各项元数据设置好 HTTP 报头(headers)就行 2)。

示例 S3 客户端:S3::Object#put 方法

保存对象 t(acl_policy=nil)

始,或者, 果没有元数据的话,就以空 hash开始。

e.size.to_s args[:body] = @value

S3: 示例 3-13)跟 S3::Bucket#delete一模一样。

示例 户端:S3::Object#delete 方法

# URI DELETE请求 open(uri, :method => :delete) end

3 服务上创建 S3 对象跟创建

,PUT 请求是比3 桶(bucket)的创

的 URI 里)以外,没有其

些,因为 户端 PUT 请求里指定对象的元数据(比如 Content-Typ

HEA T 请求 )。 HTTP 客 要在

我们之后可以

构造一个创建 S3 对象的 PUT 请求不是十分复杂,因为客户

需要把对象的值包装为 XML 文档或其他形式,只要把它按原

(entity-body)里,并按照 met

了(见示例 3-1

3-12:用 Ruby 编写

# 在 S3上 def pu

原始元数据的副本开 # 以 # 如 args = @metadata ? @metadata.clone : {} # 设置 HTTP方法、实体主体及一些另外的 HTTP报头 args[:method] = :put args["x-amz-acl"] = acl_policy if acl_policy if @value args["Content-Length"] = @valu

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

:Object#delete的实现(见

3-13:用 Ruby 编写 S3 客

# 删除对象 def delete

向对象的 发送

63

Page 17: 1242982622API2 upload

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

示例 ”的方法。你应该

为你 mz-meta-”,否则它

们被 eb 服务客户端——S3 服务器会认为它们是因为客

户端 弃它们。

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

new_metadata.each do |h,v|

['content-type', 'content-disposition', 'content-range', -amz-missing-meta']

对请求进行签名及访问控制 Request Signing and 我已 作一下讲解了。假如你主要关

心的 用 S3 客户端库”一节。不过

假如

用我 S3 后会被拒绝处理,因

为这 ization报头——这样,S3 就无法证实你是不是这

些桶 别忘了,你是要为你在 S3 服务上存放的数据及产生的流量向 Amazon 付费

的。 就执行的话,那么你可能要为别人在你的桶里存

放的

大多 证的 Web 服务,都采用标准的 HTTP 机制来核实你是否的确是自称的那个

使用。

S3 上,供人们用

BitTorrent 来下 你要

或者 对存放在 S3 上的电影文件的访问权。你的电子商务网站在收到一位

客户 。这意味着,你在授权他人“作特定 Web服务 。

3-14 显示的是一个用于“根据 HTTP 响应报头生成 S3 对象元数据

Content-Type 以外)添加前缀“x-a设置的所有元数据报头(除

发给 S3 服务器后,就不会再回到 W软件故障产生的,并丢

3-14:

private # 给定一个包含 HTTP响应报头的 hash, # 选取那些跟 S3对象相关的报头,然后把它们保存在实例变量@metadata里。 def store_metadata(new_metadata) @metadata = {}

if RELEVANT_HEADERS.member?(h) || h.index('x-amz-meta') == 0 @metadata[h] = v end end end RELEVANT_HEADERS =

'xend

Access Control 把关于 S3 认证的话题尽量延后了,现在是时候对它经

是 REST 式服务的概况,可以略过本节,直接跳到“使

你对 S3 的内部工作原理有兴趣的话,请继续阅读。

前面展示的代码是可以发出 HTTP 请求,不过这些请求到达

没有包含关键的 Author些请求里

主人。的

假如 S3 对操作桶的请求不加以认证

数据埋单。

数需要认

人。但 S3 的需求比较复杂。在大多数 Web 服务里,你绝不希望自己的数据被他人

但是 S3 的用途之一就是用于寄存服务。你也许会把一部电影寄存在

载(当然 为此付费)。

,你也许想出售

的付款后,把 S3 上的文件的 URI 告诉他

GET 请求)”的权利,而由你来付费调用(

64

Page 18: 1242982622API2 upload

对请求进行签名及访问控制 │

标准的 HTTP 认证机制无法为此种应用提供安全性支持。因为一般来说,标准的 HTTP 认

证机制需要让发送 HTTP 请求的人知道实际的密码。你可以防止别人窃取密码,但不能对

另一个人说“这是我的密码,但你必须保证你只能用它来请求这个 URI。”

在 S3 里,这个问题是用消息认证代码(Message Authentication Code,MAC)解决的。你

重要部分(比

如: 方法,以及一些 HTTP 报头)进行签名。因为只有知道密钥的人才能够

为请求生 ,所以 Amazon 由此可以确信应该向你收费。在对一个请求签名以

后, 密钥);该第三方由 名,所

以他 间内,

像你

要为 自

己的 以即使是像这样的一个简单的库,也必须支持对请求

进行 。我将对 S3::Authorized模块进行重新编写,为它增加一项功能:截取对

openS3:

功能 不为 S3::Authorized模块添加这个功能,那么我前面在各个类里定义的所有

open 调用,都将发送未签名的 HTTP 请求——S3 将对这些请求返回响应代码 403添加这个功能后,你就可以生成已签名的 HTTP

例 3-15 及后面一些示例

t-open-uri实现的 open()方法针对 S3作的一个封装。 # 该实现在发送请求之前进行一些 HTTP报头的设置,

-Type'] ||= ''

每次发送 S3 请求时,都用你的密钥(只有你和 Amazon 知道)来对请求中的

URI、HTTP成正确的签名

可以把该签名发给第三方(不必告诉他你的 于拥有你的签

费。总之,其他人可以在一定时可以发送签名的那个请求,并令 Amazon 向你收

而不必知道你的密钥。 一样发出特定的请求,

你的 S3 对象开通匿名访问权限,有一种较为简单的方法(后面我会谈到)。但是对

请求进行签名是无法避免的,所

签名才行

方法的调用,并在发出 HTTP 请求前对它进行签名。因为 S3::BucketList、

模块,所以它们将自动继承该:Buck

。假如

et 和 S3::Object 都已经包含(include)了这个

(“Forbidden”)。为 S3::Authorized模块

请求,这样就可以通过 S3 的安全策略了(并让你支付费用)。示

中的代码都相当依赖于 Amazon 自己的示例 S3 库。

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

module Authorized # 这些是 S3认为跟签名请求相关的标准 HTTP报头 INTERESTING_HEADERS = ['content-type', 'content-md5', 'date'] # 这些前缀用于自定义元数据报头。 # 所有自定义元数据报头都被认为跟签名请求相关 AMAZON_HEADER_PREFIX = 'x-amz-' # 为用 res

# 其中 重要的是 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

65

Page 19: 1242982622API2 upload

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

signed = signature(uri, headers_and_options[:method] || :get, headers_and_options) headers_and_options['Authorization'] = "AWS #{@@public_key}:#{signed}" Kernel::open(uri, headers_and_options, *args, &block) end

签名, 其中含有跟该请求相关的所有信息。

用 sign来构造规范化字符串的。

个 HTTP 请求转换成一个如

示例 具有特定的格式,其中含有(从 S3 的角度来看)跟

HTT TP 方法(PUT)、Content-type

(“t 径部

分(“ 这个字符串进行签名。任何人都知道如何

创建 知道如何生成正确的签名。

示例

bject.

Amazon 服务器收到你的 HTTP 请求后,它会根据请求也生成一个规范化字符串,并对它

进行签名(别忘了,Amazon 也知道你的密钥),然后 Amazon 服务器将比较两个签名,看

是否匹配。S3 的身份认证就是这样实现的。如果签名匹配,你的请求将被执行;否则,

你将得到响应代码 403(“Forbidden”)。

现在的一项艰巨任务是实现 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 ? "?" + query : "") end # 构造规范化字符串,并对它进行签名。 signed_string = sign(canonical_string(method, path, headers, expires)) end

注意,本方法是通过对 canonical_string的返回值调

我们来看一下这两个方法。先看 canonical_string,它把一

3-17 所示的字符串,该字符串

P 请求相关的所有信息。这些相关信息包括:HText/plain”)、日期、其他一些 HTTP 报头(“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 o/crummy.com/myobject

66

Page 20: 1242982622API2 upload

对请求进行签名及访问控制 │

示例

示例

HTTP请求的各个部分生成一个用于签名的字符串。

INTERESTING_HEADERS.each { |header| sign_headers[header] = '' }

# S3, value|

== 0

header] = value.to_s.strip

te报头,不过可能会有人设置这个报头。

== 0 canonical << value <<

后对 URI路径进行签名。我们去掉查询字符串(query string), 个专门的 S3查询参数:‘acl’、‘torrent’或‘logging’。

*$/, '')

.new("[&?]#{param}($|&|=)")

3-18 显示的是用于生成规范化字符串的代码。

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

# 根据 def canonical_string(method, path, headers, expires=nil) # 为所有相关报头设置默认值 sign_headers = {}

把实际的值(包括用于自定义 报头的值)复制进来

headers.each do |header if header.respond_to? :to_str header = header.downcase # 如果是一个自定义报头或 S3认为相关的报头...

INTERESTING_HEADERS.member?(header) || if_HEADER_PREFIX) header.index(AMAZON

里 # 把它加入到报头sign_headers[

end end end # 这个库不需要 Amazon定义的 x-amz-da # 假如设置了这个报头的话,我们就不采用 HTTP的标准 Date报头。

n_headers['date'] = '' if sign_headers.has_key? 'x-amz-date' sig # 如果提供了过期时间的话,那么它优先于其他 Date报头。 # 这个签名将在过期时间内都有效。 sign_headers['date'] = expires.to_s if expires # 现在,我们开始为该请求构造规范化字符串。我们从 HTTP方法开始。 canonical = method.to_s.upcase + "\n" # 把所有报头按名称排序,然后把这些报头(或仅仅是值)添加到将被签名的字符串里。 sign_headers.sort_by { |h| h[0] }.each do |header, value| canonical << header << ":" if header.index(AMAZON_HEADER_PREFIX)

"\n" end #

# 并附上一 canonical << path.gsub(/\?.

', 'torrent', 'logging'] for param in ['acl if path =~ Regexp canonical << "?" << param

67

Page 21: 1242982622API2 upload

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

break end

在 s 9)。

示例 Authorized#sign 方法

t::Digest.new('sha1') est_generator, @@private_key, str)

对Signing a URI 我的 库还有一个功能要实现。我已多次提到,S3 允许你在对一个 HTTP 请求签名后,

把 U signe

uri

这个方法 后它会给你一个签了名的 URI(别人可以 个 URI)。为防止滥

用,一 :expires 参数传进一个 Time

对象

示例

# HTTP 用的 URI,

expires] || (Time.now.to_i + (15 * 60)) es = expires.to_i if expires.respond_to? :to_i

adersmethod],

end # S3 开头吗?这里是它的结尾。

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

生成一个 URI。

end return canonical end

ign的实现里,实际起作用的是 Ruby 的标准加密与编码接口(见示例 3-1

3-19:用 Ruby 编写 S3 客户端:

# 用客户端的 Secret Access Key(即密钥)对一个字符串进行签名, # 并用 base64把签名后得到的二进制串编码为纯 ASCII码 def sign(str)

s digest_generator = OpenSSL::Digedig digest = OpenSSL::HMAC.digest(

return Base64.encode64(digest).strip end

URI 进行签名

S3RI 给其他人,这样他 以像你 请求了。实现这一目标需要用到们就可 一样发送 d_

这个方法(见示例 3-20)。不是用 open来发送 HTTP 请求,而是把 open 的参数传给

,然 像你一样用这

你可以通过为 个签了名的URI只在一定时间内有效。

来设置这个时间。

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

根据一个 请求的信息,返回一个你可以给别人

# 令他们可以像你一样发出特定的 HTTP请求。 # 该 URI将在你所指定的时间(默认为 15分钟)内有效。 def signed_uri(headers_and_opti expires = headers_and_options[:

ons={})

expir he _and_options.delete(:expires) signature = URI.escape(signature(uri, headers_and_options[: headers_and_options, nil)) q = (uri.index("?")) ? "&" : "?" "#{uri}#{q}Signature=#{signature}&Expires=#{expires}&AWSAccessKeyId=#{@@public_key}"

end end

还记得那个容纳所有代码的 模块的

68

Page 22: 1242982622API2 upload

对请求进行签名及访问控制 │

示例 签了名的 URI

e 'S3lib'

object = S3::Object.new(bucket, "KomodoDragon.avi")

# "https://s3.amazonaws.com/BobProductions/KomodoDragon.avi

KTJ8DG

这个 essKeyId)、

过期

电影 由我向 Amazon 支付。假如某个客

户对 任何部分作修改的话(比如他想下载另一部电影),S3 服务会拒绝他的要求。

也许有的 会把这个 URI 发给其他人用,不过 15 分钟后这个 URI 就失效了。

一个问题了。规范化字符串里通常含有 Date 报头的值,如此一来,

URI 时,它们的 Web 浏览器发出的日期报头的值肯定跟签名里的

Setting Access Policy 假如我想令一个对象可被公开访问的话,该怎么做呢?我想把一 件对外界开放,并让

Ama 设为一个很久远的日

子, 现办法:即允许匿名访问。

你可

未签

设置

这就 想令一个桶

或对

我的 把该值作为自定义 HTTP 请求报头 x-amz-acl的一部分来发送的。Amazon S3 会读取这个请求报头,并为桶或对象设置相应的访问规则。

示例 3 对象,任何人均可通过其 https://s3.

3-21:生成一个

#!/usr/bin/ruby1.9 igned-uri.rb # s3-s

requir bucket = S3::Bucket.new("BobProductions")

puts object.signed_uri

# ?Signature=J%2Fu6kxT3j0zHaFXjsLbowgpzExQ%3D 2"# &Expires=1162156499&AWSAccessKeyId=0F9DBXKB5274J

URI 将在 15 分钟内有效(默认值)。该 URI 中含有我的公共标识符(AWSAcc

时间( )及加密的签名( )。我的客户可以访问这个 URI,并下载Expires Signature

modoDragon.avi——由此产生的流量费用将文件 KoURI 里的

客户

你也许已经发现这里的

客户访问这个签过名的

不一样。这就是为什么在生成规范化字符串时,你要设置一个过期日期(expiration)而不

是请求日期的原因。回顾一下示例 3-18 中 canonical_string 的实现可以看到,过期日

期(假如有的话)是优先于 Date 报头的。

设置访问策略

些文

人的服务 可以把过期日期zon 来处理烦 器管理问题。嗯,其实我

并公开发布所有已签名的 URI。不过,还有一种更简单的实

以为桶(bucket)或对象(object)设置访问策略(access policy),告诉 S3 可以响应

amz-acl报头来名的请求。你可以在发送用于创建桶或对象的 PUT 请求时,附上 x-

访问策略。

是 Bucket#put和 Object#put方法的 acl_policy参数的作用。如果你

象成为公开可读或公开可写的,你就为 acl_policy参数设置适当的参数值就行了。

客户端是

3-22 所示的客户端创建了一个 S URIamazonaws.com/BobProductions/KomodoDragon-Trailer.avi来读取它。这里,我并

不是出售该电影文件,我只是把 Amazon 作为寄存服务使用,省去了自己做网站来存放这

些文件的麻烦。

69

Page 23: 1242982622API2 upload

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

示例 3-22:创建一个可公开读取的对象

b

.avi")

S3 支

private

public-write

你的对象或列出桶里的内容。

给桶或对象设置权限,还有细粒度的方法。我没有介绍这部分,假如你有兴趣,可以参阅

tting Access Policy with REST”部分。那里将向你介绍一个平行的附属

cket} 有一个影子资源(shadow resource) /{name-of- bucket}?acl,该影子资源对应于该桶的访问控制规则;同样地,每个对象 /{name-of-

端库

各行。

#!/usr/bin/ruby -w # s3-public-object.rrequire 'S3lib' bucket = S3::Bucket.new("BobProductions") object = S3::Object.new(bucket, "KomodoDragon-Trailerobject.put("public-read")

持下列四种访问策略。

这是默认的。只有用你的密钥签过名的请求才被接受。 public-read

可以接受未签名的 GET 请求。这意味着,任何人都可以下载一个对象或列出一个桶

里的内容。

可以接受未签名的 GET 和 PUT 请求。这意味着,任何人都可以修改一个对象或向桶

里添加对象。 authenticated-read

未签名的请求将被拒绝,但是用别的 S3 用户的密钥签名的读请求也将被接受。这就

是说,任何拥有 S3 账户的人都可以下载

S3 技术文档的“Se资源空间:每个桶 /{name-of-bu

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

Page 24: 1242982622API2 upload

用 ActiveResource 创建透明的客户端 │

示例 3-23:一个 S3 客户端的示例

#!/usr/bin/ruby -w # s3-sample-client.rb require 'S3lib' # 收集命令行参数 bucket_name, object_name, object_value = ARGV unless bucket_name puts "Usage: #{$0} [bucket name] [object name] [object value]"

S3::BucketList.new.get # GET / buckets.detect { |b| b.name == bucket_name }

uld not find bucket #{bucket_name}, creating it." S3::Bucket.new(bucket_name)

object.metadata['content-type'] = 'text/plain'

object.put # PUT /{bucket}/{object}

用 创建透明的客户端 ith ActiveResource 同的简单接口,那么定制一个适用于所有

urce 的 Ruby 库,它使得为某些种类的 Web服务编写客户端变得轻而易举。

exit end # 找到或创建桶 buckets = bucket = if bucket puts "Found bucket #{bucket_name}." else puts "Co bucket = bucket.put # PUT /{bucket} end # 创建对象 object = S3::Object.new(bucket, object_name)

object.value = object_value

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

ActiveResourceClients Made Transparent w既然所有 REST 式 Web 服务暴露的都是基本相

Web 服务的客户端并不是难事——不过这有点浪费了。你另外有两个方案可选:(1)用

WADL 文档(上一章介绍过,第 9 章将详细介绍)来描述服务,然后通过一个通用 WADL客户端来访问它;(2)有一个名为 ActiveReso

ActiveResource 用于访问那些暴露出关系数据库里的记录与数据的 Web 服务。WADL 可用

于描述几乎各类 Web 服务,但 ActiveResource 只能用于符合一定规则的 Web 服务。Ruby on

71

Page 25: 1242982622API2 upload

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

Rails 是目前唯一符合这些规则的框架。不过,一个服务只要通过跟 Rails 一样的 REST 式接 ActiveResource 客户端的请求了。

在本 source 客户端调用的公共 Web 服务还不多(我在附录 A中列 来创建一个简单的 Rails Web 服务。我将能够用 ActiveResource来调 解析代码。

创建Creating a Simple Service 我要 存带时间戳的笔记(notes)。因为 笔记本服务:

我在 编辑 Rails文件 Rails。任何 Rails的一 些初始 骤有 详

现在 了一个 Rails 应用,但是它还做不了任何事。我将用 scaffold_resource

生成 记(note)包含一个时间戳

(tio xt

v ews/note s/index.rhtml

t.rhtml t .rh l

app/controllers/notes_controller.rb e test/functional/notes_controller_test.rb

create app/helpers/notes_helper.rb

把那个接口暴露给外界;app/views/notes 里的视图部分(view)定义了用户界面。

口暴露出数据库,就能响应来自

书编写之时,可用 A了一些)。下面,我

ctiveRe出

必为此编写任何 HTTP 客户端或 XML用这个服务,而不

一个简单的服务

创建的是一个简单的笔记本(notebook)Web 服务:它能保

我的计算机上已经安装了 Rails 1.2,所以可以像下面这样来创建这个

$ rails notebook notebook $ cd

自己的计算机上创建了一个名为 elopment 的数据库,然后notebook_dev

/ atab se.yml,把连接notebook/config d a 这个数据库所需的信息提供给

都会对这 步 较 细的介绍。 般性指南

,我已经创建

器为一个简单的 REST 式 Web 服务生成代码。希望我的笔

mestamp)和一段文本(text),所以运行如下命令: $ ruby scrip / er e af ld_resource note date:date body:teeate app/ i s

t gen at sc fcrcreate app/views/notecreate app/views/notes/show.rhtml

.rhtml create app/views/notes/newicreate app/views/notes/ed

create app/views/layouts/no es tmcreate public/stylesheets/scaffold.css

app/models/note.rb create te crea

creat

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

72

Page 26: 1242982622API2 upload

用 ActiveResource 创建透明的客户端 │

一个 REST 式 Web 服务构建好了——它虽然功能不强,但用作示范足够了。

在启动该服务之前,我需要先初始化数据库:

-> 0.0119s == CreateNotes: migrated (0.0142s) ==================================

用,并开始使用我的服务了:

我刚刚创建的应用作为一个 功能并不强大,但它展示了一些印象深刻的特性。首先,

ht

$ rake db:migrate == CreateNotes: migrating =========================================== -- create_table(:notes)

现在,我可以启动笔记本应

$ 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 显示的是经过我的一系列操作之后,

tp://localhost:3000/notes 所呈现出的视图。

图 3

假如 o,那么你对这个例子应该比较熟悉。

不过在 也可以作为一个 REST 式 Web 服务——你可以

)中被开发。要获取其代码,需要从 Subversion 版本控

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

你曾经编写过 Rails 应用,或者看过

Rails 1.2 里,生成的模型和控制器

Rails 的 dem

用程序编写一个客户端,像 Web 浏览器那样简单地访问它。

可惜 ActiveResource 客户端本身没有随同 Rails 1.2 一起发布。在本书编写之时,它还在

Rails 的开发树(development tree制库里把代码 check out 出来:

$ svn co http://dev.rubyonrails.org/svn/rails/trunk activeresource_client $ cd activeresource_client

73

Page 27: 1242982622API2 upload

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

现在,准备工作已经完毕,我要开始为我的笔记本 Web 服务编写 ActiveResource 客户端

了。示例 3-24 是一个客户端,它先创建一则笔记,然后修改它,接着列出已有笔记,

示例

require 'activeresource/lib/active_resource'

# GET /notes.xml ote(s):"

# POST /notes.xml

new_note.body = "This note has been modified." new_note.save # PUT /notes/{id}.xml show_notes new_note.destroy # DELETE /notes/{id}.xml puts show_notes

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

ulation.rb 的一次运行

a

Rails 和数据库的对象关系型数据映射组件(object- relat多。 提供了一个面向对象的接口。对于 ActiveRecord,

后删除这则笔记。

3-24:一个用于笔记本服务的 ActiveResource 客户端

#!/usr/bin/ruby -w # activeresource-notebook-manipulation.rb require 'activesupport/lib/active_support'

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

def show_notes notes = Note.find :all puts "I see #{notes.size} n notes.each do |note| puts " #{note.date}: #{note.body}" end end new_note = Note.new(:date => Time.now, :body => "A test note") new_note.save

示例 3-25:activeresource-notebook-manip

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 book about REST? 2006-12-18: Pasta for lunch maybe?

如果你熟悉 ActiveRecord(用于连接

ional mapper))的话,你会注意到 ActiveResource 的接口看上去跟 ActiveRecord 差不

它们均为各种暴露统一接口的对象

74

Page 28: 1242982622API2 upload

用 ActiveResource 创建透明的客户端 │

对象在数据库里,通过 SQL(采用 SELECT、INSERT、UPDATE 和 DELETE 等)来访问

这些对象;而对于 ActiveResource,对象在 Rails 应用里,通过 HTTP(采用 GET、POST、PUT 和 DELETE)来访问这些对象。

tiveResource 客户端相关的记

录。 T 和 DELETE 请求分别与示例 3-24 里被注释的代码行对应。

示例 的 HTTP 请求

00

这些 什么?跟发给 S3 的请求一样,通过 HTTP 统一接口进行资源访问。我的笔

记本

它是一个对象列表);

的对象。

跟 S 样,这些资源也暴露 GET、PUT 和 DELETE。笔记列表还支持用 POST 来创

建一 是符

合 R

当客 是对

下层 单描绘,就像示例 3-27 或示例 3-28 那样。

示例

version="1.0" encoding="UTF-8"?>

ut REST?</body> <date type="date">2006-06-05</date>

示例

示例 3-26 是从 Rails 服务器日志里摘录的一段与运行我的 Ac其中的 GET、POST、PU

3-26:activeresource-notebook-manipulation.rb 发出

"POST /notes.xml HTTP/1.1" 201 PUT /notes/5.xml HTTP/1.1" 200 ""GET /notes.xml HTTP/1.1" 2"DELETE /notes/5.xml HTTP/1.1" 200 "GET /notes.xml HTTP/1.1" 200

请求做了

resource): 服务暴露了两种资源(

相当于 S3 里的桶(笔记列表(/notes.xml),

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

3 资源一

则新笔记——这跟 S3 有点不同(在 S3 里,对象 PUT 方法创建的),不

风格的。 是用 过这

EST

户端运行时,客户端与服务器之间的 XML 文档传递是透明的。这些 XML 文档

数据库记录的简

3-27:一个响应实体主体(对发给 /notes.xml 的 GET 请求的响应)

<?xml <notes> <note> <body>What if I wrote a book abo

<id type="integer">2</id> </note> <note> <body>Pasta for lunch maybe?</body>

<date type="date">2006-12-18</date> <id type="integer">3</id> /note> <

</notes>

3-28:一个请求实体主体(发给 /notes/5.xml 的 PUT 请求)

<?xml version="1.0" encoding="UTF-8"?> <note> <body>This note has been modified.</body> </note>

75

Page 29: 1242982622API2 upload

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

访问 ActiveResource 服务的 Python 客户端 A Python Client for the Simple Service 目前,Ruby 的 ActiveResource 库是唯一的 ActiveResource 客户端库,Rails 是唯一能暴露

用其他框架应该也能暴露同样的 URI。

在示例 3-29 中,我用 Python 实现了示例 3-24 所示的客户端程序。这个程序没有基于

Acti y 实现长一些。在这个 Python 实现里,它必

须自 但是其结构跟示例 3-24 所示的客户端程序是

基本

示例 3-29:用 Python 实现一个 ActiveResource 服 端

string from elementtree import ElementTree

import time

def showNotes():

print "%s: %s" % (note.find('date').text, note.find('body').text)

= time.strftime("%Y-%m-%d", time.localtime())

uest(BASE + "notes.xml", "POST",

ntent-type' : 'application/xml'})

") Element(modifiedBody, "body")

client.request(newURI, "PUT", ,

跟 ActiveResource 兼容的服务的框架。不过它只是发送一些传递 XML 文档的 HTTP 请求、

并获取返回的 XML 文档而已,用其他语言编写的客户端应该也能发送那些 XML 文档,

veResource,所以它要比示例 3-24 的 Rub,己来构造 XML 文档和发送 HTTP 请求

一样的。

务的客户

#!/usr/bin/python # activeresource-notebook-manipulation.py from elementtree.ElementTree import Element, SubElement, to

import httplib2

BASE = "http://localhost:3000/" client = httplib2.Http(".cache")

headers, xml = client.request(BASE + "notes.xml") doc = ElementTree.fromstring(xml) for note in doc.findall('note'):

newNote = Element("note") date = SubElement(newNote, "date")

ib['type'] = "date" date.attrdate.textbody = SubElement(newNote, "body") body.text = "A test note"

gnore = client.reqheaders, i body= tostring(newNote), headers={'conewURI = headers['location']

dy = Element("notemodifiedBobody = Subbody.text = "This note has been modified"

body=tostring(modifiedBody) headers={'content-type' : 'application/xml'})

() showNotes

76

Page 30: 1242982622API2 upload

后的话 │

client.request(newURI, "DELETE") print showNotes()

后的话 Parting Words 因为REST式Web服务具有简单和良定的(well-defined)接口,所以可以容易地做到克隆

一个REST式Web服务,或者为一个REST式Web服务替换实现。Park Place(http:// code.whytheluckystiff.net/parkplace)是一个具有跟S3 一样的接口的Ruby应用。你可以用Park Place 己的 。S3 的库和客户 样可用于访问Park Place的服务器,就

克隆 是为 Python 或其他动态语言编

写一 服

务编

现在 REST 式或 REST-RPC 混合服务(无论它提供 XML、HTML、JSON,

还是 就是 HTTP 请求和文档解析而已。

同时 !搜索服务)跟 RPC 式和混合服务(如 Flickr和 d 对其架构

的判 eb 也有“纹理”,

而 R

在接 S3(而不是像 del.icio.us API)一样的 REST 式

Web 在第 7 章,我们会把 del.icio.us 重新作为

RES

来提供自 S3 服务 端程序同

好比在访问https://s3.amazonaws.com/一样。

ActiveResource 也是可以的。虽然还没人这么做,但

个通用的 ActiveResource 客户端并不难。另一方面,为一个兼容 ActiveResource 的

客户端同样简单。 写一次性客户端,跟为其他 REST 式服务编写

,你应该对编写

某种混合)的客户端感到轻松了——它们

,你对 REST 式 Web 服务(如 S3 和 Yahooel.icio.us APIs)的区别也应该有所了解了。这不是对服务内容的评判,而是

进行得当的加工。W断。在对木材进行加工时,应顺应木材的纹理

EST 式 Web 服务正是一种吻合 Web 纹理的设计。

下来的章节中,我将教你如何创建像

服务。第 7 章之前,我都将围绕这一主题。

T 式 Web 服务来设计。

77