40/XRAP

40/XRAP

可扩展资源访问协议

本文档描述了可扩展资源访问协议(XRAP),一种构建在 ZeroMQ 之上的 RESTful 协议。XRAP 为如何处理远程资源的问题提供了一个单一的、可扩展的答案。我们设计 XRAP 是为了避免开发脆弱的特定领域 RPC 协议。

前言

Copyright (c) 2015 贡献者。

本规范是自由软件;您可以根据自由软件基金会发布的 GNU 通用公共许可证(第三版,或由您选择的任何后续版本)的条款重新分发和/或修改它。发布本规范是希望它会有用,但不附带任何担保;甚至不包含适销性或适用于特定用途的默示担保。有关更多详细信息,请参阅 GNU 通用公共许可证。您应该已经随本程序收到了 GNU 通用公共许可证的副本;如果没有,请参阅 https://gnu.ac.cn/licenses

本规范是 自由开放标准,并受数字标准组织的 共识导向规范系统 管辖。

本文档中的关键词“MUST”、“MUST NOT”、“REQUIRED”、“SHALL”、“SHALL NOT”、“SHOULD”、“SHOULD NOT”、“RECOMMENDED”、“MAY”和“OPTIONAL”应按 RFC 2119,“用于指示需求等级的 RFC 中的关键词”中的描述来解释。

概述

用例

我们的通用用例是一个服务器,它持有一组属于远程客户端并由其管理的资源。我们希望构建基于网络协议的 API,这些协议允许客户端和服务器分布在局域网和广域网上。我们认为这样的协议对于分布式架构中组件的清晰分离至关重要。

协议定义了约定且可测试的契约。我们的资源访问协议具有两层契约:

  • 就服务器提供的资源、这些资源的名称、属性和语义,以及它们彼此之间的关系达成一致。这包括任意数量的独立“资源模式”。

  • 就如何在网络上传输资源、如何导航资源模式、创建新资源、检索资源、返回错误等达成一致。这是“访问协议”,由 XRAP 提供。

XRAP 的主要目标是使资源模式的开发、测试、改进和废弃成本极其低廉。也就是说,允许该领域的专家以最小的边际成本开发出好的模式。XRAP 通过剥离并独立解决访问协议来实现这种经济性。

  • 从技术角度来看,它允许为访问层和传输层提供 100% 可重用的解决方案(任意语言的库和内部 API)。

  • 从规范角度来看,它允许将模式形式化为小型独立 RFC。

XRAP 对比 RPC

网络序列化工具(如 protobuf 和 zproto)的一个主要用途是构建特定领域的 RPC 协议。这些协议易于创建和使用,但会产生显著的上游成本:

  • 它们通常结构化为方法 + 参数,很少或没有组织。
  • 它们通常与软件领域设计紧密映射,也就是说它们没有被抽象化。
  • 因此,它们通常会随软件的每个版本发布而改变。
  • 这使得它们作为可互操作的契约变得无用,并且对于大型分布式系统极其脆弱。

实际上,RPC 协议试图将所有语义融入单个契约中,这要求系统中的所有组件并行更新。这即使在单个组织内部也很昂贵。对于更广泛的分发是病态的。

XRAP(类似于其所基于的 REST)只提供四种方法(POST、GET、PUT、DELETE),并将所有可扩展性转移到资源中,由模式定义。这是一种经过验证且简单的方式来获得未来的可扩展性。每个对等方都可以选择实现它选择的文档语义。每种文档类型都可以独立演化,作为其自身的契约。

协议设计

XRAP 定义了一种一致的协议,用于创建、检索、更新和删除远程资源。为了减少学习曲线和意外因素,我们使用了 RESTful 设计原则,并保持了 HTTP/1.1 兼容性。

REST 是一种设计模式,而非正式规范。它对于例如使用 PUT 或 POST 更新资源等内容并不正式,也不解释如何表示资源。它在异步事件传递等领域也不完整。XRAP 形式化了这些缺失的领域。它是一种可重用规范。然而,它不声称是通用或完整的。它提供了足够的抽象来支持实现一些基本模式,仅此而已。

网络传输

在本文档中,我们使用 ZMTP 作为参考传输方式。然而,XRAP 可以使用其他传输方式,例如 HTTP。XRAP 在其他传输方式上的映射应(SHALL)由单独的 RFC 定义。

性能权衡

XRAP 主要是一个详细的请求-回复协议,它使用自描述文档(JSON 或 XML)进行内容编码。这是最易于使用和扩展的编码形式。实现者很容易为实验或专门化添加新属性。忽略未知属性也很容易。

技术规范

正式文法

以下 ABNF 文法定义了基于 ZMTP 的 XRAP 序列化:

xrap_msg        = *( POST | POST-OK
                   | GET | GET-OK | GET-EMPTY
                   | PUT | PUT-OK
                   | DELETE | DELETE-OK
                   | ERROR )

;  Create a new, dynamically named resource in some parent.

POST            = signature %d1 tracker parent content_type content_body
signature       = %xAA %xA5             ; two octets
tracker         = number-4              ; request tracker
parent          = string                ; Schema/type/name
content_type    = string                ; Content type
content_body    = longstr               ; New resource specification

;  Success response for POST.

POST-OK         = signature %d2 tracker status_code location etag date_modified
                  content_type content_body metadata
status_code     = number-2              ; Response status code 2xx
location        = string                ; Schema/type/name
etag            = string                ; Opaque hash tag
date_modified   = number-8              ; Date and time modified
content_type    = string                ; Content type
content_body    = longstr               ; Resource contents
metadata        = hash                  ; Collection total size/version/hypermedia

;  Retrieve a known resource.

GET             = signature %d3 tracker resource parameters if_modified_since
                  if_none_match content_type
resource        = string                ; Schema/type/name
parameters      = hash                  ; Filtering/sorting/selecting/paging
if_modified_since = number-8            ; GET if more recent
if_none_match   = string                ; GET if changed
content_type    = string                ; Desired content type

;  Success response for GET.

GET-OK          = signature %d4 tracker status_code etag date_modified content_type
                  content_body metadata
status_code     = number-2              ; Response status code 2xx
etag            = string                ; Opaque hash tag
date_modified   = number-8              ; Date and time modified
content_type    = string                ; Actual content type
content_body    = longstr               ; Resource specification
metadata        = hash                  ; Collection total size/version/hypermedia

;  Conditional GET returned 304 Not Modified.

GET-EMPTY       = signature %d5 tracker status_code
status_code     = number-2              ; Response status code 3xx

;  Update a known resource.

PUT             = signature %d6 tracker resource if_unmodified_since if_match
                  content_type content_body
resource        = string                ; Schema/type/name
if_unmodified_since = number-8          ; Update if same date
if_match        = string                ; Update if same ETag
content_type    = string                ; Content type
content_body    = longstr               ; New resource specification

;  Success response for PUT.

PUT-OK          = signature %d7 tracker status_code location etag date_modified metadata
status_code     = number-2              ; Response status code 2xx
location        = string                ; Schema/type/name
etag            = string                ; Opaque hash tag
date_modified   = number-8              ; Date and time modified
metadata        = hash                  ; Collection total size/version/hypermedia

;  Remove a known resource.

DELETE          = signature %d8 tracker resource if_unmodified_since if_match
resource        = string                ; schema/type/name
if_unmodified_since = number-8          ; DELETE if same date
if_match        = string                ; DELETE if same ETag

;  Success response for DELETE.

DELETE-OK       = signature %d9 tracker status_code metadata
status_code     = number-2              ; Response status code 2xx
metadata        = hash                  ; Collection total size/version/hypermedia

;  Error response for any request.

ERROR           = signature %d10 tracker status_code status_text
status_code     = number-2              ; Response status code, 4xx or 5xx
status_text     = string                ; Response status text

; A list of name/value pairs
hash            = hash-count *( hash-name hash-value )
hash-count      = number-4
hash-value      = longstr
hash-name       = string

; Strings are always length + text contents
string          = number-1 *VCHAR
longstr         = number-4 *VCHAR

; Numbers are unsigned integers in network byte order
number-1        = 1OCTET
number-2        = 2OCTET
number-4        = 4OCTET
number-8        = 8OCTET

XRAP 方法和参数

一个基本的 XRAP 交互由一个请求和一个回复组成。请求由方法、一组头部字段和内容体组成。XRAP 客户端使用这些方法来处理服务器端资源:

  • POST - 创建新的、动态命名的资源。
  • GET - 检索已知资源的表示形式。
  • PUT - 更新已知资源。
  • DELETE - 删除已知资源。

所有方法都正交地作用于所有类型的资源,无论这些资源是否包含其他资源。并非所有方法和资源的组合都有意义、被允许或必然在任何基于 XRAP 的最终 API 中实现。

我们使用这些头部字段,它们具有 HTTP/1.1 的含义,并作为使用它们的每个方法中的独立字段实现:

  • Location - 标识新创建资源的 URN。
  • ETag - 标识资源实例的不透明哈希标签。
  • Date-Modified - 资源被修改的日期和时间。
  • If-Modified-Since - 使用资源日期进行的条件 GET。
  • If-None-Match - 使用资源 ETag 进行的条件 GET。
  • If-Unmodified-Since - 使用资源日期进行的条件 PUT 或 DELETE。
  • If-Match - 使用资源 ETag 进行的条件 PUT 或 DELETE。

请求可以(MAY)携带一个追踪器(tracker),它是一个非零整数。如果客户端请求带有追踪器,服务器必须(MUST)在相应的回复中包含它。

此外,我们可以向 GET 传递参数,就像我们通常使用 HTTP 查询那样。

  • Parameters - 用于过滤、排序、选择和分页的键/值对映射。参数键名不区分大小写。

此外,所有 _OK 消息都接受一个键/值对映射,以提供进一步的元数据,例如集合的总大小、超媒体(例如用于分页的链接)或返回的文档版本。

  • Metadata - 包含自定义数据的键/值对映射,用于进一步描述响应。元数据键名不区分大小写。

所有回复都包含一个三位数的状态码,该状态码符合 HTTP/1.1 状态码规范

XRAP 资源

服务器端资源要么是公共的(由许多客户端共享),要么是私有的(仅对单个客户端而言)。公共资源通过正式或非正式协议命名,而服务器单独负责命名私有资源。资源的 URN 基于资源名称,因此客户端可以提前知道公共资源的 URN,而服务器在运行时提供私有资源的 URN。

资源要么是结构化的,要么是不透明的。结构化资源具有客户端和服务器都可以访问和修改的属性。资源可以包含对子资源的引用(URN)。不透明资源具有 MIME 类型和二进制内容,中间层永远不会检查或修改它。

资源类型构成一个附加到根类型的静态模式。服务器实际持有的资源集合形成一个附加到公共根资源的动态资源树。为了导航资源树,客户端检索根资源并检查它。并非所有资源都可被发现:要处理私有资源,客户端必须已经创建了它,因此知道其 URN。

我们可以将结构化资源保存为可互换的 XML 或 JSON 资源文档,由客户端选择。XRAP 指定了表示资源及其属性的文法。

资源名称 (URN)

资源可以在不同时间由不同方创建:由服务器在启动时创建,由客户端在运行时创建,或由服务器作为其他事件的结果在运行时创建。只有创建资源的方才能删除它,并且可能对其他方处理该资源的权利施加限制。

公共资源的 URN 源自资源类型和名称,如下所示:

/{schema}/{resource type}/{resource name}

相应的 URI 前缀为协议和端点(例如主机名和端口)

{transport}://{endpoint}/{schema}/{resource type}/{resource name}

字段含义如下:

  • transport - 传输方式名称,例如“http”或“zmtp”。
  • endpoint - 网络端点,取决于传输方式。
  • schema - 资源模式名称,可能不包含字符 '/'。
  • resource type - 资源类型名称,可能不包含字符 '/'。
  • resource name - 资源本身名称。

私有资源的 URN 遵循相同的格式,其中资源类型为“resource”,资源名称是服务器生成的哈希值:

{transport}://{endpoint}/{schema}/resource/{resource hash}

注意,“resource” 是保留词,不应(SHALL NOT)用作资源类型名称。

创建资源

客户端创建新资源如下:

  • 客户端必须知道要创建的新资源的父资源的 URN。
  • 客户端向父资源 URN POST 新资源的规范。
  • 客户端通过指定名称创建公共资源。否则,服务器命名资源,此时资源是私有的。
  • 服务器要么返回状态码 2xx 附带资源文档,要么返回 4xx 或 5xx 附带错误文本。
  • 服务器在创建资源后,返回“201 Created”,并在 Location: 头部中提供新 URN。

以下是客户端到服务器的流程:

Client                                     Server
|                                           |
|  1.) POST to parent URN                   |
|      Resource specifications              |
|------------------------------------------>|
|                                           |
|  2.) 201 Created                          |
|      Location: Resource URN               |
|<------------------------------------------|
|                                           |

创建资源的 内容体 可能与客户端提供的内容体不同。客户端提供资源的规范,而服务器返回实际结果资源,该资源可能具有额外的属性和/或子资源。

以下是客户端请求创建动态资源的一般形式,以及服务器响应。假设我们使用的是 XML 而不是 JSON:

Client:
-------------------------------------------------
POST /{parent urn}
Content-Type: application/{schema}+xml

<?xml version="1.0"?>
<{schema} xmlns="http://digistan.org/schema/{schema}">
<{resource type} [name = "{name for public resource}"]>
    {resource specifications}
</{resource type}>
</{schema}>

Server:
-------------------------------------------------
201 Created
Content-Type: application/{schema}+xml
Date-Modified: {resource date and time}
ETag: {resource entity tag}
Location: {resource URN}

<?xml version="1.0"?>
<{schema} xmlns="http://digistan.org/schema/{schema}">
<{resource type} name="{resource name}"...>
    {resource contents}
</{resource type}>
</{schema}>

服务器可能针对 POST 请求返回这些特定的响应码:

  • 200 OK - 资源已经存在且符合指定(仅适用于公共资源)。
  • 201 Created - 服务器创建了资源,Location: 头部提供 URN。
  • 400 Bad Request - 资源规范不完整或格式错误。
  • 403 Forbidden - 在父 URN 上不允许 POST 方法。

如果发生 4xx 或 5xx 错误响应,服务器将在响应的内容体中返回文本错误消息。

创建公共资源是幂等的,也就是说,重复请求创建同一公共资源是被允许且安全的。客户端应将“200 OK”和“201 Created”视为同样成功。

检索资源

客户端检索资源如下:

  • 客户端必须知道想要检索资源的 URN。
  • 客户端向资源 URN 发送 GET 方法。
  • 客户端可选地指定头部字段以执行条件检索。
  • 服务器要么返回“200 OK”附带资源文档,“304 Not Modified”不带内容,要么返回 4xx 或 5xx 附带错误文本。

以下是客户端到服务器的流程:

Client                                     Server
|                                           |
|  1.) GET to Resource URN                  |
|------------------------------------------>|
|                                           |
|  2.) 200 OK                               |
|      Resource representation              |
|<------------------------------------------|
|                                           |

以下是客户端无条件请求检索资源的一般形式,以及服务器响应。假设我们使用的是 XML 而不是 JSON:

Client:
-------------------------------------------------
GET /{resource urn}

Server:
-------------------------------------------------
200 OK
Content-Type: application/{schema}+xml
Date-Modified: {resource date and time}
ETag: {resource entity tag}

<?xml version="1.0"?>
<{schema} xmlns="http://digistan.org/schema/{schema}">
<{resource type} ...>
    {resource contents}
</{resource type}>
</{schema}>

服务器可能针对 GET 请求返回这些特定的响应码:

  • 200 OK - 资源已经存在且符合指定(仅适用于公共资源)。
  • 304 Not Modified - 客户端已经拥有最新副本。

如果发生 4xx 或 5xx 错误响应,服务器将在响应的内容体中返回文本错误消息。

条件检索用于缓存资源,并且只有在资源更改时才获取它们。客户端条件性地检索资源如下:

  • 客户端必须之前已经检索过资源。
  • 客户端执行带有 If-None-Match: 和 If-Modified-Since: 头部字段的 GET 方法。
  • 如果客户端副本已过期,服务器返回“200 OK”并附带资源文档。
  • 如果客户端副本是最新版本,服务器返回“304 Not Modified”且不带内容。

以下是客户端条件性检索资源的一般形式:

Client:
-------------------------------------------------
GET /{resource urn}
If-None-Match: {resource entity tag}
If-Modified-Since: {resource date and time}

条件 GET 只在没有其他错误发生时生效,也就是说,如果结果本来会是“200 OK”。发送 If-None-Match: 或 If-Modified-Since: 头部字段中的任意一个都是有效的,但为了最佳结果,请同时使用两者。

检索资源没有副作用,也就是说,重复请求检索同一资源将引发相同的响应,除非第三方删除或修改了资源。

更新资源

客户端更新资源如下:

  • 客户端必须知道想要更新资源的 URN。
  • 在更新之前,客户端应该已经检索过资源。
  • 客户端使用修改后的资源文档向资源 URN 发送 PUT 方法。
  • 客户端可选地指定头部字段以执行条件更新。
  • 服务器要么返回 2xx 附带资源文档,要么返回 4xx 或 5xx 附带错误文本。

以下是客户端到服务器的流程:

Client                                     Server
|                                           |
|  1.) GET to Resource URN                  |
|------------------------------------------>|
|                                           |
|  2.) 200 OK                               |
|      Resource representation              |
|<------------------------------------------|
|                                           |
|  3.) PUT to Resource URN                  |
|      Modified resource representation     |
|------------------------------------------>|
|                                           |
|  4.) 200 OK                               |
|<------------------------------------------|

以下是客户端无条件修改资源的一般形式,以及服务器响应。我们忽略标准头部字段,并假设我们使用的是 XML 而不是 JSON:

Client:
-------------------------------------------------
PUT /{resource urn}
Content-Type: application/{schema}+xml

<?xml version="1.0"?>
<{schema} xmlns="http://digistan.org/schema/{schema}">
<{resource type} ...>
    {resource contents}
</{resource type}>
</{schema}>

Server:
-------------------------------------------------
200 OK
Date-Modified: {resource date and time}
ETag: {resource entity tag}

服务器可能针对 PUT 请求返回这些特定的响应码:

  • 200 OK - 资源成功更新。
  • 204 No Content - 客户端未提供资源文档,更新请求未产生任何效果。
  • 400 Bad Request - 资源文档不完整或格式错误。
  • 403 Forbidden - 在资源上不允许 PUT 方法。
  • 412 Precondition Failed - 客户端没有最新副本。

如果发生 4xx 或 5xx 错误响应,服务器将在响应的内容体中返回文本错误消息。

条件更新用于检测和防止更新冲突(当多个客户端试图同时更新同一资源时)。客户端条件性地更新资源如下:

  • 客户端必须之前已经检索过资源。
  • 客户端执行带有 If-Match: 和 If-Unmodified-Since: 头部字段的 PUT 方法。
  • 如果客户端副本已过期,服务器返回“412 Precondition Failed”。
  • 如果客户端副本是最新版本,服务器返回“200 OK”并附带新的资源文档。

以下是客户端条件性更新资源的一般形式:

Client:
-------------------------------------------------
PUT /{resource urn}
If-Match: {resource entity tag}
If-Unmodified-Since: {resource date and time}

条件 PUT 只在没有其他错误发生时生效,也就是说,如果结果本来会是“200 OK”。发送 If-Match: 或 If-Unmodified-Since: 头部字段中的任意一个都是有效的,但为了最佳结果,请同时使用两者。

修改资源是幂等的,也就是说,重复请求修改同一资源是被允许且安全的,除非第三方修改或删除了资源。

删除资源

客户端删除资源如下:

  • 客户端必须知道想要删除资源的 URN。
  • 在尝试删除之前,客户端应该已经检索过资源。
  • 客户端发送 DELETE 方法,指定资源 URN。
  • 客户端可选地指定头部字段以执行条件删除。
  • 服务器要么返回“200 OK”,要么返回 4xx 或 5xx 附带错误文本。

以下是客户端到服务器的流程:

Client                                     Server
|                                           |
|  1.) GET to Resource URN                  |
|------------------------------------------>|
|                                           |
|  2.) 200 OK                               |
|      Resource representation              |
|<------------------------------------------|
|                                           |
|  3.) DELETE to resource URN               |
|------------------------------------------>|
|                                           |
|  4.) 200 OK                               |
|<------------------------------------------|
|                                           |

以下是客户端无条件删除资源的一般形式,以及服务器响应。我们忽略标准头部字段:

Client:
-------------------------------------------------
DELETE /{resource urn}

Server:
-------------------------------------------------
200 OK

服务器可能针对 DELETE 请求返回这些特定的响应码:

  • 200 OK - 资源成功更新。
  • 403 Forbidden - 在资源上不允许 DELETE 方法。
  • 412 Precondition Failed - 客户端没有最新副本。

如果发生 4xx 或 5xx 错误响应,服务器将在响应的内容体中返回文本错误消息。

条件删除用于检测和防止删除冲突(当多个客户端试图同时删除同一资源时)。客户端条件性地删除资源如下:

  • 客户端必须之前已经检索过资源。
  • 客户端执行带有 If-Match: 和 If-Unmodified-Since: 头部字段的 DELETE 方法。
  • 如果客户端副本已过期,服务器返回“412 Precondition Failed”。
  • 如果客户端副本是最新版本,服务器返回“200 OK”。

以下是客户端条件性删除资源的一般形式:

Client:
-------------------------------------------------
DELETE /{resource urn}
If-Match: {resource entity tag}
If-Unmodified-Since: {resource date and time}

条件 DELETE 只在没有其他错误发生时生效,也就是说,如果结果本来会是“200 OK”。发送 If-Match: 或 If-Unmodified-Since: 头部字段中的任意一个都是有效的,但为了最佳结果,请同时使用两者。

删除资源是幂等的,也就是说,重复请求删除同一资源是被允许且安全的。实现可以缓存已删除资源的 URN,以区分对已删除资源的删除和对从未存在的资源的删除。

如果要删除的资源包含其他资源,这些资源会随之隐式且静默地删除。

MIME 类型选择

客户端检索资源时,可以(MAY)使用 content-type 字段指定所需的 MIME 类型。XRAP 允许以下内容类型:

  • “application/{schema}+xml”
  • “application/{schema}+json”
  • “text/xml”,或为空,两者等效。

服务器将响应一个按请求格式的资源文档,并附带相应类型的 Content-Type: 头部字段。注意,大多数浏览器将显示“text/xml”文档为 XML,而不会显示“application/{schema}+xml”。因此可以将 XRAP 资源 URN 嵌入,例如,在 HTML 文档中,并获得可用结果。

当客户端创建新资源或修改现有资源时,它应该(SHOULD)使用 Content-Type: 头部字段指定其正在发送的资源文档的 MIME 类型。XRAP 允许与 content-type 字段相同的三个值:

  • “application/{schema}+xml”
  • “application/{schema}+json”
  • “text/xml”,或为空,两者等效。

如果服务器不支持客户端请求或提供的内容类型,它应该(SHOULD)返回“501 Not Implemented.”。

异步操作

XRAP 模拟 REST,它是一个同步请求-回复模型。这很简单且健壮。然而,对于异步事件传递(主要从服务器到客户端)来说,成本过高,因为每个事件需要一次完整的往返。

XRAP 有一种异步非轮询资源传递机制,即 asynclets。Asynclet 是一种资源,在其存在之前就被赋予 URN 标识符。当客户端检索 asynclet 时,只有当资源存在时,服务器才会响应。

Asynclets 用于特定情况:当资源位于某种队列中,由客户端检索和清空(在循环中使用 GET 和 DELETE)。该队列将是父资源(即“容器”)。该队列然后包含零个或多个现有资源以及一个 asynclet。

正常资源通过 href 属性以及其他属性在容器资源中标识:

<some-type href="some-URN" ... />

Asynclet 具有 href 属性和第二个属性‘async="1”',告诉客户端该资源尽管有一个 URN,但实际上尚未存在:

<some-type href="some-URN" async="1" />

当客户端检索 asynclet 时,服务器等待直到资源在该容器内被创建,然后向客户端响应该新资源的内容。

当客户端检索 asynclet 容器时,容器资源包含所有现有资源以及一个 asynclet:

<some-type href="some-URN-1" ... />
<some-type href="some-URN-2" ... />
<some-type href="some-URN-3" ... />
<some-type href="some-URN-4" ... />
<some-type href="some-URN-5" async="1" />

任何时候都可能有新资源“到达”,客户端为 asynclet 持有的 URN 将然后指向一个实际存在的资源。这对客户端是透明的,除了当客户端对该 URN 发出 GET 请求时不会发生等待。

幂等性和副作用

GET、PUT 和 DELETE 方法是幂等的:客户端可以安全地多次发出这些请求。POST 在创建公共资源时是幂等的。用于创建私有资源的 POST 不是幂等的,每次成功执行将创建一个资源。

幂等性允许客户端从故障中安全恢复,在这些故障中它没有收到响应,但无法确定服务器是否收到了请求。例如,发生在服务器收到请求之后但在客户端收到响应之前的网络、客户端或中间件中的任何故障。客户端只需重新发出所有未完成的 GET、PUT、DELETE 和 POST-public 请求即可恢复。

GET 方法不修改服务器上任何资源的状态。

用于提升性能的请求流水线

客户端可以(MAY)发送请求而不等待响应。这可以通过减少网络往返来提高性能。HTTP 中对此的术语是“流水线”,我们使用 HTTP 的流水线语义。RFC 2616 的 8.1.2.2 节指出:

客户端不应(SHOULD NOT)使用非幂等方法或非幂等方法序列进行请求流水线操作(参阅 9.1.2 节)。否则,传输连接的过早终止可能导致不确定结果。想要发送非幂等请求的客户端应该(SHOULD)等待,直到收到前一个请求的响应状态。

客户端可以(MAY)对 GET、PUT、DELETE 和 POST-public 方法进行流水线操作,但不应(SHOULD NOT)对 POST-private 方法进行流水线操作。进行请求流水线操作时,客户端应该(SHOULD)设置一个非零的追踪器。

服务器允许以不同于接收顺序的顺序返回响应。

错误响应

当服务器返回错误响应(4xx 或 5xx)时,内容体必须(MUST)是纯文本,MIME 类型必须(MUST)是“text/plain”。客户端可以将内容体作为文本打印和记录,无需解析或解码。

资源文档文法

通过 XRAP API 管理的大多数(尽管不是全部)资源表示为结构化资源文档。

一般来说,客户端在希望创建新资源或更新资源时向服务器发送资源文档。服务器向客户端发送资源文档以响应创建、检索或更新请求。某些资源可以表示为不透明的 MIME 类型 blob,在这种情况下,它们就是这样被 POST 和检索的,而不是作为结构化资源文档。

XRAP 资源文档具有模式无关的正则文法,旨在支持 RESTful API 的所有需求。该文法使得应用程序可以按如下方式导航 API:

  • 资源是类型化的,类型名称构成文档中的主要元素名称。
  • 资源类型可以是一个容器,用于其他资源类型。
  • 这种资源类型层次结构形成了一个附加到单个根类型的静态类型树。
  • 运行时,实际资源形成一个附加到单个根资源的实际资源树。
  • 客户端通过检索根资源来导航实际资源树。
  • 作为容器的资源将包含对可访问子资源的 URN 引用。
  • 私有资源只能被创建它的客户端访问,并且知道其 URN。

此设计的我们的目标是:

  • 定义一种容易映射到任意结构化语言(包括 XML 和 JSON)的文档语法。
  • 提供只需很少的先验知识即可导航和发现的文档。
  • 提供廉价解析的好处,而无需形式验证的成本。
  • 允许服务器实现单方面扩展资源层次结构。
  • 为 XRAP 应用程序开发者尽量保持简单。
  • 允许未来演进结构化数据表示。

基本语法规则

XRAP 资源文档遵循以下基本规则:

  • 所有资源文档都是定义类型树的模式的一部分。
  • 资源文档具有一个文档根元素,其名称与模式名称相同。
  • 文档根包含零个或多个资源根
  • 资源根元素可能包含其他元素。
  • 除文档根外的所有元素都对应于 XRAP 资源,元素名称等于资源类型。
  • 资源属性表示为元素属性,而不是作为子元素。
  • 除文档根外的所有元素可以重复0次或多次。

此外,对于 XML 文档:

  • Content-type 必须(MUST)是“application/{schema-name}+xml”。
  • 文档根具有一个单一属性,xmlns="http://digistan.org/schema/{schema-name}"。注意,此 URL 不一定存在。
  • 双引号字符在属性值中必须(MUST)转义,通过写作 """。

对于 JSON 文档:

  • Content-type 必须(MUST)是“application/{schema-name}+json”。
  • 值字符串中的双引号字符必须(MUST)转义,通过写作 "\""。

以下是一个 XRAP 文档的 XML 示例,用于名为“music”的模式:

<?xml version="1.0"?>
<music xmlns="http://digistan.org/schema/music">
<playlist name = "default">
    <album
    artist="Echobelly" title="On" released="1995-10-17"
    summary="Underrated, bittersweet guitar rock perfection">
    <track title="Car Fiction" length="2:31" />
    <track title="King of the Kerb" length="3:59" />
    <track title="Great Things" length="3:31" />
    <track title="Natural Animal" length="3:27" />
    <track title="Go Away" length="2:44" />
    <track title="Pantyhose and Roses" length="3:26" />
    <track title="Something Hot in a Cold Country" length="4:01" />
    <track title="Four Letter Word" length="2:51" />
    <track title="Nobody Like You" length="3:52" />
    <track title="In the Year" length="3:31" />
    <track title="Dark Therapy" length="5:30" />
    <track title="Worms and Angels" length="2:38" />
    </album>
</playlist>
</music>

music 模式的资源树是:

playlist
    |
    o- album
        |
        o- track

以下是相同的文档的 JSON 格式:

{
"music": {
"playlist": [ { "name":"default",
    "album": [ { "artist":"Echobelly", "title":"On", "released":"1995-10-17",
    "summary":"Underrated, bittersweet guitar rock perfection",
    "track": [
        { "title":"Car Fiction", "length":"2:31" },
        { "title":"King of the Kerb", "length":"3:59" },
        { "title":"Great Things", "length":"3:31" },
        { "title":"Natural Animal", "length":"3:27" },
        { "title":"Go Away", "length":"2:44" },
        { "title":"Pantyhose and Roses", "length":"3:26" },
        { "title":"Something Hot in a Cold Country", "length":"4:01" },
        { "title":"Four Letter Word", "length":"2:51" },
        { "title":"Nobody Like You", "length":"3:52" },
        { "title":"In the Year", "length":"3:31" },
        { "title":"Dark Therapy", "length":"5:30" },
        { "title":"Worms and Angels", "length":"2:38" }
    ] }
    ] }
] }
}

注意,在 JSON 和 XML 之间进行映射是可能的,且不会丢失信息。XML 文档的关键规则是属性表示为元素属性,而不是作为子元素。

XRAP 文档使用以下规则允许导航和发现:

  • 属性“href”,如果存在,则包含表示该元素的资源的 URN。
  • 通过共同约定,客户端和服务器都知道根资源的 URN。
  • 所有非根资源的 URN 由服务器生成,可以由客户端存储。
  • 服务器提供的所有 URN 都是绝对的。客户端应该使用不带方案或主机名的 URN。

以下是客户端使用 GET 方法检索公共播放列表资源的示例,以及服务器的响应。服务器位于 host.com:

Client:
-------------------------------------------------
GET /music/playlist/default

Server:
-------------------------------------------------
200 OK
Content-Type: application/music+xml

<?xml version="1.0"?>
<music xmlns="http://digistan.org/schema/music">
<playlist name="default">
    <album
        artist="Echobelly" title="On"
        href="/music/resource/A1023" />
    <album
        artist="Muse" title="Showbiz"
        href="/music/resource/A0911" />
    <album
        artist="Toumani Diabate" title="Djelika"
        href="/music/resource/A0023" />
</playlist>
</music>

要检索特定专辑,客户端使用服务器提供的 URN,例如:

Client:
-------------------------------------------------
GET /music/resource/A1023

Server:
-------------------------------------------------
200 OK
Content-Type: application/music+xml

<?xml version="1.0"?>
<music xmlns="http://digistan.org/schema/music">
    <album
        artist="Echobelly" title="On" released="1995-10-17"
        summary="Underrated, bittersweet guitar rock perfection"
    <track title="Car Fiction" length="2:31"
        href="/music/resource/A1023/1" />
    <track title="King of the Kerb" length="3:59"
        href="/music/resource/A1023/2" />
    ...
    ...
    <track title="Worms and Angels" length="2:38"
        href="/music/resource/A1023/12" />
    </album>
</music>

要检索特定曲目,客户端再次使用服务器提供的 URN。注意,在这种情况下,服务器传递的内容类型为“audio/mpeg-3”,客户端应相应地处理(而不是作为 XRAP XML 或 JSON):

Client:
-------------------------------------------------
GET http://host.com/music/resource/A1023/5

Server:
-------------------------------------------------
200 OK
Content-Length: 2870112
Content-Type: audio/mpeg-3

...opaque binary content...

响应状态码

在本规范中,这些服务器回复码具有特定含义:

  • 200 OK - 请求正常完成。
  • 201 Created - POST 请求成功。
  • 204 No Content - 空 PUT 请求完成,无效果。
  • 304 Not Modified - 条件 GET 请求完成,无效果。
  • 400 Bad Request - 资源文档无效。
  • 403 Forbidden - 请求的方法在资源上不被允许。
  • 404 Not Found - 资源不存在。
  • 412 Precondition Failed - 未执行条件 PUT 或 DELETE。

客户端应该能够处理 HTTP/1.1 定义的所有错误(即使通过 ZMTP 传输),包括:

  • 401 Unauthorized - 需要认证。
  • 413 Too Large - 请求过大。
  • 500 Internal Error - 服务器发生内部错误。
  • 501 Not Implemented - 请求的功能未实现。
  • 503 Overloaded - 服务器过载。

处理未知元素和属性

资源文档可能包含元素,以及已知元素上的未知属性。当客户端和服务器实现不同版本的 API 时尤其可能发生,或者如果 API 由特定的客户端或服务器实现进行了扩展。

客户端和服务器应该容忍并忽略未知元素。客户端和服务器都不应保留未知元素。

基于 ZeroMQ 的传输

协议模型

以下是上述文法的 zproto 模型:

<class name = "xrap_msg"
    signature = "5"
    title = "XRAP serialization over ZMTP"
    script = "zproto_codec_c"
    >
This is the XRAP protocol (https://rfc.zeromq.cn/spec:40/XRAP):

<include filename = "license.xml" />

<message name = "POST" id = "1">
    <field name = "tracker" type = "number" size="4">Request tracker</field>
    <field name = "parent" type = "string">Schema/type/name</field>
    <field name = "content type" type = "string">Content type</field>
    <field name = "content body" type = "longstr">New resource specification</field>
Create a new, dynamically named resource in some parent.
</message>

<message name = "POST OK" id = "2">
    <field name = "tracker" type = "number" size="4">Request tracker</field>
    <field name = "status code" type = "number" size = "2">Response status code 2xx</field>
    <field name = "location" type = "string">Schema/type/name</field>
    <field name = "etag" type = "string">Opaque hash tag</field>
    <field name = "date modified" type = "number" size = "8">Date and time modified</field>
    <field name = "content type" type = "string">Content type</field>
    <field name = "content body" type = "longstr">Resource contents</field>
    <field name = "metadata" type = "hash">Collection total size/version/hypermedia</field>
Success response for POST.
</message>

<message name = "GET" id = "3">
    <field name = "tracker" type = "number" size="4">Request tracker</field>
    <field name = "resource" type = "string">Schema/type/name</field>
    <field name = "parameters" type = "hash">Filtering/sorting/selecting/paging</field>
    <field name = "if modified since" type = "number" size="8">GET if more recent</field>
    <field name = "if none match" type = "string">GET if changed</field>
    <field name = "content type" type = "string">Desired content type</field>
Retrieve a known resource.
</message>

<message name = "GET OK" id = "4">
    <field name = "tracker" type = "number" size="4">Request tracker</field>
    <field name = "status code" type = "number" size = "2">Response status code 2xx</field>
    <field name = "etag" type = "string">Opaque hash tag</field>
    <field name = "date modified" type = "number" size = "8">Date and time modified</field>
    <field name = "content type" type = "string">Actual content type</field>
    <field name = "content body" type = "longstr">Resource specification</field>
    <field name = "metadata" type = "hash">Collection total size/version/hypermedia</field>
Success response for GET.
</message>

<message name = "GET EMPTY" id = "5">
    <field name = "tracker" type = "number" size="4">Request tracker</field>
    <field name = "status code" type = "number" size = "2">Response status code 3xx</field>
Conditional GET returned 304 Not Modified.
</message>

<message name = "PUT" id = "6">
    <field name = "tracker" type = "number" size="4">Request tracker</field>
    <field name = "resource" type = "string">Schema/type/name</field>
    <field name = "if unmodified since" type = "number" size="8">Update if same date</field>
    <field name = "if match" type = "string">Update if same ETag</field>
    <field name = "content type" type = "string">Content type</field>
    <field name = "content body" type = "longstr">New resource specification</field>
Update a known resource.
</message>

<message name = "PUT OK" id = "7">
    <field name = "tracker" type = "number" size="4">Request tracker</field>
    <field name = "status code" type = "number" size = "2">Response status code 2xx</field>
    <field name = "location" type = "string">Schema/type/name</field>
    <field name = "etag" type = "string">Opaque hash tag</field>
    <field name = "date modified" type = "number" size = "8">Date and time modified</field>
    <field name = "metadata" type = "hash">Collection total size/version/hypermedia</field>
Success response for PUT.
</message>

<message name = "DELETE" id = "8">
    <field name = "tracker" type = "number" size="4">Request tracker</field>
    <field name = "resource" type = "string">schema/type/name</field>
    <field name = "if unmodified since" type = "number" size="8">DELETE if same date</field>
    <field name = "if match" type = "string">DELETE if same ETag</field>
Remove a known resource.
</message>

<message name = "DELETE OK" id = "9">
    <field name = "tracker" type = "number" size="4">Request tracker</field>
    <field name = "status code" type = "number" size = "2">Response status code 2xx</field>
    <field name = "metadata" type = "hash">Collection total size/version/hypermedia</field>
Success response for DELETE.
</message>

<message name = "ERROR" id = "10">
    <field name = "tracker" type = "number" size="4">Request tracker</field>
    <field name = "status code" type = "number" size = "2">Response status code, 4xx or 5xx</field>
    <field name = "status text" type = "string">Response status text</field>
Error response for any request.
</message>

</class>

ZeroMQ Socket 类型

服务器应(SHALL)创建一个 ROUTER socket,并且应该(SHOULD)将其绑定到端口,这是 XRAP 的注册 Internet 分配号码管理局 (IANA) 端口。服务器可以(MAY)将其 ROUTER socket 绑定到临时端口范围 (%C000x - %FFFFx) 中的其他端口。客户端应(SHALL)创建一个 DEALER socket,并连接到服务器 ROUTER 的主机和端口。

注意,ROUTER socket 向调用者提供发送者的连接标识,对于在 socket 上接收到的任何消息,该标识作为消息中其他帧之前的身份帧。

协议签名

每个 ZeroMQ 消息应(SHALL)以 XRAP/ZMTP 协议签名 %xAA %xA5 开始。服务器和客户端应(SHALL)静默丢弃收到的任何不是以这两个八位字节开始的消息。

该机制特别为绑定到临时端口的服务器设计,这些端口可能之前被其他协议使用过,且仍有对等方试图连接到这些端口。它也是一种通用的快速失败(fail-fast)机制,用于检测格式错误的 message。

安全方面

XRAP/ZMTP 使用 ZMTP 传输层安全机制(NULL、PLAIN、CURVE 等)。此协议没有特定的安全方面。

延伸阅读

XRAP 最初由 Pieter Hintjens 于 2014 年为欧盟 UNIFY 项目开发。其设计受到 RESTful 传输层 (RestTL) 的强烈影响并源自 RestTL,RestTL 是 RestMS 协议栈的一部分。RestTL 本身受到 AtomPub (IETF RFC 4287) 设计的启发。