早古时期,软件和网络是两个不同的领域,前者围绕着单机环境展开,而后者则研究系统之间的通信。随着互联网的兴起,使得这两个领域开始融合,首当其冲的就是基于 HTTP 协议的 Web 服务,越来越多的人开始意识到,“网站” 即是 “软件”。
其中的先驱者就是 Tim Berners-Lee(万维网的发明者,万维网联盟负责人)和 Roy Thomas Fielding(1996 HTTP/1.0、1999 HTTP/1.1 的主要设计者之一,Apache 基金会的第一任主席,Apache Web Server 和 HTTP 协议是共生共荣的关系)等人。
1989年,Tim Berners-Lee 在论文中提出可以在互联网上构建超链接文档,并提出了三点基本要素:
首先我们需要对 Resource 的概念有一个清晰的理解。所谓 Resource,就是互联网上的一个实体,或者叫具体信息。它可以是一段文本、一张图片、一首歌曲、一种服务,总之就是一个具体实物的抽象。在互联网中使用 URI 来唯一标记一个 Resource,包含了 URL 和 URN。
举个例子:寻找一个具体的人(URI)。使用地址来寻找就是 URL:XX省XX市XX区XX单元XX室的主人;使用身份证号和名字来寻找就是 URN:身份证号 + 名字(但无法确认人的地址)。两者各有场景,但需要注意的是 URL、URN 都是 URI 的子集,但日常生活中最常见的是 URL,所以大家口头上也习惯使用 URL 来说明一个 Resource。但在实际编码中,开发者仍要注意 URI 和 URL 的本质区别,注意规范选词。
REST(Representational State Transfer,表现层状态转移)最初被 Roy Thomas Fielding 在 2000 年的博士论文《Architectural Styles and the Design of Network-based Software Architectures(架构风格和基于网络的软件架构设计)》中提出。顾名思义,Roy Thomas Fielding 在这篇论文中主要讨论的是:如何在符合架构原理的前提下,理解和评估以网络为基础的应用软件的架构设计,得到一个功能强、性能好、适宜通信的架构。
可见,REST、HTTP 协议、Web 网站,三者之间天生联系紧密。基于此,我们要清晰理解 Representational State Transfer 含义的前提是要对 HTTP 协议有一定的认识。
Representational State Transfer 的含义:
所以,需要注意的是:URI 仅用于表示一个 Resource,不应该在 URI 中描述表现层的内容,一个优雅的 RESTful API 应该使用 Accept/Content-Type 字段来描述 Resource 的表现层。
Accept: Application/json
Content-Type: Application/json
而客户端使用的手段就是 Roy Thomas Fielding 在 HTTP 请求行中设计的 Request Methods(在 HTTP/0.9 中首次引入 GET 方法,在 HTTP/1.0 中首次引入了 POST、Head 方法,在 HTTP/1.1 中引入了 PUT 方法)。
# 客户端
GET /resource
Accept: text/html
# 服务端
Content-Type: text/html;charset=utf-8
Response code: 200
至此,我们回头再看,REST 讨论的其实是一个:如何将软件和网络两个领域进行交叉融合,继而得到一个功能强、性能好、适宜通信的互联网软件架构的问题。
虽然 REST 架构在今天(2020)的软件设计中随处可见,但了解其诞生的历史背景和发展历程,还是会对这些先驱者们产生由衷的敬意。
*-ful 在西文语境中常用于表示一种风格,RESTful API 就是符合 REST 架构设计思想的软件 API 风格。
笔者最早认识到 RESTful API,源于时任 AWS CTO 的一封内部邮件,那时是 2006 年,正值 AWS 孵化的初期,印象非常深刻的有两点:
直到 2013 年笔者接触 OpenStack(OpenStack 初期常被认为是 AWS 的开源对标版本)之后才更深刻的体会到了其内涵和精髓。一言以蔽之就是:大型分布式软件的各个组件之间必须具备解耦和扩展的能力,网络(Network-base)是组件之间通信的唯一依赖,且对资源的操作具有唯一的确定性。
退一步的,我们可以选取一个角度来比较一下 RESTful 与另外两种常见的分布式架构风格的区别:
AWS 奉行的这一铁律,使其得以在几年间飞速扩展至上百个核心组件,成为了极具韧性和良好生态的公有云架构。现在的软件公司基本都会采用 RESTful API 风格,让产品可以通过 API 的方式融入到行业生态中,形成 APIs 经济效益。
核心原则只有一句话:总是围绕着 Resources 进行建模,HTTP URI 标识资源,HTTP Request Methods 操作资源。
使用 HTTP Request Methods 时,要注意方法的 “安全性” 和 “幂等性”:
Method | 功能 | 描述 | 状态转移 | 幂等 |
---|---|---|---|---|
GET | SELECT | 获取一个(提供资源 ID)或多个(提供 Filter 条件)或全部资源,获取多个时使用 Query Parameters 进行过滤查询,使用 Pagination Parameters(e.g. limit、offset、page、sortby)进行分页查询。 | √ | √ |
POST | CREATE | 创建一个资源(杜绝一次创建多个资源)。 | X | X |
PUT | UPDATE | 更新一个资源(提供完整的资源数据)。 | X | √ |
PATCH | UPDATE | 更新一个资源(提供部分的资源数据)。 | X | √ |
DELETE | DELETE | 删除一个资源。 | X | √ |
HEAD | 获取资源的元数据而非资源本身。此方法经常被用来测试超文本链接的有效性,可访问性,和最近的改变,例如:获取虚拟机镜像的属性信息。 | √ | √ | |
OPTIONS | 获取信息,关于资源的哪些属性是由客户端决定的。在跨域或使用代理请求时,通常会用到,OPTION 请求在于判定资源的选项或需求,或者服务器的能力。 | √ | √ |
Method | 功能 | 描述 |
---|---|---|
GET | SELECT | 返回一个或多个资源的完整数据和 Status Code,返回一个时使用 {},返回多个时使用 [];若错误需要返回错误原因和正确提示。 |
POST | CREATE | 返回一个资源的完整数据和 Status Code;若错误需要返回错误原因和正确提示。 |
PUT | UPDATE | 返回一个资源的完整数据和 Status Code;若错误需要返回错误原因和正确提示。 |
PATCH | UPDATE | 返回一个资源的完整数据和 Status Code;若错误需要返回错误原因和正确提示。 |
DELETE | DELETE | 仅返回 Status Code;若错误需要返回错误原因和正确提示。 |
HTTP Response Status Codes(状态码)就是一个三位数,分成五个类别:
登录认证是 RESTful API 设计中的一个特殊课题,登陆验证源于用户使用 Web 应用时记录用户身份状态的需求,其特点是:
在 RESTful 设计中通常使用 Cookies 或 Token 的方式来实现登陆验证的需求:
Cookie 方式:因为 HTTP 协议是无状态的,所以一般使用 Cookie 来解决会话状态的保存,以弥补无法进行会话跟踪的不足。
Token 方式:
显然,Token 是更加推荐的方式,因为 Cookie 方式会在 HTTP Header 中保持一个状态(Session ID)。这个状态会要求该请求只能被存储了对应的 Session 进行处理,这一点违背了 REST 架构的原则。
现实情况中,总有一些场景(资源)是 HTTP Request Methods 所抽象不了的。上述的登陆验证是一个典型的例子。针对登陆验证场景,可以把用户在远程服务器的会话信息抽象为一个资源,这样的话,登陆动作其实就是在远程服务器增加了一个会话资源,反正,登出就是删除一个会话资源,所以 RESTful API 可以这样设计:
[POST] /login
[DELETE] /logout
再比如,网上汇款场景,将汇款的动作定义为一种服务:
[POST] /smsService
{"mobile":"13813888888","text":"hello world"}
再比如,OpenStack 的虚拟机操作 start、stop、reboot、migration 等,将这些操作定义为一个 os-action 资源,然后通过不同在 Request Body 中使用不同的内容来进行区分:
[POST] /servers/{uuid}
{"os-action": "start"}
{"os-action": "stop"}
简而言之,如果某些动作是 HTTP Method 动词所表示不了的,就应该把这个动作做成一种资源。
Roy Thomas Fielding 在论文中还提到了 HATEOAS(Hypermedia as the Engine of Application State,超媒体作为应用状态的引擎)的概念。
所谓 “应用状态”,即:客户端的状态,可以理解为会话状态。服务端以 HyperMedia(超媒体的)形式将资源展示在客户端,当客户端访问其中的超媒体的链接(URI)时,就可以获取该链接关联的资源或者可以对资源执行特定的操作。获取的资源或者经特定操作响应后的资源在经过同样 Resource Request Handler 确定表现层后,继续以超媒体的形式在表现在客户端中。而这种资源内容或形式的改变都会导致客户端会话状态的改变,所以媒体就成为了驱动客户端会话状态转换的引擎(应用状态的改变基于超媒体的改变)。
简而言之,HATEOAS 就是不断的在 Response Body 加入 HyperMedia API 链接,以供客户端进行调用。
通常的,API 的调用者完全掌握了 URI 是怎么设计的。一个解决方法就是在 Response Body 中给出相关链接,便于客户端进行下一步操作的判断。使得用户需要查阅繁琐的文档,也知道下一步应该怎么做。
Github 的 API 就实现了 HATEOAS,访问 api.github.com 会得到一个所有可用 API 的网址列表:
{
"current_user_url": "https://api.github.com/user",
"authorizations_url": "https://api.github.com/authorizations",
// ...
}
从上面可以看到,如果想获取当前用户的信息,应该继续访问 api.github.com/user,然后就得到了下面结果:
{
"message": "Requires authentication",
"documentation_url": "https://developer.github.com/v3"
}
服务器的返回中提示了相关文档的网址。
HATEOAS 格式并没有统一的标准,上面例子中,GitHub 就将它们与其他属性放在了一起。其实,更好的做法应该是将相关链接与其他属性分开在不同的区间中:
HTTP/1.1 200 OKContent-Type: application/json
{
"status": "In progress",
"links": {[
{ "rel":"cancel", "method": "delete", "href":"/api/status/12345" } ,
{ "rel":"edit", "method": "put", "href":"/api/status/12345" }
]}
}
OpenStack 也大量的使用到了这种设计:
HTTP/1.1 200 OK
Content-Type: application/json
{"servers": [{
"status": "ACTIVE",
"links": [{
"href": "http://192.168.10.111:8774/v2.1/e5ab2182bb984f3bb4773d4a83672549/servers/95f684d4-0802-484e-b852-7ded35a8eeb5",
"rel": "self"
}, {
"href": "http://192.168.10.111:8774/e5ab2182bb984f3bb4773d4a83672549/servers/95f684d4-0802-484e-b852-7ded35a8eeb5",
"rel": "bookmark"
}],
"image": {
"id": "be4e8e37-226f-4784-b19d-a439400edca0",
"links": [{
"href": "http://192.168.10.201:8774/e5ab2182bb984f3bb4773d4a83672549/images/be4e8e37-226f-4784-b19d-a439400edca0",
"rel": "bookmark"
}]
},
"flavor": {
"id": "ed218eec-1e00-4ea9-93e7-f6e4e7c0ba93",
"links": [{
"href": "http://192.168.10.201:8774/e5ab2182bb984f3bb4773d4a83672549/flavors/ed218eec-1e00-4ea9-93e7-f6e4e7c0ba93",
"rel": "bookmark"
}]
},
"id": "95f684d4-0802-484e-b852-7ded35a8eeb5",
......
}]}