1. 程式人生 > >詳解REST架構風格

詳解REST架構風格

編輯推薦:

本文來自於segmentfault.com,一起了解REST的內在,認識REST的優勢,而不再將它當作是“理所當然”

引言

作為Web開發者,你可能或多或少了解一些REST的知識,甚至已經非常習慣於它,以至於在正式地學習REST的時候,你可能心裡會想:“本來就是這樣做的啊,不然還能怎麼做呢?”

確實是這樣,REST已經成為Web世界的一種內在架構原則。這主要是因為REST的產生確實與HTTP有著密不可分的聯絡。REST的提出者Roy Fielding在Web界是一位舉足輕重的人物,他是HTTP協議(1.0版和1.1版)的主要設計者、Apache伺服器軟體的作者之一、Apache基金會的第一任主席……Fielding在幾年以後 

回顧起REST的設計過程時,他說道:

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是Representational State Transfer(在表示層上的狀態傳輸)的縮寫,這個詞的字面意思要在文章的後面才能解釋清楚。REST是一種WEB應用的架構風格,它被定義為6個限制,滿足這6個限制,能夠獲得諸多優勢(詳細優點在文章最後總結)。

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

但是REST遠遠不僅是指API的風格,它是一種網路應用的架構風格。我們到後面會有所體會。

另外,需要注意的是,REST的原則不僅僅適用於HTTP協議。但是,由於REST的應用場景絕大部分是WEB應用,本篇文章將基於HTTP來討論REST。

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

我們瀏覽一個網站,說到底就是與這個網站中的資源進行互動(獲取、提交、更新、刪除)。前端的工作,就是為使用者從服務端獲取資源、展示資源、請求服務端改變資源。

RESTful API有助於客戶端和服務端的功能分離,伺服器完全扮演著一個“資源服務商”的角色。各種不同的客戶端都可以通過一致的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不是冪等的。

參考: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架構風格的時候,如果我們要設計一個系統,會以“操作”為出發點,然後圍繞它去建設其他需要的東西。

舉個例子,我們要向系統中增加一個使用者登陸的功能:

需要一個使用者登陸的功能(操作)

約定一個用於登入的API(也就是URL)

約定這個API的使用方式(傳送響應什麼資料、格式是什麼)

前後端針對這個API進行開發

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

當我們不斷為這個系統增加操作,每增加一個操作都要按照上面的流程設計一次,第2和3點的工作實際是可以大大削減的(通過REST)。

操作之間可能是有依賴的,依賴多起來,系統會變得很複雜。

我們的API缺乏一致性(需要一份龐大的文件來記錄api的地址、使用方式)。

操作通常被認為是有副作用(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個缺陷:

設計API工作量減少,因為功能需求一旦出來,需要操作的資源、操作的方式立刻就能分析出來,因此資源URL和API的使用方式(GET, POST...)都很容易得到。

沒有了操作之間的依賴。資源之間雖然可能有關聯,但是小得多。

對資源的操作也就那麼幾種(獲取、新建、修改、刪除),API的一致性、自我描述性很強,不需要過多解釋。

對於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

圖片來自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風格的軟體架構具有很強的演化、拓展能力:

一致的URL和HTTP動詞使用:確保系統能夠接納多樣而又標準的客戶端,保證客戶端的演化能力。

無狀態:保證了系統的橫向拓展能力、服務端的演化能力。

HATEOAS:保證了應用本身的演化能力(功能增加、改變)。

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

 

http://www.uml.org.cn/zjjs/201805142.asp