了解 Ruby 中的具象状态传输 (REST)

使用 Ruby 构建一个简单的 RESTful 客户端

REST(也称 Representational State Transfer)是一种分布式通信架构,很快就成为了云的通用语言。它极其简单,但具有很强的表示功能,可以表示大量的云资源和整体的配置与管理。了解如何使用 Ruby 从头开始开发一种简单的 REST 代理,从而了解其实现和用法。

M. Tim Jones, 独立作家, 顾问

M. Tim Jones 是一名嵌入式固件架构师,也是 Artificial Intelligence:A Systems Approach、GNU/Linux Application Programming(现已发行第二版)、AI Application Programming(第二版)和 BSD Sockets Programming from a Multilanguage Perspective 等书籍的作者。他的工程背景非常广泛,从对地同步宇宙飞船的内核开发到嵌入式系统架构设计与网络协议开发。Tim 居住在科罗拉多州 Longmont,他是 Intel 的一名平台架构师,也是一名作家。



2012 年 9 月 20 日

具象状态传输 (Representational State Transfer, REST) 是一种架构风格,用于基于 Web 的通信,允许客户端以一种独特的方式与服务器进行通信。特别是,REST 可以将一个给定服务器内的资源表示为一种统一资源标识符 (uniform resource identifiers, URIs),这就简化了 REST 架构在超文本传输协议 (Hypertext Transport Protocol, HTTP) 上的实现。让我们首先介绍一下 REST 和 HTTP 背后的理念。随后我们将探讨数据表示法,然后使用 Ruby 语言实现一种简单的 REST 客户端。

HTTP 简单介绍

让我们首先简单介绍一下 HTTP,因为这对于理解单个 REST 事务很重要。尽管 HTTP 是连接 Web 浏览器和各个服务器的基本通信协议,它也是除了 HTML 之外传输各种类型数据的一种有用的协议。

HTTP 是一种请求和响应协议,也就是说,客户端发出请求,服务器使用一个响应来满足这些请求。用于 HTTP 的实际协议对于人具有很强的可读性,为了演示 HTTP,我使用 Telnet 向 Web 服务器发出请求。

从浏览器发出的请求

请注意,在 清单 1 中,我手动发出的请求将被指定为 URI http://www.mtjones.com/index.html。Web 浏览器会分解该 URI,以便用于特定请求中(如 清单 1 所示)。

清单 1 提供了一个 HTTP 请求和一个来自 Web 服务器的部分响应。通过指定 Web 服务器域名和端口(80 是典型的 HTTP 端口),我使用 Telnet 启动了该请求。Telnet 首先使用域名的 Domain Name System 解析对 IP 地址作出响应,随后表明我已链接到 Web 服务器。然后,我指定了一个请求行(包含我的 HTTP GET 方法、一个连接到 Web 服务器上资源的路径,以及我将使用的协议(在本例中,该协议是 HTTP v1.1)。接下来,我提供一组请求标头(这组标头可能非常大,但因为我要将其键入,所以只指定 Host 请求标头,该标头表明了主机和我发出请求的可选端口)。请求后面紧跟着一个空白行,告诉 Web 服务器我的请求已经完成。Web 服务器随后提供一个响应,指明所使用的协议,并提供一个状态代码(在本例中是 200 OK,表示一个良好的请求)和其他的响应标头。随后出现一个空行,空行后面是 1944 个字符的响应。这就是资源的表示法(在本例中是一个 HTML 文档)。

清单 1. 使用 Telnet 执行一个 HTTP 事务
$ telnet mtjones.com 80
Trying 198.145.43.103...
Connected to mtjones.com.
Escape character is '^]'.
GET /index.html HTTP/1.1Host: example.org

HTTP/1.1 200 OK
Date: Sun, 25 Mar 2012 05:33:07 GMT
Server: Apache
Last-Modified: Sat, 26 Sep 2009 20:22:36 GMT
ETag: "2c984bf-798-d3451b00"
Accept-Ranges: bytes
Content-Length: 1944
Vary: Accept-Encoding
Content-Type: text/html

<DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" ...

本示例阐述了一个简单的事务,但是在 HTTP 中已实际实现了多种方法。GET 方法用于检索 Web 服务器中的资源,其中 HEAD 只用于获取资源的元信息(不是实际内容)。POST 方法用于向 Web 服务器提供新内容,PUT 方法用于放置 Web 服务器上现有资源的数据。表 1 提供了 HTTP 方法的完整列表。

表 1. 常见的 HTTP 1.1 请求方法
方法幂等描述
OPTIONS请求有关通信选项的信息
GET检索 URI 的表示法
HEAD检索 URI 的元信息
PUT使用一个表示法创建或取代资源
POST使用一个表示法扩大现有资源
DELETEYes删除 URI 指定的资源

在这个对 HTTP 的简短探讨中,值得注意的是它是一个在资源上支持基本操作的协议。尽管 HTTP 如今常用于在服务器和客户端之间传输 Web 内容,它还是一种不断发展的协议,可用于分布式系统交互,以及用于允许异构系统进行通信和共享数据的应用程序编程接口 (APIs) 的开发。

现在,让我们使用协议堆栈来探讨 REST 层。


什么是 REST?

REST 的力量

REST 是一种架构风格,而不是一种协议或是实现。REST 拥有一些核心原则,但归根结底,它是一个抽象概念,而不是一个具体的实现。

REST 更像是一种架构风格,而不是特定的设计或是实现。RESTful 架构是由一组简单的约束条件的定义,如 图 1 所示。在 RESTful 架构的核心部位是一组资源。这些资源由 URI(比如一个 Uniform Resource Locator [URL])和内部表示法(通常为一种自描述形式的数据,稍后您将进行简短讨论)来识别。最后,还有一组操作,您可以使用这些操作来处理资源。

图 1. RESTful 架构的高级视图
该图显示了 RESTful 架构的高级视图

在更具体的术语中,这些资源可以表示各种类型(比如 JavaScript Object Notation [JSON])的数据对象的使用。您可以通过 URL(比如 http://www.mtjones.com)使用一组标准的操作(HTTP GETPOSTDELETE 等)来处理这些资源。将 HTTP 用作一种传输方式可以极大地简化 RESTful 架构的开发,因为他们使用的是一种众所周知且稳定的基础协议。HTTP 还可广泛使用,且不需要使用新的配置,包括 Internet 服务(比如网关、代理、安全执行实体和 HTTP 缓存服务)。要使 REST 服务器高度可扩展,您可以利用其他有用的功能,比如负载均衡。


RESTful 架构的特征

尽管 RESTful 架构在其实现中具有相当大的自由性,但其中有一组特征还是很重要的。

REST 定义客户端-服务器架构,其中客户端可通过由服务器导出的表示法来访问服务器资源。客户端不会直接访问资源,而是通过一个统一的接口使用资源表示法进行访问。正如许多客户端-服务器架构一样,REST 也实现为一种分层架构,允许其使用低层(HTTP 负载均衡等)提供的各种功能。

然而,RESTful 架构的一个关键方面是它们是无状态的。服务器无法保存事务之间的任何客户端上下文,而且每一次事务必需包含满足特定请求所需的所有信息。此特征常常使得 RESTful 架构更可靠,还有助于增加它们的可扩展性。


样例 REST 接口

让我们来看一个样例 REST 实现,介绍 RESTful 架构的一些特征。切记 REST 依赖于客户端-服务器交互(参见 图 2)。客户端应用程序发出一个请求,并转换成一个 RESTful HTTP 请求。像发起其他 HTTP 事务一样从客户端到服务器发起该请求。服务器会处理该请求,并作出适当的响应。

图 2. RESTful 交互的分层架构
该图显示了 RESTful 交互的分层架构

您可以使用 REST API 来构建一个简单客户端的有趣示例就是 CrunchBase。CrunchBase 是一个与技术相关的企业、个人和投资者的免费数据库。除了提供传统的 Web 前端外,CrunchBase 还在 HTTP 之上提供基于 REST/JSON 的接口。

CrunchBase 通过其 API 实现了三种操作:

  • 展示(检索特定实体上的信息)
  • 搜索(检索与给定搜索标准匹配的实体列表)
  • 列出(检索给定名称空间内的所有实体)

CrunchBase 还导出了用于其数据的五个名称空间(参见 图 3),以及用于 CrunchBase REST 交互的 URL 形式。(名称空间有企业、个人、产品、金融组织和服务提供商)。请注意,/v/1 表示 API 的版本,通常为 1。还需注意的是永久链接字段,该字段表示各个实体在其数据库内的惟一名称。

图 3. CrunchBase API 中的名称空间
该图显示了 CrunchBase API 中的名称空间

如果您想获取有关 IBM 的最新信息,可以使用 Company 名称空间来构造一个 URL(在您的浏览器中试试这个):

http://api.crunchbase.com/v/1/company/ibm.js

您可以将该 URL 键入到浏览器中,浏览器将为您呈现文本(基于 JSON)响应(同时使用 HTTP 标头)。当探讨 CrunchBase 的 JSON 格式数据表示法时,请查看更多相关的详细资料。


自描述数据简介

在各个异构系统之间的通信引入了一些有趣的问题,其中一个就是用于传输的数据序列化。机器以各种方式(从不同的浮点表示法到标准的字节次序冲突)来表示数据。早期实现包括抽象语法表示法 (Abstract Syntax Notation, ASN.1) 格式和外部数据表示 (External Data Representation, XDR) 协议(在 Network File System 内使用)。其他方法包括 XML,此 XML 在 ASCII 格式化的文档中对数据进行编码。

在过去的六年中,JSON 格式已经备受瞩目。正如其名称所示,JSON 来源于 JavaScript 语言,且用于表示自描述数据结构(比如关联数组)。撇开其名称,JSON 还是一种常见的数据交换格式,支持多种语言。它同样对读取无关紧要。

现在,看一个 JSON 示例,尤其是通过 CrunchBase REST 接口的一个示例。该例使用了交互式的 Ruby shell (irb),允许您实时试用 Ruby。

清单 2 所示,从执行交互式 Ruby shell 开始。通过加载一些模块(尤其是 JSON 和 HTTP 组件),以及定义自己的 URL 来准备您的环境。此处需要注意的是,URI 是一个完整的 CrunchBase 请求(在 Company 名称空间中,使用 ibm 的永久链接)。您将此提供给 Net::HTTPget_response 方法,Net::HTTP 是在指定 URL(通过 URI.parse 方法解析为其单个组件)上执行 GET 请求的一种快捷方式。如果发出了 resp.body,您就可以看到返回的 JSON 数据。该数据包括一组名称/值对(比如 “name” 和 “IBM”)。使用 JSON.parse 方法将响应结果解析成一个 Ruby 对象结构。最后,您可以通过指定值的名称来提取特定的值。

清单 2. 使用 Ruby 与 CrunchBase 进行交互
$ irb
irb(main):001:0> require 'rubygems'
=> true
irb(main):002:0> require 'json'
=> true
irb(main):003:0> require 'net/http'
=> true
irb(main):004:0> uri = "http://api.crunchbase.com/v/1/company/ibm.js"
=> "http://api.crunchbase.com/v/1/company/ibm.js"
irb(main):005:0> resp = Net::HTTP.get_response(URI.parse(uri))
=> #<Net::HTTPOK 200 OK readbody=true>
irb(main):006:0> puts resp.body
{"name": "IBM",
 "permalink": "ibm",
 "crunchbase_url": "http://www.crunchbase.com/company/ibm",
 "homepage_url": "http://www.ibm.com",
 "blog_url": "",
 "blog_feed_url": "",
 "twitter_username": "",
 "category_code": "software",
 "number_of_employees": 388000,
...
=> nil
irb(main):007:0> parsedresp = JSON.parse(resp.body)
=> {"updated_at"=>"Wed Feb 01 03:10:14 UTC 2012", "alias_list"=>nil, 
...
irb(main):008:0> 
irb(main):009:0* puts parsedresp['founded_year']
1896
=> nil
irb(main):010:0>

清单 2 中,您可以看到,将从一个 JSON 响应(7 个行)中快速提取数据进行原型化是很简单的一件事。现在,继续采取这一步骤构建一个简单、可重用的 API 以便与 CrunchBase 进行交互。


构建一个简单的 REST 客户端

构建 REST 客户端之前,必需安装一些内容。如果还没有安装 Ruby,那么就先安装。因为我使用的是 Ubuntu,所以我使用 Advanced Packaging Tool(其次使用 Ruby gem 包管理器)来满足大多数安装需求。

使用下列代码捕获 Ruby 包:

$ sudo apt-get install ruby

或者,捕获 Interactive Ruby Shell (irb),这是试用 Ruby 语言的一种有用方式:

$ sudo apt-get install irb

最后,您需要对 Ruby 使用 JSON gem。下列代码展示了如何同时获取 gem 前端和 JSON gem。

$ sudo apt-get install rubygems1.8
$ sudo apt-get install ruby-dev
$ sudo gem install json

准备好环境之后,开始使用 Ruby 构建简单的 REST 客户端 API。在 清单 1 中,您可以看到如何使用 Ruby 与一个 HTTP 服务器进行通信,以及如何从 JSON 对象中解析一个简单的名称。以这种知识为基础,使用一组 Ruby 类来实现一个简单的 API。

首先,考虑使用与 CrunchBase REST 服务器交互的两个样例类。第一个类专注于 Company 名称空间;第二个类专注于 Person 名称空间。请注意,这两个类都将扩展方法的最小集,但对其他数据元素来说也很轻易进行扩展。

清单 3 提供了 API,以便与 Company 名称空间进行交互。该类扩展了构造函数方法 (initialize),以及用于从基于 JSON 企业记录中提取数据的四种方法。该类的操作原理是:用户创建一个对象实例,从而提供了一个企业(永久链接)名称。作为构造函数的一部分,CrunchBase 用于请求企业记录。您可以动态构造 URL,这就将所传递的企业名称添加为 new 方法的一部分。get_response 方法用于检索响应,并将解析结果(一个散列对象)加载到一个实例变量 (@record) 中。

解析记录可用之后,每种方法在调用时只提取所需的数据并将其返回给用户。founded_yearnum_employeescompany_type 方法直观明了,前提是具有来自 清单 1 的讨论。people 方法需要稍作解释。

来自 CrunchBase 的 JSON 响应是由一个包含一些密钥的散列来表示。请注意,在前三个方法中,您指定了返回值的密钥。people 方法对由密钥 relationships 识别的散列进行迭代。每个记录包含一个 is_past 密钥(用于确定 Person 是否已经不在某个 Company 中工作)、一个 title 密钥,以及一个 Person 密钥,Person 密钥包含 first_namelast_namepermalink。迭代程序仅仅是遍历每一个密钥,当密钥 is_past 错误时,提取 person 和 title,并从该信息中创建一个新的散列。当找不到更多密钥时,则会返回该散列。请注意,您只需发出 JSON.parse 响应即可查看 IRB 中的全部散列。

清单 3. Company 名称空间 (company.rb) 的 Ruby CrunchBase API
require 'rubygems'
require 'json'
require 'net/http'

class Crunchbase_Company

  @record = nil

  def initialize( company )

    base_url = "http://api.crunchbase.com"
    url = "#{base_url}/v/1/company/#{company}.js"

    resp = Net::HTTP.get_response(URI.parse(url))

    @record = JSON.parse(resp.body)

  end

  def founded_year
    return @record['founded_year']
  end

  def num_employees
    return @record['number_of_employees']
  end

  def company_type
    return @record['category_code']
  end

  def people

    employees = Hash.new

    relationships = @record['relationships']

    if !relationships.nil?

        relationships.each do | person |
            if person['is_past'] == false then
                permalink = person['person']['permalink']
                title = person['title']
                employees[permalink] = title
            end
        end

    end

    return employees

  end

end

清单 4清单 3 中所讨论的 Company 类提供了一个类似的函数。initialize 构造函数以同样的方式进行操作,而且对于提取给定的 Person(从他或她的永久链接中)名称,您拥有两种简单方法。companies 方法迭代用于 relationships 密钥的散列,并将该特定个人过去与之关联的公司(企业)返回,在这种情况下,关联的企业作为一种简单的(永久链接的)Ruby 数组返回。

清单 4. Person 名称空间 (person.rb) 的 Ruby CrunchBase API
require 'rubygems'
require 'json'
require 'net/http'

class Crunchbase_Person

  @record = nil

  def initialize( person )

    base_url = "http://api.crunchbase.com"
    url = "#{base_url}/v/1/person/#{person}.js"

    resp = Net::HTTP.get_response(URI.parse(url))

    @record = JSON.parse(resp.body)

  end

  def fname
    return @record['first_name']
  end

  def lname
    return @record['last_name']
  end

  def companies

    firms = Array.new

    @record['relationships'].each do | firm |

        firms << firm['firm']['permalink']

    end

    return firms

  end

end

请注意,在这两个简单的类中,Ruby 隐藏了处理 Net::HTTP 类中的 HTTP 服务器的复杂性。解析 JSON 响应的复杂性已通过 JSON gem 完全简化。现在,让我们来考虑使用这些 API 中的一对应用程序来演示其用法。


构建一些简单的应用程序

先从类演示开始。在第一个示例(参见 清单 5)中,您想要确定与给定 Company 有关联的 People(基于企业的永久链接)。从 Company 名称空间检索到的记录包含与该 Company 所关联的 People 的永久链接列表。对该 People 散列进行迭代,并根据该永久链接检索 People 的记录。该记录为您提供了 People 的第一个和最后一个名称,以及他或她的头衔也属于 Company 记录(作为 name-title 散列的一部分返回)。

清单 5. 识别与 Company 关联的 People (people.rb)
#!/usr/bin/ruby

load "company.rb"
load "person.rb"

# Get argument (company permalink)
input = ARGV[0]

company = Crunchbase_Company.new(input)

people = company.people

# Iterate the people hash
people.each do |name, title|

    # Get the person record
    person = Crunchbase_Person.new( name )

    # Emit the name and title
    print "#{person.fname} #{person.lname} | #{title}\n"

end

people = nil
company = nil

执行 清单 5 中的脚本,如 清单 6 所示。有了该脚本,您可以提供一个 Company 永久链接,并且结果是个人的第一个和最后一个名称与头衔。

清单 6. 测试 people.rb 脚本
$ ./people.rb emulex
Jim McCluney | President and CEO
Michael J. Rockenbach | Executive Vice President and CFO
Jeff Benck | Executive Vice President & COO
$

现在,看看一个更复杂的示例。该示例采用了一个给定的 Company,随后确定了执行主管。然后,确定这些主管过去工作过的企业。清单 7 提供了一个名为 influence.rb 的脚本。如清单所示,您接受企业名称作为参数,检索企业记录,并随后检索了该企业中当前的 People 散列(通过 People 方法)。然后,对这些 People 进行迭代并通过他们的头衔(并不完全准确,需要考虑到 CrunchBase 中头衔的可变性)来确定主管。对于任何已经确定的主管来说,您发出了这些个人曾经工作过的企业(通过从 People 记录中检索一个企业数组来完成)。

清单 7. 执行关系和影响 (influence.rb)
#!/usr/bin/ruby

load "crunchbase.rb"

input = ARGV[0]

puts "Executive Relationships to " + input

company = Crunchbase_Company.new(input)

people = company.people

# Iterate through everyone associated with this company
people.each do |name, title|

    # Search for only certain titles
    if title.upcase.include?("CEO") or
       title.upcase.include?("COO") or
       title.upcase.include?("CFO") or
       title.upcase.include?("CHIEF") or
       title.upcase.include?("CTO") then

        person = Crunchbase_Person.new( name )

        companies = person.companies

        companies.each do | firm |
            if input != firm
                puts "  " + firm
            end
        end

    end

end

清单 8 为大数据企业 Cloudera 演示了这个脚本。正如您从 清单 7 所看到的,Ruby 和其 gem 对您隐藏了许多细节,这使得您能够专注于手头的任务。

清单 8. 测试 Ruby 脚本的影响
$ ./influence.rb cloudera
Executive Relationships to cloudera
  accel-partners
  bittorrent
  mochimedia
  yume
  lookout
  scalextreme
  vivasmart
  yahoo
  facebook
  rock-health
$

使用其他 REST HTTP 方法

在此处所示的简单示例中,您只使用了 GET 方法,用于从 CrunchBase 数据库中提取数据。其他站点可能会将界面扩展为可以从其特定的 REST 服务器中检索数据并向其发送数据。清单 9 提供了其他 Net::HTTP 方法的简介。

清单 9. 用于其他 RESTful 交互的 HTTP 方法
http = Net::HTTP.new("site url")

# Delete a resource
transaction = Net::HTTP::Delete.new("resource")
response = http.request(transaction)

# Post a resource
resp, data = Net::HTTP.post_form( url, post_arguments )

# Put a resource
transaction = Net::HTTP::Put.new("resource")
transaction.set_form_data( "form data..." )
response = http.request(transaction)

要想进一步简化 REST 客户端的开发,您可以使用其他的 Ruby gem,比如 rest-client(参阅 参考资料)。该 gem 提供了一个 HTTP 上的 REST 层,从而提供了诸如 getpostdelete 之类的方法。


结束语

衷心希望有关 REST 原理和 Ruby 的这篇简单介绍能够展示基于 Web 架构的力量,并说明 Ruby 成为我最喜欢语言之一的原因。REST 是用于计算和存储云的最受欢迎的 API 架构之一,因此值得去了解和探讨。在 参考资料 中,您将会找到有关本文所用技术的其他信息的链接,以及有关 developerWorks 的其他 REST 文章。

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source, Linux
ArticleID=834300
ArticleTitle=了解 Ruby 中的具象状态传输 (REST)
publish-date=09202012