詳解REST架構風格

引言

做爲Web開發者,你可能或多或少了解一些REST的知識,甚至已經很是習慣於它,以致於在正式地學習REST的時候,你可能內心會想:「原本就是這樣作的啊,否則還能怎麼作呢?」
確實是這樣,REST已經成爲Web世界的一種內在架構原則。這主要是由於REST的產生確實與HTTP有着密不可分的聯繫。REST的提出者Roy Fielding在Web界是一位舉足輕重的人物,他是HTTP協議(1.0版和1.1版)的主要設計者、Apache服務器軟件的做者之1、Apache基金會的第一任主席……Fielding在幾年之後回顧起REST的設計過程時,他說道:html

Throughout the HTTP standardization process, I was called on to defend the design choices of the Web. That is an extremely difficult thing to do within a process that accepts proposals from anyone on a topic that was rapidly becoming the center of an entire industry. I had comments from well over 500 developers, many of whom were distinguished engineers with decades of experience, and I had to explain everything from the most abstract notions of Web interaction to the finest details of HTTP syntax. That process honed my model down to a core set of principles, properties, and constraints that are now called REST.

在HTTP標準化的過程當中,Fielding做爲做者之一,負責向外界對HTTP的設計做出解釋和辯護。在這個過程當中,他的思惟模型受到不斷地錘鍊,一套準則從中沉澱了下來,這就是REST。前端

本篇文章的寫做目的是,與讀者一塊兒瞭解REST的內在,認識REST的優點,而再也不將它看成是「理所固然」。web

REST

REST是Representational State Transfer(在表示層上的狀態傳輸)的縮寫,這個詞的字面意思要在文章的後面才能解釋清楚。REST是一種WEB應用的架構風格它被定義爲6個限制,知足這6個限制,可以得到諸多優點(詳細優勢在文章最後總結)。數據庫

先用一句話來歸納RESTful API(具備REST風格的API): 用URL定位資源,用HTTP動詞(GET,HEAD,POST,PUT,PATCH,DELETE)描述操做,用響應狀態碼錶示操做結果。json

可是REST遠遠不只是指API的風格,它是一種網絡應用的架構風格。咱們到後面會有所體會。
另外,須要注意的是,REST的原則不只僅適用於HTTP協議。可是,因爲REST的應用場景絕大部分是WEB應用,本篇文章將基於HTTP來討論REST。後端

引入:從另外一個角度看待先後端分離

咱們瀏覽一個網站,說到底就是與這個網站中的資源進行互動(獲取、提交、更新、刪除)。前端的工做,就是爲用戶從服務端獲取資源、展現資源、請求服務端改變資源。api

RESTful API有助於客戶端和服務端的功能分離,服務器徹底扮演着一個「資源服務商」的角色。各類不一樣的客戶端均可以經過一致的API與這個「資源服務商」交流,從而與資源進行互動。
RESTful API數組

資源

在REST架構中,「資源」扮演者主要角色。它具備如下特色:緩存

  • 資源是任何能夠操做(獲取、提交、更新、刪除)的數據,好比一個文檔(document)、一張圖片……安全

    wikipedia: "Web resources" were first defined on the World Wide Web as documents or files identified by their URLs. However, today they have a much more generic and abstract definition that encompasses every thing or entity that can be identified, named, addressed, or handled, in any way whatsoever, on the web. 「資源」包括Web中任何能夠被標識、命名、定位、處理的事物。
  • 資源的集合也是一種資源,好比blogs表示博客(資源)的集合。
  • 進行資源操做的時候,用URI來指定被操做的資源。若是一個URI不只能標識一個網絡上的資源,還可以定位這個資源,那麼這個URI也叫URL。
  • 資源是一個抽象的概念,資源沒法被傳輸,只能傳輸資源的表示(representation)。一個資源能夠有多種表示,好比,一個資源能夠用HTML、XML、JSON來表示。具體傳輸哪一種表示取決於服務端的能力和客戶端的要求。傳輸的表示未必就是服務器存儲時使用的表示,好比,這個資源在服務器不是以HTML或XML或JSON來存儲的,多是一種更加利於壓縮的表示。總的來講,「表示」是「資源」的存儲和傳輸形式,「資源」是「表示」的內容(抽象概念)。無論用什麼形式來表示,始終描述的是這個資源。
  • 舉一個例子,當咱們討論「文章列表」這個資源時,咱們並不在意它是json格式仍是xml格式,咱們指的是它的含義:某個用戶的全部文章。可是當咱們真的要在服務器與客戶端之間傳輸數據的時候,不能直接「傳輸資源」,由於資源太抽象了,發送方必需要以某一種表示(representation)來傳遞它(好比json),接收方纔能很好地解析和處理。
  • 表示(representation)包括數據(data,表示資源自己)和元數據(metadata,用於描述這個representation)。在Roy Fielding的論文中有這個定義:A representation is a sequence of bytes, plus representation metadata to describe those bytes.
  • 在前面的例子中,嚴格來講,「json字符串」並非完整的representation,整個HTTP響應纔是representation。HTTP body中的是數據,HTTP header中的是元數據(尤爲是Content-Type這種字段)。
參考 https://restfulapi.net/

用URL定位資源

在RESTful架構風格中,URL用來指定一個資源。資源就是服務器上可操做的實體(能夠理解爲數據)。好比說URL/api/users表示的是該網站的全部用戶,這是一種資源,能夠與之互動(獲取、提交、更新、刪除)。另外,資源地址具備層次結構,好比/api/users/csr表示用戶名爲'csr'的用戶,/api/users/csr/blogs表示'csr'的全部博客,/api/users/csr/blogs/1234567表示其中的某一篇博客。這些都是資源,後者嵌套在前者之中。

既然URL表示一個資源,天然就不該該包含動詞,它應該由名詞組成。一個 not RESTful 的例子是經過向api/delete/resource發送GET請求來刪除一個資源。

更詳細的URL設計能夠查看 阮一峯的"RESTful API 設計指南"或者 知乎高票回答。URL風格只是REST的外表,不是本文的重點。

操做資源

既然經過URL可以指定一個服務器上的資源。那麼咱們應該如何與這個資源進行互動呢?咱們對這個資源(URL)使用不一樣的HTTP方法,就表明對這個資源的不一樣操做:

  • GET(SELECT):從服務器獲取資源(一個資源或資源集合)。
  • POST(CREATE):在服務器新建一個資源(也能夠用於更新資源)。
  • PUT(UPDATE):在服務器更新資源(客戶端提供改變後的完整資源)。
  • PATCH(UPDATE):在服務器更新資源(客戶端提供改變的部分)。
  • DELETE(DELETE):從服務器刪除資源。
  • HEAD:獲取資源的元數據。
  • OPTIONS:獲取信息,關於資源的哪些屬性是客戶端能夠改變的。

GET、HEAD、PUT、DELETE方法是冪等方法(對於同一個內容的請求,發出n次的效果與發出1次的效果相同)。
GET、HEAD方法是安全方法(不會形成服務器上資源的改變)。

PATCH不必定是冪等的。PATCH的實現方式有多是"提供一個用來替換的數據",也有多是"提供一個更新數據的方法"(好比 data++)。若是是後者,那麼PATCH不是冪等的。
Method 安全性 冪等性
GET
HEAD
POST × ×
PUT ×
PATCH × ×
DELETE ×
參考: HTTP Methods for RESTful Services

經過HTTP狀態碼錶示操做的結果

雖然HTTP狀態碼設計的本意就是表示操做結果,可是有時候人們每每沒有很好的利用它,RESTful API要求充分利用HTTP狀態碼

200 OK - [GET]:服務器成功返回用戶請求的數據,該操做是冪等的(Idempotent)。
201 CREATED - [POST/PUT/PATCH]:用戶新建或修改數據成功。
202 Accepted - [*]:表示一個請求已經進入後臺排隊(異步任務)
204 NO CONTENT - [DELETE]:用戶刪除數據成功。
400 INVALID REQUEST - [POST/PUT/PATCH]:用戶發出的請求有錯誤,服務器沒有進行新建或修改數據的操做,該操做是冪等的。
401 Unauthorized - [*]:表示用戶沒有權限(令牌、用戶名、密碼錯誤)。
403 Forbidden - [*] 表示用戶獲得受權(與401錯誤相對),可是訪問是被禁止的。
404 NOT FOUND - [*]:用戶發出的請求針對的是不存在的記錄,服務器沒有進行操做,該操做是冪等的。
406 Not Acceptable - [GET]:用戶請求的格式不可得(好比用戶請求JSON格式,可是隻有XML格式)。
410 Gone -[GET]:用戶請求的資源被永久刪除,且不會再獲得的。
422 Unprocesable entity - [POST/PUT/PATCH] 當建立一個對象時,發生一個驗證錯誤。
500 INTERNAL SERVER ERROR - [*]:服務器發生錯誤,用戶將沒法判斷髮出的請求是否成功。
完整狀態碼列表

如何設計RESTful API

在過去不使用RESTful架構風格的時候,若是咱們要設計一個系統,會以「操做」爲出發點,而後圍繞它去建設其餘須要的東西。
舉個例子,咱們要向系統中增長一個用戶登錄的功能:

  1. 須要一個用戶登錄的功能(操做)
  2. 約定一個用於登陸的API(也就是URL)
  3. 約定這個API的使用方式(發送響應什麼數據、格式是什麼)
  4. 先後端針對這個API進行開發

這種設計方式有以下缺點:

  1. 當咱們不斷爲這個系統增長操做,每增長一個操做都要按照上面的流程設計一次,第2和3點的工做實際是能夠大大削減的(經過REST)。
  2. 操做之間多是有依賴的,依賴多起來,系統會變得很複雜。
  3. 咱們的API缺少一致性(須要一份龐大的文檔來記錄api的地址、使用方式)。
  4. 操做一般被認爲是有反作用(Side Effect)的,很難使用緩存技術。

而若是咱們設計REST風格的系統,資源是第一位的考慮,首先從資源的角度進行系統的拆分、設計,而不是像以往同樣以操做爲角度來進行設計。

用兩個例子來講明:銀行的轉帳API,即時通信軟件中發送消息的API。

這兩個功能很是具備「動做性」,看起來和「資源」聯繫不大,很容易就會設計成not RESTful的API:POST /transfer/${amount}/to/${toUserID}POST /api/sendMessage
一旦在URL中引入了動詞,這個URL的功能就定死了,沒法用於別的用途(好比,GET /transfer/${amount}/to/${toUserID}GET /api/sendMessage的語義很奇怪,很差使用)。而且,不一樣功能的API有各自的結構,一致性不好,須要一份詳細的API文檔才能使用。

這種狀況下,要如何經過RESTful架構風格,設計一套一致、多用途的URL呢?
簡單地說,就是將一個「動做」理解爲「操做一個資源」。這裏的「操做」是指HTTP的方法。

對於轉帳動做,就能夠理解爲「新建一個轉帳事務」(轉帳事務是資源),所以API就能夠設置成這樣: POST /transactions,請求體爲:to=632&amount=500。這樣的設計不但簡潔明瞭,並且咱們能夠將這個URL用於別的用途:經過GET /transactions來獲取該用戶的全部轉帳事務。還能夠將GET /transactions/456828定義爲「獲取某一次轉帳記錄」。

即時通信軟件中發送消息的動做,咱們能夠理解爲「操做聊天記錄(聊天記錄是資源,它是由「消息」組成的集合,消息也是資源)」,因此API設計爲

POST /messages # 建立新的聊天記錄(body傳輸消息的內容)
GET /messages # 獲取聊天記錄(返回一個數組,其中每一個項是一個消息)
GET /messages/${messageID} # 獲取某個消息的詳細信息
PUT /messages/${messageID} # 更新某個消息(body傳輸消息的內容)
DELETE /messages/${messageID} # 刪除某個消息的記錄
同理,論壇類應用發帖、回帖的API也能夠這樣設計。

從以上的兩個例子咱們能夠看出,使用RESTful風格能夠克服傳統架構風格的那4個缺陷:

  1. 設計API工做量減小,由於功能需求一旦出來,須要操做的資源、操做的方式馬上就能分析出來,所以資源URL和API的使用方式(GET, POST...)都很容易獲得。
  2. 沒有了操做之間的依賴。資源之間雖然可能有關聯,可是小得多。
  3. 對資源的操做也就那麼幾種(獲取、新建、修改、刪除),API的一致性、自我描述性很強,不須要過多解釋。
  4. 對於GET請求,咱們均可以考慮使用緩存,由於在RESTful的架構中,GET請求表明獲取數據,必須是安全、冪等的。

服務器無狀態

根據REST的架構限制,RESTful的服務器必須是無狀態的,這意味着來自客戶的每個請求必須包含服務器處理該請求所需的全部信息, 服務器不能利用任何已經存儲的「上下文(context,在這裏表示用戶的會話狀態)」來處理新到來的請求,會話狀態只能由客戶端來保存,而且在請求時一併提供。

這裏注意兩點。1. 服務器不能存儲「上下文」不表明連數據庫都不能有,「上下文」指那些在服務器內存中的、非持久化的數據。2. 無狀態不表明不能有會話(sessions),無狀態僅僅指 服務器無狀態。服務器不記錄、維護會話,可是會話狀態能夠由 客戶端在每次請求的時候提供。

我一開始覺得無狀態與用戶登錄是衝突的,後來在Do sessions really violate RESTfulness? - StackOverflow上找到了一個令我滿意的解答。如下兩幅圖摘錄自這個答案。
無狀態的認證機制:
無狀態的認證機制

What you need is storing username and password on the client and send it with every request. You don't need more to do this than HTTP basic auth and an encrypted connection.
只須要將用戶名和密碼存儲在客戶端,而後客戶端每次發送請求都附帶上用戶名和密碼。要作到這點你只須要 HTTP基本認證(簡單來講就是將用戶名和密碼放在HTTP頭部)和一個加密的鏈接(HTTPS)。
若是每次認證,都要去數據庫查詢用戶的信息來覈對,那麼響應會很是慢,並且服務器也會有很大的性能損失。爲了加快認證的速度,最好在內存中使用認證緩存。這並不違背「無狀態」的限制,由於緩存的做用僅僅起加速的做用,沒有緩存照樣能工做。

無狀態的第三方鑑權機制:
無狀態的第三方鑑權機制

What about 3rd party clients? They cannot have the username and password and all the permissions of the users. So you have to store separately what permissions a 3rd party client can have by a specific user. So the client developers can register they 3rd party clients, and get an unique API key and the users can allow 3rd party clients to access some part of their permissions. Like reading the name and email address, or listing their friends, etc... After allowing a 3rd party client the server will generate an access token. These access token can be used by the 3rd party client to access the permissions granted by the user.
經過這個方式,用戶能夠給第三方應用受權,讓第三方應用拿着用戶的「令牌」訪問網站的一些服務。

以上兩幅圖講的是RESTful風格的身份認證機制。在實踐中最好使用OAuth 2.0框架

無狀態加強了系統的故障恢復能力,由於在服務器上沒有保存session的狀態,因此恢復起來更容易。
更重要的是,無狀態意味着分佈式系統可以更好地工做,負載均衡器能夠自由地將請求分發到任意的服務器。由於請求中都已經包含了服務器所需的全部信息,任何服務器均可以處理。
不只僅是服務器,代理、網關、防火牆也能夠理解消息,從而能夠在不修改接口的狀況下,增長更多強大的功能(好比代理緩存)。
而且,無狀態讓系統的橫向拓展能力強大。由於不須要在不一樣的服務器之間同步session狀態,因此服務器之間的溝通開銷很低。增長服務器的數量不會帶來明顯的性能損失(「1+1」更接近於「2」了)。

須要注意的是,REST不是一個「宗教」。在你本身的應用中,遵循REST的同時應該保持合適的尺度。經過權衡利弊,選擇整體效益最大的方案,即便這個方案有可能「稍微違反REST的原則」。詳見 "REST is not a religion..." - stackoverflow

HATEOAS

REST的4個層次

圖片來自 steps toward the glory of REST

前面已經討論了level 1和level 2,實際上REST還有一個更高的層次:HATEOAS(Hypermedia As The Engine Of Application State)。

對於客戶端的資源請求,服務器不只要返回所請求的資源,並且要返回客戶端所處的狀態和可轉移的狀態。(客戶端有狀態)

狀態能夠簡單地理解爲客戶端展現的數據。能夠把客戶端比喻成一個狀態機,那麼這個狀態機跳轉到一個新的狀態,就會顯示新的內容。「首頁」「文章列表」「某篇文章」就是三種客戶端狀態。

客戶端不須要提早知道應用有哪些狀態,而是根據服務端響應的「可轉移的狀態」,提供給用戶選擇,從而發生狀態轉移。

用簡單的話來講,在嚴格的RESTful架構中,客戶端不須要提早知道服務端的API有哪些、怎麼調用,在客戶端與服務器通訊的過程當中,服務端會告訴客戶端:在你當前所處的狀態下,有哪些API可使用、能夠轉移到哪些狀態。

既然服務器是無狀態的,那麼它要如何知道發起請求的用戶處於什麼狀態呢?這就要求客戶端在發送請求的時候要攜帶上足夠的信息,讓服務器可以判斷客戶端所處的狀態。

這就很像10086的「電話自動語音應答服務」:你想要查詢你的手機流量,只須要會撥打「10086」,對方會提示你按下哪些按鍵就能進入哪些狀態。進入下一個狀態之後,又會有語音提示你接下來可以按哪些按鍵……最終,你能進入到你想要的那個狀態(流量查詢服務)。你須要記住的僅僅是「10086」這個號碼而已!

10086的語音提示至關於Hypermedia,是驅動應用狀態轉換的「引擎」。

再進一步想一想,在RESTful架構中,全部的狀態其實就組成了一顆樹(更準確地說是網):根節點就是網站的基地址。在你獲取一個節點中的資源的同時,服務器還會返回給你這個節點的邊:Hypermedia(超連接就是一種Hypermedia)。經過Hypermedia,你可以知道相鄰節點的基本信息、地址。
結果就是:你可以訪問到這顆樹的全部節點,而你所須要提早知道的只是「如何到達根節點」而已!

每一個節點就是一個狀態。用戶能夠在這個狀態網中不斷跳轉。

這個例子(知乎)這個例子(stackoverflow)也是不錯的解釋。

wikipedia的解釋:a REST client should then be able to use server-provided links dynamically to discover all the available actions and resources it needs. As access proceeds, the server responds with text that includes hyperlinks to other actions that are currently available. There is no need for the client to be hard-coded with information regarding the structure or dynamics of the REST service.

這種架構的優點很是明顯:先後端之間的耦合更加微弱。
隨着應用功能的升級改變,「樹」的樣子會大大改變,可是隻須要讓後端修改返回的資源內容和Hypermedia,前端幾乎不用改動。功能的演化更加靈活了。

「資源」和「狀態」的關係

如今你應該明白Representational State Transfer中的State Transfer(狀態傳輸)是什麼意思了:在HATEOAS中,服務端將客戶端所處的狀態和能夠達到的狀態傳輸給客戶端。

等一下,在前面的資源小節,咱們不是說過傳輸的是資源表示(representation)嗎?怎麼這裏又說傳輸的是狀態?

其實在REST架構風格中,「傳輸狀態」和「傳輸資源表示」是同一個意思。客戶端所處的狀態,是由它接收到的資源表示來決定的。好比,客戶端接收到/user/csr/blogs資源,那麼客戶端的狀態就變成/user/csr/blogs(顯示csr的文章列表)。
等一下,爲何客戶端會收到「/user/csr/blogs」資源?由於客戶端請求的就是「/user/csr/blogs」資源。
繼續追溯,爲何客戶端會請求這個資源?由於用戶點擊了「查看文章列表」的連接(這個連接其實就是一個Hypermedia)。
繼續追溯,爲何有一個「查看文章列表」的連接顯示給用戶點擊?由於HATEOAS:服務端在返回上一個狀態(資源)的時候,會返回全部相鄰狀態的Hypermedia,其中就包括「查看文章列表」這個Hypermedia。客戶端會展現全部相鄰狀態的Hypermedia供用戶選擇。

按照從前日後的順序梳理一遍:

客戶端請求根資源
=> 服務器返回根資源的表示,以及相鄰資源的Hypermedia
=> 客戶端進入「根資源」狀態(好比說,展現首頁)
=> 客戶端顯示全部相鄰狀態的Hypermedia供用戶選擇(好比,在首頁有一個導航欄,裏面有幾個連接)
=> 用戶選擇了某個Hypermedia(好比,點擊了「查看文章列表」的連接)
=> 客戶端請求「文章列表」資源
=> 服務器返回「文章列表」資源的表示,以及相鄰資源的Hypermedia
=> 客戶端進入「文章列表」狀態
=> 客戶端顯示全部相鄰狀態的Hypermedia供用戶選擇(好比,在文章列表裏,顯示全部文章的連接)
……

不難發現,客戶端接收到一個新的資源表示,就會跳轉到新的狀態,這個過程稱爲狀態傳輸(服務器給客戶端傳輸新狀態)。所以狀態傳輸是經過傳輸資源表示來完成的。

REST的字面意思

Representational State Transfer的語法結構是(Representational (State Transfer)),在這裏咱們用的是representation的形容詞形式,意思是在表示層上的狀態傳輸。這個詞的字面意思是經過傳輸資源表示來傳輸客戶端狀態

REST的字面意思在網絡上有不少種理解,我參考了某位答主的兩個回答: https://stackoverflow.com/a/1...https://stackoverflow.com/a/4... ,由於這位答主的回答最符合 wikipedia的解釋:"The term is intended to evoke an image of how a well-designed Web application behaves: it is a network of Web resources (a virtual state-machine) where the user progresses through the application by selecting links, such as /user/tom, and operations such as GET or DELETE (state transitions), resulting in the next resource (representing the next state of the application) being transferred to the user for their use."

總結

至此,咱們應該可以體會到REST已經不只僅是一種API風格了,它是一種軟件架構風格(REST自己不是一種架構)。REST風格的軟件架構具備很強的演化、拓展能力:

  1. 一致的URL和HTTP動詞使用:確保系統可以接納多樣而又標準的客戶端,保證客戶端的演化能力。
  2. 無狀態:保證了系統的橫向拓展能力、服務端的演化能力。
  3. HATEOAS:保證了應用自己的演化能力(功能增長、改變)。

這3點是單單對演化拓展優點的說明,這個回答總結了REST的6個約束分別對應的優勢。


參考資料

Roy Fielding 提出REST的論文
Representational state transfer - wikipedia
What exactly is RESTful programming? - stackoverflow
Do sessions really violate RESTfulness? - stackoverflow
steps toward the glory of REST - Martin Fowler(軟件開發「教父」)

怎樣用通俗的語言解釋REST,以及RESTful? - 知乎
REST風格的優點是什麼? - 知乎
RESTful API 設計指南
What does Representational State mean in REST? - stackoverflow
Clarifying REST
REST APIs must be hypertext-driven - Roy Fielding

REST tutorial 1
REST tutorial 2

用戶註冊、登錄、登出的RESTful API設計

相關文章
相關標籤/搜索