什麼是 REST
REST 全稱是 Representational State Transfer,翻譯成中文是『表現層狀態轉移』,估計讀者看到這個詞也是雲裡霧裡的,我當初也是!這裡,我們先不糾結這個詞到底是什麼意思。事實上,REST 是一種 Web 架構風格,它有六條準則,滿足下面六條準則的 Web 架構可以說是 Restuful 的。
- 客戶端-伺服器(Client-Server)伺服器和客戶端之間有明確的界限。一方面,伺服器端不再關注使用者介面和使用者狀態。另一方面,客戶端不再關注資料的儲存問題。這樣,伺服器端跟客戶端可以獨立開發,只要它們共同遵守約定。
- 無狀態(Stateless)來自客戶端的每個請求必須包含伺服器所需要的所有資訊,也就是說,伺服器端不儲存來自客戶端的某個請求的資訊,這些資訊應由客戶端負責維護。
- 可快取(Cachable)伺服器的返回內容可以在通訊鏈的某處被快取,以減少互動次數,提高網路效率。
- 分層系統(Layered System)允許在伺服器和客戶端之間通過引入中間層(比如代理,閘道器等)代替伺服器對客戶端的請求進行回應,而且這些對客戶端來說不需要特別支援。
- 統一介面(Uniform Interface)客戶端和伺服器之間通過統一的介面(比如 GET, POST, PUT, DELETE 等)相互通訊。
- 支援按需程式碼(Code-On-Demand,可選)伺服器可以提供一些程式碼(比如 Javascript)並在客戶端中執行,以擴充套件客戶端的某些功能。
使用 Flask 提供 REST Web 服務
REST Web 服務的核心概念是資源(resources)。資源被 URI(Uniform Resource Identifier, 統一資源識別符號)定位,客戶端使用 HTTP 協議操作這些資源,我們用一句不是很全面的話來概括就是:URI 定位資源,用 HTTP 動詞(GET, POST, PUT, DELETE 等)描述操作。下面列出了 REST 架構 API 中常用的請求方法及其含義:
HTTP Method | Action | Example |
---|---|---|
GET | 從某種資源獲取資訊 | http://example.com/api/articles (獲取所有文章) |
GET | 從某個資源獲取資訊 | http://example.com/api/articles/1 (獲取某篇文章) |
POST | 建立新資源 | http://example.com/api/articles (建立新文章) |
PUT | 更新資源 | http://example.com/api/articles/1 (更新文章) |
DELETE | 刪除資源 | http://example.com/api/articels/1 (刪除文章) |
設計一個簡單的 Web Service
現在假設我們要為一個 blog 應用設計一個 Web Service。
首先,我們先明確訪問該 Service 的根地址是什麼。這裡,我們可以這樣定義:
1 |
http://[hostname]/blog/api/ |
然後,我們明確有哪些資源是要公開的。可以知道,我們這個 blog 應用的資源就是 articles。
下一步,我們要明確怎麼去操作這些資源,如下所示:
HTTP Method | URI | Action |
---|---|---|
GET | http://[hostname]/blog/api/articles | 獲取所有文章列表 |
GET | http://[hostname]/blog/api/articles/[article_id] | 獲取某篇文章內容 |
POST | http://[hostname]/blog/api/articles | 建立一篇新的文章 |
PUT | http://[hostname]/blog/api/articles/[article_id] | 更新某篇文章 |
DELETE | http://[hostname]/blog/api/articles/[article_id] | 刪除某篇文章 |
為了簡便,我們定義一篇 article 的屬性如下:
- id:文章的 id,Numeric 型別
- title: 文章的標題,String 型別
- content: 文章的內容,TEXT 型別
至此,我們基本完成了這個 Web Service 的設計,下面我們就來實現它。
使用 Flask 提供 RESTful api
在實現這個 Web 服務之前,我們還有一個問題沒有考慮到:我們應該怎麼儲存我們的資料。毫無疑問,我們應該使用資料庫,比如 MySql、MongoDB 等。但是,資料庫的儲存不是我們這裡要討論的重點,所以我們採用一種偷懶的做法:使用一個記憶體中的資料結構來代替資料庫。
GET 方法
下面我們使用 GET 方法獲取資源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# -*- coding: utf-8 -*- from flask import Flask, jsonify, abort, make_response app = Flask(__name__) articles = [ { 'id': 1, 'title': 'the way to python', 'content': 'tuple, list, dict' }, { 'id': 2, 'title': 'the way to REST', 'content': 'GET, POST, PUT' } ] @app.route('/blog/api/articles', methods=['GET']) def get_articles(): """ 獲取所有文章列表 """ return jsonify({'articles': articles}) @app.route('/blog/api/articles/<int:article_id>', methods=['GET']) def get_article(article_id): """ 獲取某篇文章 """ article = filter(lambda a: a['id'] == article_id, articles) if len(article) == 0: abort(404) return jsonify({'article': article[0]}) @app.errorhandler(404) def not_found(error): return make_response(jsonify({'error': 'Not found'}), 404) if __name__ == '__main__': app.run(host='127.0.0.1', port=5632, debug=True) |
將上面的程式碼儲存為檔案 app.py
,通過 python app.py
啟動這個 Web Service。
接下來,我們進行測試。這裡,我們採用命令列語句 curl 進行測試。
開啟終端,敲入如下命令進行測試:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
$ curl -i http://localhost:5632/blog/api/articles HTTP/1.0 200 OK Content-Type: application/json Content-Length: 224 Server: Werkzeug/0.11.4 Python/2.7.11 Date: Tue, 16 Aug 2016 15:21:45 GMT { "articles": [ { "content": "tuple, list, dict", "id": 1, "title": "the way to python" }, { "content": "GET, POST, PUT", "id": 2, "title": "the way to REST" } ] } $ curl -i http://localhost:5632/blog/api/articles/2 HTTP/1.0 200 OK Content-Type: application/json Content-Length: 101 Server: Werkzeug/0.11.4 Python/2.7.11 Date: Wed, 17 Aug 2016 02:37:48 GMT { "article": { "content": "GET, POST, PUT", "id": 2, "title": "the way to REST" } } $ curl -i http://localhost:5632/blog/api/articles/3 HTTP/1.0 404 NOT FOUND Content-Type: application/json Content-Length: 26 Server: Werkzeug/0.11.4 Python/2.7.11 Date: Wed, 17 Aug 2016 02:32:10 GMT { "error": "Not found" } |
上面,我們分別測試了『獲取所有文章列表』、『獲取某篇文章』和『獲取不存在的文章』這三個功能,結果也正是我們所預料的。
POST 方法
下面我們使用 POST 方法建立一個新的資源。在上面的程式碼中新增以下程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 |
from flask import request @app.route('/blog/api/articles', methods=['POST']) def create_article(): if not request.json or not 'title' in request.json: abort(400) article = { 'id': articles[-1]['id'] + 1, 'title': request.json['title'], 'content': request.json.get('content', '') } articles.append(article) return jsonify({'article': article}), 201 |
測試如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$ curl -i -H "Content-Type: application/json" -X POST -d '{"title":"the way to java"}' http://localhost:5632/blog/api/articles HTTP/1.0 201 CREATED Content-Type: application/json Content-Length: 87 Server: Werkzeug/0.11.4 Python/2.7.11 Date: Wed, 17 Aug 2016 03:07:14 GMT { "article": { "content": "", "id": 3, "title": "the way to java" } } |
可以看到,建立一篇新的文章也是很簡單的。request.json 儲存了請求中的 JSON 格式的資料。如果請求中沒有資料,或者資料中沒有 title 的內容,我們將會返回一個 “Bad Request” 的 400 錯誤。如果資料合法(必須要有 title 的欄位),我們就會建立一篇新的文章。
PUT 方法
下面我們使用 PUT 方法更新文章,繼續新增程式碼:
1 2 3 4 5 6 7 8 9 10 11 |
@app.route('/blog/api/articles/<int:article_id>', methods=['PUT']) def update_article(article_id): article = filter(lambda a: a['id'] == article_id, articles) if len(article) == 0: abort(404) if not request.json: abort(400) article[0]['title'] = request.json.get('title', article[0]['title']) article[0]['content'] = request.json.get('content', article[0]['content']) return jsonify({'article': article[0]}) |
測試如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$ curl -i -H "Content-Type: application/json" -X PUT -d '{"content": "hello, rest"}' http://localhost:5632/blog/api/articles/2 HTTP/1.0 200 OK Content-Type: application/json Content-Length: 98 Server: Werkzeug/0.11.4 Python/2.7.11 Date: Wed, 17 Aug 2016 03:44:09 GMT { "article": { "content": "hello, rest", "id": 2, "title": "the way to REST" } } |
可以看到,更新文章也是很簡單的,上面我們更新了第 2 篇文章的內容。
DELETE 方法
下面我們使用 DELETE 方法刪除文章,繼續新增程式碼:
1 2 3 4 5 6 7 |
@app.route('/blog/api/articles/<int:article_id>', methods=['DELETE']) def delete_article(article_id): article = filter(lambda t: t['id'] == article_id, articles) if len(article) == 0: abort(404) articles.remove(article[0]) return jsonify({'result': True}) |
測試如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
$ curl -i -H "Content-Type: application/json" -X DELETE http://localhost:5632/blog/api/articles/2 HTTP/1.0 200 OK Content-Type: application/json Content-Length: 20 Server: Werkzeug/0.11.4 Python/2.7.11 Date: Wed, 17 Aug 2016 03:46:04 GMT { "result": true } $ curl -i http://localhost:5632/blog/api/articles HTTP/1.0 200 OK Content-Type: application/json Content-Length: 125 Server: Werkzeug/0.11.4 Python/2.7.11 Date: Wed, 17 Aug 2016 03:46:09 GMT { "articles": [ { "content": "tuple, list, dict", "id": 1, "title": "the way to python" } ] } |
附錄
常見 HTTP 狀態碼
HTTP 狀態碼主要有以下幾類:
- 1xx —— 後設資料
- 2xx —— 正確的響應
- 3xx —— 重定向
- 4xx —— 客戶端錯誤
- 5xx —— 服務端錯誤
常見的 HTTP 狀態碼可見以下表格:
程式碼 | 說明 |
---|---|
100 | Continue。客戶端應當繼續傳送請求。 |
200 | OK。請求已成功,請求所希望的響應頭或資料體將隨此響應返回。 |
201 | Created。請求成功,並且伺服器建立了新的資源。 |
301 | Moved Permanently。請求的網頁已永久移動到新位置。 伺服器返回此響應(對 GET 或 HEAD 請求的響應)時,會自動將請求者轉到新位置。 |
302 | Found。伺服器目前從不同位置的網頁響應請求,但請求者應繼續使用原有位置來進行以後的請求。 |
400 | Bad Request。伺服器不理解請求的語法。 |
401 | Unauthorized。請求要求身份驗證。 對於需要登入的網頁,伺服器可能返回此響應。 |
403 | Forbidden。伺服器拒絕請求。 |
404 | Not Found。伺服器找不到請求的網頁。 |
500 | Internal Server Error。伺服器遇到錯誤,無法完成請求。 |
curl 命令參考
選項 | 作用 |
---|---|
-X | 指定 HTTP 請求方法,如 POST,GET, PUT |
-H | 指定請求頭,例如 Content-type:application/json |
-d | 指定請求資料 |
–data-binary | 指定傳送的檔案 |
-i | 顯示響應頭部資訊 |
-u | 指定認證使用者名稱與密碼 |
-v | 輸出請求頭部資訊 |
完整程式碼
本文的完整程式碼可在 Gist 檢視。