什么是REST
REST全稱是Representational State Transfer,中文意思是表述性狀態轉移,它首次出現在2000年Roy Fielding的博士論文中。Roy Fielding是HTTP規范的主要編寫者之一。他在論文中提到:
我這篇文章的寫作目的,就是想在符合架構原理的前提下,理解和評估以網絡為基礎的應用軟件的架構設計,得到一個功能強、性能好、適宜通信的架構。REST指的是一組架構約束條件和原則。
如果一個架構符合REST的約束條件和原則,我們就稱它為RESTful架構。
REST本身並沒有創造新的技術、組件或服務,而隱藏在RESTful背后的理念就是使用Web的現有特征和能力,更好地使用現有Web標准中地一些准則和約束。
雖然REST本身受Web技術的影響很深,但是理論上REST架構風格並不是綁定在HTTP上,只不過目前HTTP是唯一與REST相關的實例。所以我們這里描述的REST也是通過HTTP實現的REST。
理解RESTful
要理解RESTful架構,需要理解Representational State Transfer這個詞組到底是什么意思,它的每一個詞都有什么涵義。
下面我們結合REST原則,圍繞資源展開,從資源的定義、獲取、表述、狀態變遷等角度列舉一些關鍵概念。
資源與URI
REST全稱是表述性狀態轉移,那究竟指的是什么的表述?其實指的就是資源。
任何事物,只要有被引用到的必要,它就是一個資源。資源可以是實體(例如手機號碼),也可以只是一個抽象概念(例如價值)。
要讓一個資源可以被識別,需要有一個唯一標識。在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
https://github.com/git/git/compare/master…next
下面我們來看看URI設計上的一些技巧:
使用下划線或破折號來讓URI可讀性更好
曾經Web上的URI都是冰冷的數字或者無意義的字符串,但現在越來越多的網站使用下划線【_】或破折號【-】來分割一些單詞,讓URI看上去更為人性化。
使用斜杠來表示資源的層級關系
例如/orders/2018/10可以用來表示2018年10月的訂單記錄。
使用問號來過濾資源
很多人只是把問號【?】簡單地當作是參數的傳遞,很容易造成URI過於復雜和難以理解。其實可以把問號用於對資源的過濾,例如/orders/2018/10用來表示2018年10月的所有訂單記錄,而/orders/2018/10?state=canceled用來表示2018年10月取消的訂單記錄。這種URL通常對應的是一些特定條件的查詢結果或算法運算結果。
使用逗號或分號表示同級資源的關系
有時候我們需要表示統計資源的關系時,可以使用逗號【,】或分號【;】來進行分割。例如需要比較兩個訂單的差異,或者可以使用/orders/2018/10/nb233.nb666作為URI,比較2018年10月的兩個訂單nb233和nb666之間的差異。
github現在則是使用【...】分隔符來做這個事情,比如/git/git/compare/master…next。
統一資源接口
RESTful架構應該遵循統一接口原則,統一接口包含了一組首先的預定義的操作,不論什么樣的資源,都是通過使用相同的接口進行資源的訪問。接口應該使用標准的HTTP方法,如GET,PUT,POST和DELETE,並遵循這些方法的語義。
如果按照HTTP方法的語義來暴露資源,那么接口將會擁有安全性和冪等性的特性,例如GET和HEAD請求都是安全的,無論請求多少次,都不會改變服務器狀態。而GET、HEAD、PUT和DELETE請求都是冪等的,無論對資源操作多少次,結果總是一樣的,后面的請求並不會產生比第一次更多的影響。
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)- 服務端當前無法處理請求
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)- 服務當前無法處理請求
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)- 服務當前無法處理請求
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)- 服務端當前無法處理請求
下面我們來看一些時間中常見的問題:
POST和PUT用於創建資源時有什么區別?
POST和PUT在創建資源的區別在於,所創建的資源的名稱(URI)是否由客戶端決定。
例如為我的博文增加一個java的分類,生成的路徑就是分類名/categories/java,那么就可以采用PUT方法。不過很多人直接把POST、GET、PUT、DELETE直接對應上CRUD,例如在一個典型的rails實現的RESTful應用中就是這么做的。
我認為,這是因為rails默認使用服務端生成的ID作為URI的緣故,而不少人就是通過rails實踐REST的,所以很容易造成這種誤解。
客戶端不一定都支持這些HTTP方法吧?
的確會有這種情況,特別是一些比較古老的基於瀏覽器的客戶端,只能支持GET和POST兩種方法。
在實踐上,客戶端和服務端都可能需要做一些妥協。例如rails框架就支持通過隱藏參數_method=DELETE來傳遞真實的請求方法,而想Backbone這樣的客戶端MVC框架則允許傳遞_method傳輸和設置X-HTTP-Method-Override頭來規避這個問題。
統一接口是否意味着不能擴展帶特殊語義的方法?
統一接口並不阻止你擴展方法,只要方法對資源的操作有着具體的、可識別的語義即可,並能夠保持整個接口的統一性。
像WebDAV就對HTTP方法進行了擴展,增加了LOCK、UPLOCK等方法。而github的API則支持使用PATCH方法來進行issue的更新,例如:
PATCH /repos/:owner/:repo/issues/:number
不過,需要注意的是,像PATCH這種不是HTTP標准方法的,服務端需要考慮客戶端是否能夠支持的問題。
統一資源接口對URI有什么指導意義?
統一資源接口要求使用標准的HTTP方法對資源進行操作,所以URI只應該來表示資源的名稱,而不應該包括資源的操作。通俗來說,URI不應該使用動作來描述:
GET /getUser/1
POST /createUser
PUT /updateUser/1
DELETE /deleteUser/1
這些都是不符合統一接口要求的URI。
如果GET請求增加計數器,是否違反安全性?
安全性不代表請求不產生副作用,例如像很多API開發平台,都對請求流量做限制。像github就會限制沒有認證的請求每小時只能請求60次。但服務端不是為了追求副作用而發出這些GET或HEAD請求的,產生副作用是服務端自作主張的。
另外,服務端在設計時,也不應讓副作用太大,因為客戶端認為這些請求是不會產生副作用的。
直接忽視緩存的做法可取嗎?
即使你按各個動作的原本意圖來使用它們,你仍然可以輕易禁止緩存機制。最簡單的做法就是在你的HTTP響應里增加一個報文頭:
Cache-controll: no-cache
但是這樣的話,你也會同時失去高效緩存與再驗證的支持(使用Etag等機制)。
對於客戶端來說,在為一個REST式服務實現程序客戶端時,也應該充分利用現有的緩存機制,以免每次都重新獲取表示。
響應代碼的處理有必要嗎?
HTTP的響應代碼可用於應付不同場合,正確使用這些狀態代碼意味着客戶端與服務器可以在一個具備較豐富語義的層次上進行溝通。
例如,201(Created)響應代碼表明已經創建了一個新的資源,其URI在Location響應報文頭里。
假如你不利用HTTP狀態代碼豐富的應用語義,那么你將錯失提高重用性、增強互操作性和提高松耦合性的機會。
如果這些所謂的RESTful應用必須通過響應實體才能給出錯誤信息,那么SOAP(簡單對象訪問協議, Simple Object Access Protocol,交換數據的一種協議規范,是一種輕量的、簡單的、基於XML的協議)就是這樣的了,它就能滿足了。
資源的表述
上面提到,客戶端通過HTTP方法可以獲取資源,確切地說,客戶端獲取的只是資源的表述而已。
資源在外界的具體呈現,可以有多種表述形式。在客戶端和服務端之間傳遞的也是資源的表述而不是資源本身。例如文本資源可以采用html、xml、json等格式,圖片可以通過png或jpg展現出來。
資源的表述包括數據和描述數據的元數據,例如HTTP頭Content-Type就是這樣一個元數據屬性。
那么客戶端是如何知道服務端提供的是哪種表述形式呢?
答案是通過HTTP內容協商。客戶端可以通過Accept頭請求一種特定的表述,服務端則通過Content-Type告訴客戶端資源的表述形式。
以github為例,請求某組織資源的json格式的表述形式:
假如github也能夠支持xml格式的表述格式,那么結果就是這樣的:
下面我們來看一些時間上常見的設計:
在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
使用URI后綴來區分表述格式
像rails框架,就支持使用/users.xml或/users.json來區分不同的格式。這樣的方式對於客戶端來說,無疑是更為直觀,但混淆了資源的名稱和資源的表述形式。我個人認為,還是應該優先使用內容寫上來區分表述格式。
如何處理不支持的表述格式
當服務器不支持所請求的表述格式,那么應該怎么辦?若服務器不支持,它應該返回一個HTTP 406響應,表示拒絕處理該請求。
以github為例,展示一個請求XML表述資源的結果:
資源的鏈接
我們知道REST是使用標准的HTTP方法來操作資源的,但僅僅因此就理解成帶CRUD的Web數據庫架構就太過於簡單了。
這種反模式忽略了一個核心概念:超媒體即應用引擎(hypermedia as the engine of application state)。超媒體是什么?當你瀏覽Web網頁時,從一個鏈接跳到一個頁面,再從另一個鏈接跳到另一個頁面,就是利用了超媒體的概念:把一個個的資源鏈接起來。
要達到這個目的,就要求在表述格式里邊加上鏈接來引導客戶端,這種具有鏈接的特性被稱為連通性。
下面展示的是github獲取某個組織下的項目列表的請求,可以看到在響應頭里面增加Link頭,高祖客戶端怎么訪問下一頁和最后一頁的記錄。而在響應體里面,用url來鏈接項目所有者和項目地址。
又例如下面這個例子,創建訂單后通過鏈接引導客戶端如何去付款。
上面的例子展示了如何使用超媒體來增強資源的連通性。很多人在設計RESTful架構時,使用很多時間來尋找漂亮的URI,而忽略了超媒體。所以,應該多花一點時間來給資源的表述提供鏈接,而不是專注於資源的CRUD。
狀態的轉移
有了上面的鋪墊,再討論REST里面的狀態轉移就會很容易理解了。
不過,我們先來討論一下REST原則中的無狀態通信原則。初看一下,好像自相矛盾了,既然無狀態,何來狀態轉移一說?
其實,這里說的無狀態通信原則,並不是說客戶端應用不能有狀態,而是指服務端不應該保存客戶端狀態。
應用狀態與資源狀態
實際上,狀態應該區分應用狀態和資源狀態,客戶端扶着維護應用狀態,而服務端維護資源狀態。
客戶端與服務端的交互必須是無狀態的,並在每一次請求中保函處理改請求所需的一切信息。
服務端不需要在請求間保留應用狀態,只有在接收到實際請求的時候,服務端才會關注應用狀態。
這種無狀態通信原則, 使得服務端和中介能理解獨立的請求和響應。
在多次請求中,同一個客戶端也不再需要依賴同一個服務器,方便實現高可擴展和高可用性的服務端。
但是有時候我們會做出違反無狀態通信原則的設計,例如利用Cookie跟蹤某個服務端會話狀態,常見的像J2EE里面的JSESSIONID。
這就意味着,瀏覽器隨歌詞請求發出去的Cookie是被用於構建會話狀態的。
當然,如果Cookie保存的是一些服務器不依賴於會話狀態即可驗證的消息(比如認證令牌),這樣的Cookie也是符合REST原則的。
應用狀態的轉移
狀態轉移到這里已經很好理解了,會話狀態不是作為資源狀態保存在服務端的,而是被客戶端作為應用狀態進行跟蹤的。客戶端應用狀態太在服務端提供的超媒體的指引下發生變遷。服務端通過超媒體告訴客戶端當前狀態有哪些后續狀態可以進入。
這些類似下一頁之類的鏈接,起的就是這種推進狀態的作用——指引你如何從當前狀態進入下一個可能的狀態。
轉自https://www.runoob.com/w3cnote/restful-architecture.html
"生活這么難,溫柔一點不好嘛。"