REST架构原则初探

什么是RESTful架构?

RESTful 架构,是目前最流行的一种互联网软件架构。它基于REST原则,结构清晰、符合标准、易于理解、扩展方便,正得到越来越多网站的采用。

在传统上,软件和网络是两个不同的领域,很少有交集;软件开发主要针对单机环境,网络则主要研究系统之间的通信。互联网的兴起,使得这两个领域开始融合,越来越多的人开始意识到,网站即软件,而且是一种新型的软件。

2000年,Roy Thomas Fielding(HTTP 协议的主要设计者、Apache 基金会的第一任主席)在他的博士论文中提出了「REST」这一概念;对于论文的写作目的,他有如下的描述:

   我这篇文章的写作目的,就是想在符合架构原理的前提下,理解和评估以网络为基础的应用软件的架构设计,得到一个功能强、性能好、适宜通信的架构。

Fielding将他对互联网软件的架构原则,定名为 REST,即 Representational State Transfer 的缩写。这里对这个词组的翻译是「表现层状态转化」。

如果一个架构符合REST原则,就称它为 RESTful 架构;也就是说,只要我们弄清楚了 Representational State Transfer 这个词组的含义,也就弄明白了 RESTful 架构是怎么一回事。

这里先对 REST 原则进行描述,再对 RESTful API 的设计方法展开讨论。

REST 架构原则

前面提到,REST 其实是「Representational State Transfer」词组的缩写,我们可以从下面三个部分入手,理解这个词组的含义:

资源(Resource)

REST 可译作“表现层状态转换“,其实对主语还进行了一层省略:这里的“表现层”指的就是「资源(Resource)」的「表现层」。

在 REST 架构原则看来,网络上的每个具体信息都是一个实体,也就是资源;资源可以是一个文本、一段文字、一张图片、一种服务,总之就是一个具体的实体。我们用一个 URI (唯一资源标示符)来唯一指向特定的某个资源。在使用时,我们应保证 uri 具有良好的可读性和标示性。

下面给出一个 github 的链接示例:

https://github.com/git/git/commit/e3af72cdafab5993d18fae056f87e1d675913d08

上述链接便很清晰明了,标示了 git 上一次特定的 commit 请求。

所谓的“上网”,其实就是与网络上的“资源”进行交互,对“资源”的 uri 进行调用。

表现层(Representation)

上面提到,「Resource」是网络上某种信息的实体,但即使对于同一种资源,我们会希望能够它能以不同的形式呈现在用户面前。而这种将「资源」具体呈现出来的形式,便称为表现层(Representation)

例如,现有一段文字信息,对于这个「文字资源」,在进行数据传输时我们希望它能够以 JSON 进行传输,而在存储时我们又可能希望它能够以 XML 或是 txt 形式存储在本地。

对于一个资源,我们用一个 URI 来标示它的实体,但 URI 不能标示资源的形式。套用上面的例子,文字资源的 uri 只能让服务器或客户端准确地找到这个资源,但服务器和客户端不知道这个资源是哪种形式的。也就是说,用户真正需要看到的并不仅仅是资源,而是资源的特定 Representation

很多网站直接在 URL 中直接指定资源的表现形式,例如 CSDN 和博客园,每篇博客的 URL 后都可以看到「.html」的后缀:

https://www.cnblogs.com/bellkosmos/p/5237146.html

严格意义上来说这样的后缀名是不规范的:因为 URI 应该只代表资源的地址,「.html」后缀代表文字信息的格式,属于「表现层」的范畴;一个资源的具体表现形式,应该在 HTTP 请求的头信息中,使用 Accept 和 Content-Type 字段来指定

下面的 ajax 代码就是一个很好的例子,它通过 Content-Type 字段指定 HTTP 的 body 是一个 JSON 格式、且采用 utf-8 字符集编码的文本信息:

$.ajax({
    type: "POST",
    url: "/users",
    contentType: "application/json;charset=utf-8",
    data:{
        "name": "test",
        "pwd": "123456"
    },
    dataType: "json",
    success:function (message) {
        alert("提交成功"+JSON.stringify(message));
    },
    error:function (message) {
        alert("提交失败"+JSON.stringify(message));
    }
});

状态转换(State Transfer)

了解了「Resource」和「Presentation」后,我们便可以按照自己的需要获取到我们理想状态的数据,但现实生活中,用户访问网站实际上是一个客户端和服务器之间的互动过程,这个过程中双方不断的建立连接进行通信,势必会涉及到数据和状态的变化。

这里涉及到「状态」这个可能引起困惑的名词,我们将其和 REST 原则中的「无状态通信原则」放在一起讨论。

无状态通信原则

首先需要说明的是,这里说的无状态通信原则,并不是说客户端应用不能有状态,而是指服务端不应该保存客户端状态。同时,互联网的基石,HTTP 协议,也是一个「无状态」协议。

「状态」指的是什么?

实际上,「状态」应该区分为应用状态资源状态,客户端负责维护应用状态,而服务端维护资源状态。「应用状态」便是用户在客户端进行的种种业务操作,例如对数据的更新操作;而在客户端,当对一个数据进行诸如更新等操作后,我们就可以说「资源状态」发生了改变。

什么是「无状态通信」?

从名字的意义相近,在「无状态通信」中,客户端与服务端的交互必须是无状态的,并在每一次请求中包含处理该请求所需的一切信息。

  • 「无状态」的交互

    这样以来,服务端不需要在请求间保留应用状态,只有在接受到实际请求的时候,服务端才会关注应用状态,并且根据应用状态来令资源状态作出相应的改变。

  • 每次请求中包含这次请求所需的所有信息

    这个比较好理解,应为服务端不会保存客户端的状态,所以每次请求对服务端来说都是独立存在的;如果当前请求中的信息不足以让服务端完成这个请求,那么这个请求将无效。

这种无状态通信原则,使得服务端和中介能够理解独立的请求和响应。 在多次请求中,同一客户端也不再需要依赖于同一服务器,方便实现高可扩展和高可用性的服务端。

当然,我们也有需要违法无状态通信的业务场景,例如「登录」操作,如果是遵循无状态通信,那么用户每次进行业务操作都需要重新进行登录,无疑是非常反人类的体验。在这种类似的场景中,我们可以使用「session」和「cookie」来跟踪和保存会话状态。

看到这里,状态转移应该就很好理解了:在客户端和服务器进行交互时,如果客户端的「应用状态」发生变化(如用户注册一个新账号提交了一个表单数据),程序就需要让服务器的「资源状态」按照客户端要求发生「状态转换」(也就是 CREATE 一个新的账号数据)。而这种转换是建立在表现层之上的,所以是「表现层状态转换」,也就是「REST」。

那么,对「REST」这个词组大家应该有一个大致的了解了,那再看看不难猜到「REST(表现层状态转换)」架构原则是做什么的了:对程序员(针对资源表现层的)程序中编写的状态转换方法作出约束,建立一个大家都应该去遵守的标准。

而在当今的互联网架构下,客户端能用到的手段,只有 HTTP 协议。简单来说,就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。它们分别对应四种基本操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源。

如果在编程中我们能够做到遵循这一原则,假设我们希望对一辆小车的图片进行查询,我们设计的 API 可能是这样的:

[GET] /img/car

如果我们需要希望删除这个图片,我们只需要将「GET」改成使用「DELETE」方法即可,这种 API 将资源定位和资源操作相分离,能够更加简洁直观地展示指令的需求。

更具体的便是 RESTful API 设计考虑的内容了,我们会在后续进行讨论。

RESUful API 的设计

RESTful 的核心思想就是,客户端发出的数据操作指令都是「动词 + 宾语」的结构。比如,GET /articles这个命令,GET是动词,/articles是宾语。按照之前提到的 REST 原则,「动词」便是「状态转换」,「宾语」就是「表现层」,连起来就是「表现层状态转换」,也就是 REST 了。

动词规约:统一资源接口

对于所有的资源,都是使用相同的接口进行访问,动词指的都是固定五种 HTTP 方法,对应数据的 CRUD 操作,这便是 RESTful架构的「统一接口原则」。

  • GET:读取(Read)
  • POST:新建(Create)
  • PUT:更新(Update)
  • PATCH:更新(Update),通常是部分更新
  • DELETE:删除(Delete)

按照 HTTP 的规范,动词一律采用大写。

宾语规约:必须是名词

宾语就是 API 的 URI,是 HTTP 动词作用的对象。按照 REST 原则,URI 应该只能起到对资源及其表现层进行唯一标示的作用。也就是说,它应该是名词,不能是动词,因此下面 URI 中含有 get、create等动词,都是不符合要求的 URI:

[GET] /getUser/1
[POST] /createUser
[PUT] /updateUser/1
[DELETE] /deleteUser/1

URI 既可以看成是资源的地址,也可以看成是资源的名称。如果某些信息没有使用 URI 来表示,那它就不能算是一个资源, 只能算是资源的一些信息而已。

对 RESTful 进行基础的了解后,我们来看看 URI 的一些设计技巧:

URI 设计技巧

多级 URI 的设计

多级 URI 设计这一块网络上有多种说法,这里我认为 github 的规范应该足以作为我们的标准:

  • 在页面间进行跳转时,使用「/」来表示资源的层级关系

    例如,我们需要查询「octokit」用户的所有仓库:

    [GET] /octokit
    

    我们会看到如下页面:

    用户拥有的仓库

    我们点击其第一个仓库「rest.js」,这时浏览器进行了页面跳转:

    用户拥有的某个仓库

    可以看到 github 这里进行使用「/」进行分割 URI,上面页面的 URI 是这样的:

    [GET] /octokit/rest.js
    
  • 在当前页面进行过滤时,使用「?」用来过滤资源

    看下面这样一个 github 页面,它是上面仓库的 issue 页面,它的 URI 是/octokit/rest.js/issues

    issue

    假设这时我们希望查看所有 「Closed」的 issue,按照前面分级 URI 的思想,很容易写出这样的 URI:

    [GET] /octokit/rest.js/issues/closed
    

    这种 URL 不利于扩展,语义也不明确,往往要想一会,才能明白含义。

    更好的做法是,除了第一级,其他级别都用查询字符串表达:

    [GET] /octokit/rest.js/issues?status=closed
    

    这里用「?」对资源过滤,这种URL通常对应的是一些特定条件的查询结果或算法运算结果。看看 github 中的实现:

    closed_issues

    可以看到,github 中使用了「?」来进行过滤,为/octokit/rest.js/issues?q=is%3Aissue+is%3Aclosed,只是过滤字段我们前面预估有一些不同。

HTTP 的 CRUD 动词无法表述的业务需求

如果某些动作是 HTTP 动词表示不了的,你就应该把动作做成一种资源。

比如网上汇款,从账户1向账户2汇款500元,错误的URI是:

[POST] /accounts/1/transfer/500/to/2

正确的写法是把动词「transfer」改成名词「transaction」,资源不能是动词,但是可以是一种服务

[POST] /transaction/from=1&to=2&amount=500.00

对需要获取不同版本的资源

一个常见的设计误区,就是在URI中加入版本号:

http://www.example.com/app/1.0/foo
http://www.example.com/app/1.1/foo
http://www.example.com/app/2.0/foo

因为不同的版本,可以理解成同一种资源的不同表现形式,所以应该采用同一个URI。版本号可以在HTTP请求头信息的「Accept」字段中进行区分(参见Versioning REST Services):

Accept: vnd.example-com.foo+json;version=1.0
Accept: vnd.example-com.foo+json;version=1.1
Accept: vnd.example-com.foo+json;version=2.0

总结

最后,我们再总结一下 RESTful 架构:

  1. 每一个 URI 代表一种资源;
  2. 客户端和服务器之间,传递这种资源的某种表现层;
  3. 客户端通过五个 HTTP 动词,对服务器端资源进行操作,实现「表现层状态转化」。

参考链接:

理解RESTful架构

RESTful API 最佳实践

RESTful 架构详解

http协议无状态中的 "状态" 到底指的是什么?!

posted @ 2019-08-23 09:15  Bylight  阅读(454)  评论(0编辑  收藏  举报