文章目錄
參考資料
菜鳥教程 RESTful 架構詳解
--------------------------------------正文分割線-----------------------------------------------------
1. 什麼是RESTful風格?
1.1 REST全稱
REST是REpresentational State Transfer的縮寫(一般中文翻譯爲表述性狀態轉移),REST 是一種體系結構,而 HTTP 是一種包含了 REST 架構屬性的協議,爲了便於理解,我們把它的首字母拆分成不同的幾個部分:
- 表述性(REpresentational): REST 資源實際上可以用各種形式來進行表述,包括 XML、JSON 甚至 HTML——最適合資源使用者的任意形式;
- 狀態(State): 當使用 REST 的時候,我們更關注資源的狀態而不是對資源採取的行爲;
- 轉義(Transfer): REST 涉及到轉移資源數據,它以某種表述性形式從一個應用轉移到另一個應用。
簡單地說,REST 就是將資源的狀態以適合客戶端或服務端的形式從服務端轉移到客戶端(或者反過來)。在 REST 中,資源通過 URL 進行識別和定位,然後通過**行爲(即 HTTP 方法)**來定義 REST 來完成怎樣的功能。
1.1 實例說明
在平時的 Web 開發中,method 常用的值是 GET 和 POST,但是實際上,HTTP 方法還有 PATCH、DELETE、PUT 等其他值,這些方法又通常會匹配爲如下的 CRUD 動作:
CRUD 動作 | HTTP 方法 |
---|---|
Create | POST |
Read | GET |
Update | PUT 或 PATCH |
Delete | DELETE |
儘管通常來講,HTTP 方法會映射爲 CRUD 動作,但這並不是嚴格的限制,有時候 PUT 也可以用來創建新的資源,POST 也可以用來更新資源。實際上,POST 請求非冪等的特性(即同一個 URL 可以得到不同的結果)使其成一個非常靈活地方法,對於無法適應其他 HTTP 方法語義的操作,它都能夠勝任。
在使用 RESTful 風格之前,我們如果想要增加一條商品數據通常是這樣的:
/addCategory?name=xxx
但是使用了 RESTful 風格之後就會變成:
/category
這就變成了使用同一個 URL ,通過約定不同的 HTTP 方法來實施不同的業務,這就是 RESTful 風格所做的事情了,爲了有一個更加直觀的理解,引用一下來自how2j.cn的圖:
2. RESTful的概念
要理解RESTful架構,最好的方法就是去理解Representational State Transfer這個詞組到底是什麼意思,它的每一個詞代表了什麼涵義。
下面結合REST原則,圍繞資源展開討論,從資源的定義、獲取、表述、關聯、狀態變遷等角度,列舉一些關鍵概念並加以解釋。
- 資源與URI
- 統一資源接口
- 資源的表述
- 資源的鏈接
- 狀態的轉移
2.1 資源與URI
REST的名稱"表現層狀態轉化"中,省略了主語。“表現層"其實指的是"資源”(Resources)的"表現層"。
**所謂"資源",就是網絡上的一個實體,或者說是網絡上的一個具體信息。**它可以是一段文本、一張圖片、一首歌曲、一種服務,總之就是一個具體的實在。你可以用一個URI(統一資源定位符)指向它,每種資源對應一個特定的URI。要獲取這個資源,訪問它的URI就可以,因此URI就成了每一個資源的地址或獨一無二的識別符。
任何事物,只要有被引用到的必要,它就是一個資源。資源可以是實體(例如手機號碼),也可以只是一個抽象概念(例如價值) 。
下面是一些資源的例子:
- 某用戶的手機號碼
- 某用戶的個人信息
- 最多用戶訂購的GPRS套餐
- 兩個產品之間的依賴關係
- 某用戶可以辦理的優惠套餐
- 某手機號碼的潛在價值
要讓一個資源可以被識別,需要有個唯一標識,在Web中這個唯一標識就是URI(Uniform Resource Identifier)。
URI既可以看成是資源的地址,也可以看成是資源的名稱。如果某些信息沒有使用URI來表示,那它就不能算是一個資源, 只能算是資源的一些信息而已。URI的設計應該遵循可尋址性原則,具有自描述性,需要在形式上給人以直覺上的關聯。這裏以github網站爲例,給出一些還算不錯的URI:
- https://github.com/git
- https://github.com/git/git
- https://github.com/git/git/blob/master/block-sha1/sha1.h
- https://github.com/git/git/commit/e3af72cdafab5993d18fae056f87e1d675913d08
- https://github.com/git/git/pulls
- https://github.com/git/git/pulls?state=closed
2.2 統一資源接口
RESTful架構應該遵循統一接口原則,統一接口包含了一組受限的預定義的操作,不論什麼樣的資源,都是通過使用相同的接口進行資源的訪問。接口應該使用標準的HTTP方法如GET,PUT和POST,並遵循這些方法的語義。
如果按照HTTP方法的語義來暴露資源,那麼接口將會擁有安全性和冪等性的特性,例如GET和HEAD請求都是安全的, 無論請求多少次,都不會改變服務器狀態。而GET、HEAD、PUT和DELETE請求都是冪等的,無論對資源操作多少次, 結果總是一樣的,後面的請求並不會產生比第一次更多的影響。
下面列出了GET,DELETE,PUT和POST的典型用法:
2.2.1 GET
-
安全且冪等
-
獲取表示
-
變更時獲取表示(緩存)
-
200(OK) - 表示已在響應中發出
-
204(無內容) - 資源有空表示
-
301(Moved Permanently) - 資源的URI已被更新
-
303(See Other) - 其他(如,負載均衡)
-
304(not modified)- 資源未更改(緩存)
-
400 (bad request)- 指代壞請求(如,參數錯誤)
-
404 (not found)- 資源不存在
-
406 (not acceptable)- 服務端不支持所需表示
-
500 (internal server error)- 通用錯誤響應
-
503 (Service Unavailable)- 服務端當前無法處理請求
2.2.2 POST
-
不安全且不冪等
-
使用服務端管理的(自動產生)的實例號創建資源
-
創建子資源
-
部分更新資源
-
如果沒有被修改,則不過更新資源(樂觀鎖)
-
200(OK)- 如果現有資源已被更改
-
201(created)- 如果新資源被創建
-
202(accepted)- 已接受處理請求但尚未完成(異步處理)
-
301(Moved Permanently)- 資源的URI被更新
-
303(See Other)- 其他(如,負載均衡)
-
400(bad request)- 指代壞請求
-
404 (not found)- 資源不存在
-
406 (not acceptable)- 服務端不支持所需表示
-
409 (conflict)- 通用衝突
-
412 (Precondition Failed)- 前置條件失敗(如執行條件更新時的衝突)
-
415 (unsupported media type)- 接受到的表示不受支持
-
500 (internal server error)- 通用錯誤響應
-
503 (Service Unavailable)- 服務當前無法處理請求
2.2.3 PUT
-
不安全但冪等
-
用客戶端管理的實例號創建一個資源
-
通過替換的方式更新資源
-
如果未被修改,則更新資源(樂觀鎖)
-
200 (OK)- 如果已存在資源被更改
-
201 (created)- 如果新資源被創建
-
301(Moved Permanently)- 資源的URI已更改
-
303 (See Other)- 其他(如,負載均衡)
-
400 (bad request)- 指代壞請求
-
404 (not found)- 資源不存在
-
406 (not acceptable)- 服務端不支持所需表示
-
409 (conflict)- 通用衝突
-
412 (Precondition Failed)- 前置條件失敗(如執行條件更新時的衝突)
-
415 (unsupported media type)- 接受到的表示不受支持
-
500 (internal server error)- 通用錯誤響應
-
503 (Service Unavailable)- 服務當前無法處理請求
2.2.4 DELETE
-
不安全但冪等
-
刪除資源
-
200 (OK)- 資源已被刪除
-
301 (Moved Permanently)- 資源的URI已更改
-
303 (See Other)- 其他,如負載均衡
-
400 (bad request)- 指代壞請求
-
404 (not found)- 資源不存在
-
409 (conflict)- 通用衝突
-
500 (internal server error)- 通用錯誤響應
-
503 (Service Unavailable)- 服務端當前無法處理請求
2.3 資源的表述
"資源"是一種信息實體,它可以有多種外在表現形式。我們把"資源"具體呈現出來的形式,叫做它的"表現層"(Representation)。
比如,文本可以用txt格式表現,也可以用HTML格式、XML格式、JSON格式表現,甚至可以採用二進制格式;圖片可以用JPG格式表現,也可以用PNG格式表現。
資源的表述包括數據和描述數據的元數據,例如,HTTP頭"Content-Type" 就是這樣一個元數據屬性。
URI只代表資源的實體,不代表它的形式。嚴格地說,有些網址最後的".html"後綴名是不必要的,因爲這個後綴名錶示格式,屬於"表現層"範疇,而URI應該只代表"資源"的位置。它的具體表現形式,應該在HTTP請求的頭信息中用Accept和Content-Type字段指定,這兩個字段纔是對"表現層"的描述。
2.3.1 json格式
以github爲例,請求某組織資源的json格式的表述形式:
response的body如下所示,爲json格式
{
"login": "github",
"id": 9919,
"node_id": "MDEyOk9yZ2FuaXphdGlvbjk5MTk=",
"url": "https://api.github.com/orgs/github",
"repos_url": "https://api.github.com/orgs/github/repos",
"events_url": "https://api.github.com/orgs/github/events",
"hooks_url": "https://api.github.com/orgs/github/hooks",
"issues_url": "https://api.github.com/orgs/github/issues",
"members_url": "https://api.github.com/orgs/github/members{/member}",
"public_members_url": "https://api.github.com/orgs/github/public_members{/member}",
"avatar_url": "https://avatars1.githubusercontent.com/u/9919?v=4",
"description": "How people build software.",
"name": "GitHub",
"company": null,
"blog": "https://github.com/about",
"location": "San Francisco, CA",
"email": "[email protected]",
"is_verified": true,
"has_organization_projects": true,
"has_repository_projects": true,
"public_repos": 341,
"public_gists": 0,
"followers": 0,
"following": 0,
"html_url": "https://github.com/github",
"created_at": "2008-05-11T04:37:31Z",
"updated_at": "2020-02-07T13:08:07Z",
"type": "Organization"
}
2.3.2 XML格式
假如github也能夠支持xml格式的表述格式,那麼結果就是這樣的:
2.3.3 常用的設計
2.3.3.1 在URI裏邊帶上版本號
有些API在URI裏邊帶上版本號,例如:
- http://api.example.com/1.0/foo
- http://api.example.com/1.2/foo
- http://api.example.com/2.0/foo
如果我們把版本號理解成資源的不同表述形式的話,就應該只是用一個URL,並通過Accept頭部來區分。
以github爲例,它的Accept的完整格式是:application/vnd.github[.version].param[+json]
對於v3版本的話,就是Accept: application/vnd.github.v3。對於上面的例子,同理可以使用使用下面的頭部:
- Accept: vnd.example-com.foo+json; version=1.0
- Accept: vnd.example-com.foo+json; version=1.2
- Accept: vnd.example-com.foo+json; version=2.0
2.3.3.2 使用URI後綴來區分表述格式
像rails框架,就支持使用/users.xml或/users.json來區分不同的格式。 這樣的方式對於客戶端來說,無疑是更爲直觀,但混淆了資源的名稱和資源的表述形式。
而django的DRF框架則如下,利用format這一param來確定返回數據的格式,如圖所示:
format=api時,
format=json時,
2.3.3.3 如何處理不支持的表述格式
當服務器不支持所請求的表述格式,那麼應該怎麼辦?若服務器不支持,它應該返回一個HTTP 406響應,表示拒絕處理該請求。
以github爲例,展示了一個請求XML表述資源的結果:
2.4 資源的鏈接
我們知道REST是使用標準的HTTP方法來操作資源的,但僅僅因此就理解成帶CURD的Web數據庫架構就太過於簡單了。
這種認知模式忽略了一個核心概念:“超媒體即應用狀態引擎(hypermedia as the engine of application state)”。即忽視了超鏈接這一關鍵要素。
當瀏覽Web網頁時,從一個連接跳到一個頁面,再從另一個連接跳到另外一個頁面,就是利用了超媒體的概念:把一個個把資源鏈接起來。要達到這個目的,就要求在表述格式裏邊加入鏈接來引導客戶端。在《RESTful Web Services》一書中,作者把這種具有鏈接的特性成爲連通性。
例如,在一個超市的項目中,/goods/的接口不僅要把商品的信息給出來,還要把商品類別,下一頁的api也一同給出來,以此提高api的連通性。
2.5 狀態的轉移
訪問一個網站,就代表了客戶端和服務器的一個互動過程。在這個過程中,勢必涉及到數據和狀態的變化。
互聯網通信協議HTTP協議,是一個無狀態協議。這意味着,所有的狀態都保存在服務器端。因此,如果客戶端想要操作服務器,必須通過某種手段,讓服務器端發生"狀態轉化"(State Transfer)。而這種轉化是建立在表現層之上的,所以就是"表現層狀態轉化"。
客戶端用到的手段,只能是HTTP協議。具體來說,就是HTTP協議裏面,四個表示操作方式的動詞:GET、POST、PUT、DELETE。通常情況下,它們分別對應四種基本操作:GET用來獲取資源,POST用來新建資源(也可以用於更新資源),PUT用來更新資源,DELETE用來刪除資源。
2.5.1 應用狀態與資源狀態
-
狀態應該區分
應用狀態
和資源狀態
,客戶端負責維護應用狀態,而服務端維護資源狀態。 -
客戶端與服務端的交互必須是無狀態的,並在每一次請求中包含處理該請求所需的一切信息。
-
服務端不需要在請求間保留應用狀態,只有在接受到實際請求的時候,服務端纔會關注應用狀態。這種無狀態通信原則,使得服務端和中介能夠理解獨立的請求和響應。
-
通過這種設計,在多次請求中,同一客戶端也不再需要依賴於同一服務器,方便實現高可擴展和高可用性的服務端。例如,服務器集羣時,客戶端就不必擔心該向集羣中的哪一臺服務器請求數據。
-
但有時候也會做出違反無狀態通信原則的設計,例如利用Cookie跟蹤某個服務端會話狀態,常見的是利用SESSIONID(會話id)來進行跟蹤。這意味着,瀏覽器隨各次請求發出去的Cookie是被用於構建會話狀態的。
-
當然,如果Cookie保存的是一些服務器不依賴於會話狀態即可驗證的信息(比如認證令牌token),這樣的Cookie也是符合REST原則的。
2.5.2 應用狀態的轉移
狀態轉移到這裏已經很好理解了, "會話"狀態不是作爲資源狀態保存在服務端的,而是被客戶端作爲應用狀態進行跟蹤的。客戶端應用狀態在服務端提供的超媒體的指引下發生變遷。服務端通過超媒體告訴客戶端當前狀態有哪些後續狀態可以進入。
這些類似"下一頁"之類的鏈接起的就是這種推進狀態的作用——指引你如何從當前狀態進入下一個可能的狀態。
例如,第一頁接口中,提供了第二頁的接口指向,即進入下一個狀態的入口
3. RESTful API 設計指南
3.1 協議
API與用戶的通信協議,通常基於HTTPs協議(生產環境中)。
3.2 域名 (Domain name)
應該儘量將API部署在專用域名之下。
https://api.example.com
如果確定API很簡單,不會有進一步擴展,可以考慮放在主域名下。
https://example.org/api/
3.3 版本(Versioning)
可以將API的版本號放入URL。
https://api.example.com/v1/
另一種做法是,將版本號放在HTTP頭信息中,但不如放入URL方便和直觀。Github採用這種做法。
具體用哪種,視情況而定就行了。
3.4 路徑(Endpoint)
路徑又稱"終點"(endpoint),表示API的具體網址。
在RESTful架構中,每個網址代表一種資源(resource),所以網址中不能有動詞,只能有名詞,而且所用的名詞往往與數據庫的表格名對應。一般來說,數據庫中的表都是同種記錄的"集合"(collection),所以API中的名詞也應該使用複數。
例如,有一個API提供電影(movie)的信息,還包括各種演員和導演的信息,則它的路徑應該設計成下面這樣。
- https://api.example.com/v1/movies
- https://api.example.com/v1/actors
- https://api.example.com/v1/directors
3.5 HTTP動詞
對於資源的具體操作類型,由HTTP動詞表示。
常用的HTTP動詞有下面五個(括號裏是對應的SQL命令)。
- GET(SELECT):從服務器取出資源(一項或多項)。
- POST(CREATE):在服務器新建一個資源。
- PUT(UPDATE):在服務器更新資源(客戶端提供改變後的完整資源)。
- PATCH(UPDATE):在服務器更新資源(客戶端提供改變的屬性)。
- DELETE(DELETE):從服務器刪除資源。
還有兩個不常用的HTTP動詞。
- HEAD:獲取資源的元數據。
- OPTIONS:獲取信息,關於資源的哪些屬性是客戶端可以改變的。
下面是一些例子,以movies數據集爲例。
- GET /movies:列出所有電影
- POST /movies:新建一個電影
- GET /movies/ID:獲取某個指定電影的信息
- PUT /movies/ID:更新某個指定電影的信息(提供該電影的全部信息)
- PATCH /movies/ID:更新某個指定電影的信息(提供該電影的部分信息)
- DELETE /movies/ID:刪除某個電影
- GET movies/ID/actors:列出某個指定電影的所有演員
- DELETE /movies/ID/actors/ID:刪除某個指定電影的指定演員
3.6 過濾信息(Filtering)
如果記錄數量很多,服務器不可能都將它們返回給用戶。API應該提供參數,過濾返回結果。
下面是一些常見的參數。
- ?limit=10:指定返回記錄的數量
- ?offset=10:指定返回記錄的開始位置。
- ?page=2&per_page=100:指定第幾頁,以及每頁的記錄數。
- ?sortby=name&order=asc:指定返回結果按照哪個屬性排序,以及排序順序。
- ?animal_type_id=1:指定篩選條件
參數的設計允許存在冗餘,即允許API路徑和URL參數偶爾有重複。
比如,GET /movies/ID/actors 與 GET /actors?movie_id=ID 的含義是相同的,都是找出指定id的電影下的所有演員。
3.7 狀態碼(Status Codes)
服務器向用戶返回的狀態碼和提示信息,常見的有以下一些(方括號中是該狀態碼對應的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 - [*]:服務器發生錯誤,用戶將無法判斷髮出的請求是否成功。
狀態碼的完全列表參見這裏。
3.8 錯誤處理(Error handling)
如果狀態碼是4xx,就應該向用戶返回出錯信息。一般來說,返回的信息中將error作爲鍵名,出錯信息作爲鍵值即可。
{ error: "Invalid API key" }
3.9 返回結果
針對不同操作,服務器向用戶返回的結果應該符合以下規範。
- GET /collection:返回資源對象的列表(數組)
- GET /collection/resource:返回單個資源對象
- POST /collection:返回新生成的資源對象
- PUT /collection/resource:返回完整的資源對象
- PATCH /collection/resource:返回完整的資源對象
- DELETE /collection/resource:返回一個空文檔
3.10 Hypermedia API
RESTful API最好做到Hypermedia,即返回結果中提供鏈接,連向其他API方法,使得用戶不查文檔,也知道下一步應該做什麼。
比如,當用戶向api.example.com的根目錄發出請求,會得到這樣一個文檔。
{"link": { "rel": "collection https://www.example.com/movies", "href": "https://api.example.com/movies", "title": "List movies", "type": "application/vnd.yourformat+json" }}
上面代碼表示,文檔中有一個link屬性,用戶讀取這個屬性就知道下一步該調用什麼API了。rel表示這個API與當前網址的關係(collection關係,並給出該collection的網址),href表示API的路徑,title表示API的標題,type表示返回類型。
Hypermedia API的設計被稱爲HATEOAS。Github的API就是這種設計,訪問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" }
Django的DRF中,api Root也是基於該理念而設計的,如圖所示
3.11 其他
(1)API的身份認證應該使用OAuth框架。
(2)服務器返回的數據格式,應該儘量使用JSON,避免使用XML。
------------------------------------------------------正文結束分割線--------------------------------------