詳解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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM