理解面向 HTTP API 的 REST 和 RPC

在过去的这几年当中,当人们想要构建一个 HTTP API,在诸如 XML-RPC、SOAP以及 JSON-RPC 这些选项之中,几乎都会选择 REST 作为首选的架构风格。REST 的出现最终被认为优于其它的“基于 RPC”的方式,这其实是一种无解,它们只是不同而已。

本文讨论构建 HTTP API 的场景中的两种方法, 因为这两种方法最常被用到。REST 和 RPC 都可以被其他的传输协议使用,比如 AMQP,不过那完全是另外一个话题了。

REST 表示的是“描述性状态传递(representational state transfer),” Roy Fielding 在他的论文做了如是描述。可悲的是,那篇论文并没有多少人读过,许多人对 REST 是什么都有自己的简介,这就导致许多的混乱和分歧。REST 整个就是关于 客户端和服务端之间的关系的,其中服务端要提供格式简单的描述性数据,常用的是 JSON 和 XML。对这些资源或者资源集合的描述,有些也许是可以通过一种叫做超媒体的方法所发现的动作和关系来修改的。 超媒体是 REST 的基础,它本质上就是一个向其它资源提供链接的概念。

除了超媒体之外,还有其它的一些约束,如下:

  • REST 必须是无状态的:请求之间不能对会话进行持久化。

  • 响应消息应该对可缓存性进行声明: 如果客户端遵守这个规则就能帮助你进行 API 扩展。

  • REST 专注于统一性: 如果你使用的是HTTP,就应该在哪儿都尽可能使用 HTTP 的功能,而不是再弄一些约定出来。

这些约束 (还有另外一些) 让 REST 架构能帮助 API 持续使用几十年,而不是几年。

在 REST 流行 (在诸如 Twitter 和 Facebook 这样的公司将它们的 API 称作 REST 以后)之前, 大多数 API 都是使用 XML-RPC 或者 SOAP 构建的。XML-RPC 是有毛病的,因为要确定 XML 的数据类型,开销是比较大的。在 XML 中,许多东西都只是字符串而已,因此你需要在顶部有一层元数据来描述诸如那一个域对应哪一种数据,之类的事情。这成为SOAP(简单对象访问协议 Simple Object Access Protocol)基本的组成部分。XML-RPC 和 SOAP, 以及自定义的本土解决方案主宰了 API 领域很长一段时间,而它们都是基于 RPC 的 HTTP API。

其它翻译版本(1)

“RPC”指的是“远程过程调用(remote procedure call)” ,本质上在JavaScript、PHP、Python等等中调用都是一样的:取方法名,传参数。因为不是每个人都喜欢XML,RPC API可以使用 JSON-RPC协议,也可以考虑自定义基于JSON的API,像Slack 是用它的Web API实现的。

以这个RPC调用为例:

POST /sayHello HTTP/1.1
HOST: api.example.com
Content-Type: application/json

{"name": "Racey McRacerson"}

JavaScript也是如此,定义一个函数,然后在其他地方调用:

/* 签名 */
function sayHello(name) {
  // ...
}

/* 用法 */
sayHello("Racey McRacerson");

想法一样,定义公共方法建立API接口,然后这些方法传入参数被调用。RPC就是一堆功能,但在一个HTTP API的上下文里,需要把方法放在URL中,同时将参数放在查询串或请求块(body)中。SOAP访问一个大同小异的数据时,就像报告一样,异常冗长。如果你在Google中搜索“SOAP案例”,你会找到下面类似,名为getAdUnitsByStatement的案例:

<?xml version="1.0" encoding="UTF-8"?><soapenv:Envelope
        xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <soapenv:Header>
    <ns1:RequestHeader
         soapenv:actor="http://schemas.xmlsoap.org/soap/actor/next"
         soapenv:mustUnderstand="0"
         xmlns:ns1="https://www.google.com/apis/ads/publisher/v201605">
      <ns1:networkCode>123456</ns1:networkCode>
      <ns1:applicationName>DfpApi-Java-2.1.0-dfp_test</ns1:applicationName>
    </ns1:RequestHeader>
  </soapenv:Header>
  <soapenv:Body>
    <getAdUnitsByStatement xmlns="https://www.google.com/apis/ads/publisher/v201605">
      <filterStatement>
        <query>WHERE parentId IS NULL LIMIT 500</query>
      </filterStatement>
    </getAdUnitsByStatement>
  </soapenv:Body></soapenv:Envelope>

这样的数据载荷相当庞大,如包装为参数仅仅就一行:

`<query>WHERE parentId IS NULL LIMIT 500</query>`

在JavaScript中, 看起来会是这样:

/* 签名 */
function getAdUnitsByStatement(filterStatement) {
  // ...
};

/* 用法 */
getAdUnitsByStatement('WHERE parentId IS NULL LIMIT 500');

JSON API会更加简洁, 或许看起来像这样:

POST /getAdUnitsByStatement HTTP/1.1
HOST: api.example.com
Content-Type: application/json
{"filter": "WHERE parentId IS NULL LIMIT 500"}

尽管此负载更简单,但我们还是需要不同的方法来处理getAdUnitsByStatement 、 getAdUnitsBySomethingElse。当你看这样的案例时,REST很快就能适应,因为它允许泛型端点与查询字符( 例如,GET /ads?statement={foo}orGET /ads?something={bar})联合查询。你可以联合字符去获取GET /ads?statement={foo}&limit=500,然后就可以考虑抛弃以前看似SQL语法的那种参数了。

目前,REST看似更好,但只是因为这些使用RPC处理的服务是REST更擅长处理的。这篇文章将不再尝试阐述什么”更好“,但取而代之的是帮你做出非正式的决定,以决定一个方案何时更合适。

它们是什么?

基于 RPC 的 API  适用于动作(过程、命令等)。

基于 REST 的 API  适用于领域模型(资源或实体),基于数据的 CRUD (create, read, update, delete)  操作。

REST 不 仅 用于 CRUD,但主要是基于 CRUD 的操作。REST 会使用 HTTP 方法,如 GET,POST,PUT,DELETE,OPTIONS 以及很有希望的 PATCH 来从语义上说明动作的意图。

然而 RPC 不会这么干。它多数时候只使用 GET 和 POST。GET 用于获取信息,POST 用来干其它事情。RPC API 通常会使用像 POST /deleteFoo 这样的方法,带上内容 { "id": 1 }。如果用 REST,就会像这样 DELETE /foos/1。

这并不是一个重要的区别,它只是一个简单的细节上的实现。我认为如何处理动作才是应该关注的区别。在 RPC 中,只需要 POST /doWhateverThingNow,这很简洁。但是用 REST 使用类似 CRUD 的操作会让你觉得 REST 不适合干除 CRUD 之外的事情。

当然,不能以偏概全。两种方法都可以触发动作。REST 的触发可以想像成是结果导向的,比如,如果你想向用户 “发送消息”,用 RPC 会这样做:

POST /SendUserMessage HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"userId": 501, "message": "Hello!"}

而在 REST 方法中,做同样的事情会这样:

POST /users/501/messages HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"message": "Hello!"}

虽然它们看起来很相似,但这里确实有一个概念上的区别:

  • RPC

发送一个消息,然后可能会往数据库里保存一些东西以作为历史记录,有可能另一个 RPC 会在这里请求同一个数据——谁知道呢?

  • REST

在用户消息集中创建一个消息资源,如果用同一个 URL 的 GET 请求,可以看到之前的用户消息,消息将在后台发送。

这里 “事后发生的行为” 在 REST 可以干很多事情。想像一个拥有“旅途”的拼车App。那些旅途需要“开始”、“结束”和“取消”动作,否则用户就无法知道它们何时开始,何时结束。

使用 REST API,你已经拥有 GET /trips 和 POST /trips,然后很多人会尝试使用有点像下面这些动作的节点:

  • POST /trips/123/start

  • POST /trips/123/finish

  • POST /trips/123/cancel

这些节点基本上就是在 REST API 中混入了 RPC 风格,这是流行的解决方案,但是从技术上来说并不是 REST。这个现象展示了将行为放在 REST 中的难处——看起来并不明显,但却是可能的。有一种方法是使用状态机,比如使用 state 字段:

PATCH /trips/123 HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"status": "in_progress"}

像其它字段一样,你可以给 status PATCH 一个新值,在后台启动一些重要的行为:

module States
  class Trip
    include Statesman::Machine

    state :locating, initial: true
    state :in_progress
    state :complete

    transition from: :locating, to: [:in_progress]
    transition from: :in_progress, to: [:complete]

    after_transition(from: :locating, to: :in_progress) do |trip|
      start_trip(trip)
    end

    after_transition(from: :in_progress, to: :complete) do |trip|
      end_trip(trip)
    end
  end
end

Statesman 是 Ruby 中一个简单的状态机,它由 GoCardless 团队开发。有各种不同语言的许多状态机实现,但对于用作演示来说,它是最简单的一个。

基本上在你的控制器中,libcode 或 DDD 逻辑的某处,你可以在 PATCH 请求中找到“status”,然后,可以尝试转变这个状态:

`resource.transition_to!(:in_progress)`

这段代码执行的时候,它会进行转变,并且运行定义在 after_transitionblock 中的任何逻辑。如果不成功,就抛出错误。

成功的动作可能是任何东西:发送邮件、推荐提醒、调用另一个服务以开始监控驾驶员的 GPS 所代表的汽车位置——什么都行,只要你喜欢。

RPC 方法 POST /startTrip 或者像 REST 的 POST /trips/123/start 节点都不再需要,因为只需要简单的处理就可以遵循 REST API 的约定。

当行为不事后发生时

我们已经看到了两种适应REST API行为,而不破坏其RESTful的方法,但是依赖于API被构建的应用程序的不同,这些方法可能会让人觉得越来越缺少逻辑,像在转圈圈。有人开始怀疑,为什么我要试图将所有的行为都使用REST API来实现?RPC API或许能对现有的REST API进行更好地补充。我们可以相对放宽使用RPC的Web API的条件,因为它只用于不适合使用REST API的情况。设想一下,我们为用户提供“tick”,“ban”或者“leave”选项,让用户只使用REST进行保留操作,或者从一个频道或者整个Slack中进行移除操作,如下:

DELETE /users/jerkface HTTP/1.1
Host: api.example.com

刚开始,DELETE似乎最适用的HTTP方法,但是这个需求却相对模糊。它可能意味着完全关闭用户账户,也可能只是禁用这个用户。可以肯定的是,它绝对是这些选项中的一个。另一种方法是尝试使用PATCH:

PATCH /users/jerkface HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"status": "kicked"}

有一点很奇怪,因为用户的状态不会因为任何原因被kick,这就需要获取进一步的传递信息进入指定频道。

PATCH /users/jerkface HTTP/1.1
Host: api.example.com
Content-Type: application/json
{"status": "kicked", "kick_channel": "catgifs"}

这样处理仍然不和常理,因为它产生了一个新的专用字段,并且这个字段实际上并不是为用户而存在的。我们尝试使用如下关系:

DELETE /channels/catgifs/users/jerkface HTTP/1.1
Host: api.example.com

这样好一些了,因为我们不会混淆全局/users/jerkfase的资源,但依然缺少“kick”,“ban”或者“leave”选项。再次将它们放入正文或者查询语句中,为RPC方式添加一个专用字段。

其它翻译版本(1)

唯一想到的其他方法是创建一个kicks集合,一个bans集合和一个leaves集合,另外为POST/kicks,POST/bans和POST/leaves端点创建一些端点用于匹配。这些集合将允许特定资源的元数据,例如,列出用户被踢出的频道,但是感觉很像强制将一个不适合的应用程序作为范例。

Slack的网页API看起来像下面这样:

POST /api/channels.kick HTTP/1.1
Host: slack.com
Content-Type: application/json

{
  "token": "xxxx-xxxxxxxxx-xxxx",
  "channel": "C1234567890",
  "user": "U1234567890"
}

简单漂亮!我们仅仅为手头的任务发送了参数,就像任何具有函数的编程语言中一样。

一个简单的经验法则:

  • 如果一个API主要是动作,也许它应该是RPC。

  • 如果一个API主要是CRUD和操作相关数据,也许它应该是REST。

如果两者都不是明显的赢家?你选择哪种方法?

同时使用REST和RPC

你需要选择一个方法并只有一个API的想法是错误的。应用程序可以很轻松的拥有多个不被视为“主”API的API或者额外的服务。使用暴漏HTTP端点的API或者服务,你可以选择遵循REST或者RPC规则,只需要有一个REST API和少量的RPC服务。例如,在会议上,有人问这个问题:

我们有一个REST API来管理web托管公司。我们可以创建新的服务实例并将它们分配给用户,这很好,但是我们如何通过API以RESTful方式来重启服务并在批量服务器上运行命令?

除了创建一个具有POST/restartServer方法和POST/execServer方法的简单的RPC风格的服务,并可以通过REST服务器构建和维护的服务器上执行的服务之外,没有什么可行的方法时可行的。

总结

了解REST和RPC之间的差异在你设计新的API时可能会非常有用,当你使用现有API的功能时,它也可以真正帮助你。最好不要在单个API中混合使用样式,因为这可能会迷惑您的API的用户,并使所有需要统一编码约定(例如REST)却看到不同风格的编码规约集合(例如RPC)的工具崩溃。 在有使用REST意义时使用才REST;如果RPC更合适,请使用RPC。 或两者结合使用,这样就两全其美了!

猜你喜欢

转载自blog.csdn.net/aa19891204/article/details/81291188
今日推荐