65.9K
CodeProject 正在变化。 阅读更多。
Home

深入 REST 的核心——REST 架构风格更深层次、更通用的本质

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (16投票s)

2017 年 11 月 2 日

CPOL

10分钟阅读

viewsIcon

31003

本文旨在提炼 REST 架构风格更深层次、更通用的本质,而不将其局限于 Web 服务。

引言

Roy Fielding 在他的论文中将 REST(Representational State Transfer,表述性状态转移)定义为“分布式超媒体系统的架构风格”,并将其定义为带有统一接口的分层代码按需客户端缓存无状态服务器。网络上描述这种风格的文章总是将 REST 定义在“RESTful Web 服务”的语境中,其中 HTTP 用于操纵大量可通过 URL 定址的资源。但经过深思熟虑,人们会意识到 RESTful Web 服务只是 REST 作为一种架构风格的应用,而这种风格本身具有更深层次、更简单、隐藏的本质,我将在这里尝试揭示它。

REST 的核心

为了揭示表述性状态转移风格的隐藏本质,让我们倒序解剖它的名称。

转移这个词意味着至少有两个进程通过某种介质进行通信,这暗示着一个分布式系统。

状态这个词意味着分布式系统的一个进程将其内部的“世界观”(surrounding world)转移给另一个进程。这种“内部世界观”是进程履行其职责所需的所有相关信息(参见图 1)。它包含从环境中收集的信息和内部生成的信息,并由名词表示。

图 1. 进程嵌入在其环境中,并包含一个“内部世界观”(surrounding world)。

表述性这个词意味着进程不是字面上发送它们的“内部世界观”,而是将其编码成接收方可以理解的描述(表述)。表述隐藏了进程内部状态的内部性质(实现)。

状态 vs 命令

状态的转移与命令的转移不同,因为它不暗示接收方的任何具体行为。如果发送方转移命令(如RPC的情况),它负责接收方的行为,但如果发送方转移状态,它只是表明它所观察到的当前世界观是什么,并让接收方执行它认为需要的任何操作(参见图 2)。这意味着发送方假定对发送的状态有某种反应,但并不要求具体的行动,将这个决定留给接收方。这导致了进程之间控制的分散和耦合的松散。

图 2. RPC(顶部)与 REST(底部)通信风格。

为了实现成功的通信,转移的“世界观”需要从接收方的角度来看是完整的。这意味着每个消息都需要是自包含的,携带足够的信息,独立于之前或之后发送的任何其他消息进行处理。这反过来意味着无上下文交互,因为任何通信进程都不需要维护交互上下文(如预期的消息序列号)(我将使用“无上下文”而不是“无状态”来避免混淆)。

阀门示例

为了澄清所说的内容,我们以一个通过通信链路连接到 SCADA 系统的电子控制阀为例(图 3)。阀门可以放置在 0(完全关闭)到 100(完全打开)之间的任何位置。

图 3. 连接到 SCADA 系统的电子控制阀。

电子阀门控制器通过通信链路周期性地发送一个单字节消息(图 4 中的消息 1,3,4,5),表示当前阀门位置。它也接受一个表示目标阀门位置的单字节消息(图 4 中的消息 2 和 5)。

图 4. RESTful 进程之间的示例消息序列——非请求消息模型。

包含阀门位置的单字节是阀门控制器完整的“内部世界观”。当控制器发送消息时,它不假设 SCADA 系统的任何特定操作,它只是宣告其状态。当 SCADA 系统发送消息时,它也不假设任何特定操作,因为控制器可以自由地采取它认为可行的任何行动(尽管 SCADA 系统当然期望物理阀门最终处于所需位置)。在图 4 中,控制器接受它接收到的状态并适当地移动阀门,周期性地报告阀门的新当前位置。

RESTful 风格同样适用于图 5 中所示的请求-响应模型。

图 5. RESTful 进程之间的示例消息序列 - 请求-响应模型。

REST 最重要的含义是,阀门控制器有责任找出如何从一个状态转移到另一个状态,因为 SCADA 系统一无所知。SCADA 系统甚至可能足够“盲目”,不处理从控制器接收到的消息。它可能只是周期性地发送自己的“内部世界观”(图 4 中的消息 2 和 5),假设这实际上是唯一的“真理”,而控制器需要处理这个事实。

相比之下,图 6 显示了相同的进程以 RPC 风格进行通信,其中 SCADA 系统完全负责明确管理控制器的内部状态。

图 6. 进程通过 RPC 通信的示例消息序列图。

安全性和幂等性

这个例子说明了 REST 的另外两条规则。第一条规则指出,读取或接收另一个进程的状态是安全操作,这意味着它不会触发该进程可见状态的改变。可见状态是指系统业务逻辑通过后续与该进程的任何交互可以观察到的状态。安全操作可能会改变远程进程的不可见状态,例如生成日志条目。第二条规则是,向远程进程发送状态是幂等操作,这意味着多次发送相同的状态会产生与只发送一次状态相同的效果(请注意,图 4 中的消息 5 不会产生任何效果)。这与 RPC 风格形成对比,RPC 风格不强制这种行为。值得一提的是,所有安全操作本质上都是幂等的。

REST = 简单事务

总结到目前为止所写的内容,RESTful 协议由消息组成,这些消息以相互可理解的表示形式传输发送方与消息接收方相关的完整状态,并且每个消息都由接收方以安全或幂等的方式处理。这种方法的优点,除了松散耦合之外,还在于实现事务操作的简单性和效率。

读取事务

当在一个“对话”中连续读取远程进程状态的某些部分,无论并发更新如何,都返回一个一致的状态视图时,就实现了事务性 RPC 风格读取。常见的解决方案是首先通过发送专门的消息在远程组件上启动事务,使其对内部状态进行快照,然后通过一系列部分请求读取快照,最后以两阶段三阶段事务提交结束(参见图 7 顶部)。这种方法会带来通信和资源开销。如果整个相关状态在单个消息中传输(参见图 7 底部),则可以完全避免这个问题。在这种情况下,经典的锁定或队列机制足以有效地序列化操作。

图 7. RPC(顶部)与 RESTful(底部)读取。在 RESTful 方法中,可以以更节省资源的方式推迟并发状态修改。

写入事务

与事务性读取一样,RPC 风格的事务性写入需要在最后进行冗长的对话,并进行两阶段或三阶段事务提交(参见图 8 顶部)。相反,RESTful 写入事务可以通过单个请求消息执行(参见图 8 底部)。当所有相关状态都在单个消息中传输时,锁定或排队机制足以有效地序列化操作。此外,如果事务因某种原因失败(请求消息丢失、确认丢失等),发送方可以简单地再次重新发送消息,因为接收方具有幂等行为。

图 8 RPC(顶部)与 RESTful(底部)写入。在 RESTful 方法中,可以以更节省资源的方式推迟并发状态修改。

状态修补

在单个消息中发送整个状态是最简单也是首选的解决方案,但由于需要传输大量数据,通常效率不高。当状态的修改部分相对于整个状态较小时,效率低下的问题尤其明显。为了解决这个问题,需要实现状态分区机制。如果状态块是唯一可识别的(例如,通过名称或索引),那么只需要传输状态的更改(补丁)。

为了说明这一点,假设我们的电子阀门控制器控制的不是一个而是上千个阀门,并且 SCADA 系统以请求-响应方式与其通信。SCADA 系统发送包含阀门编号的请求,控制器则响应一个 <阀门编号,位置> 对的向量。控制器还接受来自 SCADA 系统的对向量,并相应地设置指定阀门的位置(参见图 9)。这样,SCADA 系统只能读取或写入它感兴趣的阀门位置。

图 9. 具有命名状态部分的状态修补。

状态修补可以以之前描述的方式进行事务性处理。唯一的条件是在单个消息中传输完整的补丁集,以利用幂等性。

状态范围变更

在两个“阀门示例”中,只使用了固定大小的状态,但 RESTful 方法也适用于可变大小状态的使用场景。图 10 展示了 PC 和音乐播放器之间的消息交换。PC 首先请求包含三首歌曲的播放器全部状态。之后,PC 向其发送一个新状态(由用户修改),其中缺少 Song2,并额外包含 Songs 4 和 5。作为响应,播放器通过下载歌曲 4 和 5 并删除歌曲 2 来将其内部状态调整为接收到的表示所描述的状态。

图 10. 可变大小状态使用场景。

这个例子很好地说明了进程内部状态与该状态的表示之间的区别。音乐播放器的内部状态是一组音频文件,但它的表示是一组歌曲名称。

修补可变大小状态

如果歌曲集合很小,播放器示例工作正常,但如果歌曲数量增长到数千首,每次来回发送整个歌曲列表可能会效率低下。取而代之的是,可以只发送 PC 和播放器状态之间的差异。引入描述 NEW、MODIFIED 和 MISSING 歌曲的元数据解决了这个问题(参见图 11)。重要的是要注意,NEW、MODIFIED 和 MISSING 标签是描述状态块“及时性”的形容词,而不是命令接收方添加、更新或删除的动词。

图 11. 使用名词进行可变状态修补。

PC 以类似于 diff UNIX 工具描述文本文件中修改集的方式发送描述音乐播放器状态差异的消息。写入消息是自包含的,并且可以以幂等方式处理。

结论

总结一下,REST 的核心思想是**以名词**的形式,在自包含的消息中以无上下文对话的方式传输“内部世界观”的表示,以便接收方能够以幂等方式处理,并将状态修补(用**形容词**表示)作为一种优化(参见图 12)。与 RPC(使用名词表示数据,动词表示操作)相比,其优点是控制分散、松散耦合和简化的事务处理。RESTful 通信的缺点是(通常)消耗更多带宽,并给接收方带来更多负担,因为接收方有责任推断出正确的动作序列,以使其当前内部状态适应新的、期望的状态。

图 12. REST 规则。

本文最重要的结论是,REST 作为一种架构风格并不与特定的通信协议绑定。例如,它可以通过消息服务在中间件层面实现,通过 SNMP 在应用层面实现,通过纯 TCP 或 UDP 在传输层面实现,甚至可以通过 RS232 在物理层实现。最流行(老实说也不是很准确)的实现是通过 HTTP 协议,被称为“RESTful Web 服务”。我将让读者将本文中描述的概念映射到 HTTP 协议。

注意:在接下来的文章《裸 HTTP 并非完全 RESTful》中,我解释了为什么裸 HTTP 协议并非完全 RESTful,并展示了一个完全 RESTful 协议的示例。

© . All rights reserved.