微服務協議篇之REST

概述

微服務所使用的協議自然要根據服務的特點和類型來選擇

微服務類型 推薦協議 推薦理由
Web Service Restful via HTTP 簡單實用, 應用廣泛
VoIP 及 Telephony Service 信令用SIP, 媒體用RTP 支持的終端和媒體網關眾多
多媒體流服務 Multimedia Stream Service RTP/SRTP/RTSP 基于傳輸延遲考慮
實時消息服務 Realtime Message Service XMPP, PDU via TCP XMPP 是開源的標準協議, 效率不高,手機應用不推薦
異步消息服務 Async Message Service JMS/AMQP ActiveMQ 用 JMS, RabbitMQ 用后者

這里說的協議主要是指應用層協議, 傳輸層協議一般都是TCP, 除非是媒體傳輸考慮用低延遲的 UDP

簡單來說, 一般的信令控制協議用基于 HTTP 的 REST 協議就夠了, 或者是基于 TCP /WebSocket 的用 Protobuf 來封裝應用層消息體也不錯.

SIP/SDP/MGCP 在電話及語音服務領域應用較廣

媒體傳輸一般用 RTPSRTPRTSP 來承載音頻或視頻, 在多方會議共享及遠程控制應用中也常用如下協議

REST

先從應用最廣的 REST 說起, REST (Representational State Transfer) 可表現的狀態遷移, 是2000年由 Roy Fielding 在他的關于REST的博士論文中提出的.

REST準確來說不算是一種協議, 而是一種設計分布式系統的架構風格, 它是指資源在網絡中以某種表現形式進行狀態轉移.

也就是說它是面向資源的, 每種資源都有相對應的URI, 每個URI 都指向一個資源, 而資源是可展現的(Representational ) 和有狀態的(state), 而HTTP 請求則是無狀態的, 即它不需要依賴其它的請求, 每個請求都是相對獨立的, 超媒體 Hypermedia 可以通過 鏈接Link 和 URI 把資源連接起來, Web成功的秘訣也就是用鏈接把世界連接起來.

這里主要指用 HTTP 和 Json 承載的面向資源的 Restful 風格的協議.
由于HTTP協議比較簡單, 系統對外的接口被分為多個資源 API, 都可以獨立地進行測試, 并且符合無狀態通信的原則, 天然具有比較好的松耦合性和可伸縮性.

在介紹完它的特性之后, 我們就會明白它為什么會在分布式系統中大受歡迎

REST 的特點

  • REpresentational State Transfer 可表現的狀態遷移
  • Nouns, not verbs, in endpoints 在各端點中資源是名詞而非動詞
  • All state the client needs is queryable 客戶端所需的所有狀態是可查詢到的
  • Server has a complete picture of system state 服務端具有完整的系統狀態
  • Particularly useful for intermittently-connected clients 對間斷性連接的客戶端特別有用

REST 的好處

  • 簡單
    HTTP + Json 地球人都知道,HTTP method 表示對于資源的 CRUD 簡單明了

  • 可伸縮
    短連接,無狀態,易于橫向擴展

  • 松耦合
    基于 URL 和 API 的協作,保持接口簡單,一致和穩定,避免產生復雜的網狀結構和閉環,耦合自然沒那么緊

REST 的風格

  1. 客戶-服務器(Client-Server)
    通信只能由客戶端單方面發起,表現為請求-響應的形式。

  2. 無狀態(Stateless)
    通信的會話狀態(Session State)應該全部由客戶端負責維護。

  3. 緩存(Cache)
    響應內容可以在通信鏈的某處被緩存,以改善網絡效率。

  4. 統一接口(Uniform Interface)
    通信鏈的組件之間通過統一的接口相互通信,以提高交互的可見性。

  5. 分層系統(Layered System)
    通過限制組件的行為(即,每個組件只能“看到”與其交互的緊鄰層),將架構分解為若干等級的層。

  6. 按需代碼(Code-On-Demand,可選)
    支持通過下載并執行一些代碼(例如Java Applet、Flash或JavaScript),對客戶端的功能進行擴展。

REST 的特性

  • 面向資源 Resource Oriented
    要考慮合適的粒度, 可緩存性, 修改頻率和可變性
  • 可尋址 Addressability
  • 連通性 Connectedness
  • 無狀態 Statelessness
  • 統一接口 Uniform Interface
    POST, GET, PUT, DELETE , PATCH, HEAD, OPTIONS, TRACE, Connect
  • 超文本驅動 Hypertext Driven

REST 的原則

  • 它基于無狀態, 客戶端-服務器, 可緩存的通訊協議
  • 資源以易于理解的目錄結構的URI 來公布
  • 以JSON或XML形式傳輸來表示數據對象和屬性。
  • 消息明確地使用了 HTTP 方法(例如,GET,POST,PUT和DELETE)。
  • 在HTTP請求與請求之間的無狀態交互不在服務器上存儲客戶端上下文。
    狀態依賴性限制了可擴展性, 所以在客戶端存儲會話狀態使得橫向擴展更加容易

用 HTTP 方法來表示 CRUD

格式為 [HTTP Method] https://host/{service}/{apiclass}/v{version}

HTTP 方法 含義 冪等嗎?
POST 創建資源 Create N
GET 獲取或查詢資源 Retrieve Y
PUT 全部替換資源 Update Y
DELETE 刪除資源 Delete Y
PATCH 部分修改資源 N
HEAD 類似于 GET, 但是只傳輸狀態行和 HTTP 頭 Y
OPTIONS 描述目標資源的通信選項 Y
TRACE 執行沿目標資源路徑的消息環回測試。 Y
CONNECT 建立到由給定URI標識的服務器的隧道 Y

所謂冪等性 Idempotence, 它的意思是你調用一次和調用多次的效果是一樣的

簡單列舉一下一些在 REST 中常用的 Http header

常見的 Http 頭域

Header name Header value example 備注
Accept application/json Respond 406 not acceptable if not support the format
Content-Type application/json 媒體內容類型,
If-Modified-Since Respond 304 not modified if the data is not changed
If-None-Match Respond 304 not modified if the data is not changed
If-Match 412 precondition failed if the ETag is not matched
ETag The version of the resource for integrity
Location 201 response contains it contains the URI of the new created resource

還有一些擴展頭:

X-Forwarded-For

HTTP 請求到達 HTTP Server 的時候往往已經過了反向代理服務器,所以這時候看到的 TCP 源地址已經不是真正的客戶端應用的地址了,這個擴展頭就是代理服務器所添加的真正的 source IP 地址, 它由 https://tools.ietf.org/html/rfc7239 定義

比如在 Citrix 的負載均衡器 netscaler 可以這樣配置, 參見insert client ip to http header

    set service Service-HTTP-1 -CIP enabled X-Forwarded-For

Origin 和 Access-Control-Allow-Origin

現代瀏覽器允許突破同源策略(Same Origin Policy), 使用稱為跨域資源共享 CORS(Cross-Origin Resource Sharing), 微軟的 IE8/9 并不支持,需要用 XDomainRequest 替換 XHTTPRequest

例如請求頭如下,表示請求源自哪里:

Origin: https://www.example.com

響應頭有

Access-Control-Allow-Headers: AppId, MetricsTicket, ConfID, SiteID, TimeStamp, APPName, Ver
Access-Control-Allow-Methods: OPTIONS, POST, PUT
Access-Control-Allow-Origin: https://www.example.com

這樣一來, XHTTPRequest 對 www.example.com 的訪問就是合法的。

X-RateLimit-Limit

現在許多 public API 都限定了客戶端的請求頻率, 比如 twitter, github 等,在響應頭中有如下擴展頭:

  • X-RateLimit-Limit: 單位時間的訪問上限
  • X-RateLimit-Remaining: 剩余的訪問次數
  • X-RateLimit-Reset 訪問次數重置的時間

常見的 Http 狀態碼

2xx

  • 200 OK with Etag head
  • 201 Created with Location head
  • 204 No content
  • 206 Partial content

3xx

  • 301 Move Permanently
  • 302 Found
  • 304 Not Modified

4xx

  • 401 Unauthorized with WWW-Authorizate head
  • 403 Forbidden
  • 404 Not Found
  • 405 Not Allowed with Allow head
  • 406 Not Acceptable
  • 409 Conflict
  • 410 Gone
  • 412 Precondition Failed
  • 413 Request Entity Too Large
  • 415 Unsupported Media Type
  • 451 Unavailable For Legal Reasons

5xx

  • 500 Internal Server Error
  • 501 Not Implemented
  • 502 Bad Gateway
  • 503 Service Unavailable with Retry-After head
  • 504 Gateway Timeout

URI 設計

REST 是面向資源的, 如何定位和尋找資源呢, 就象找人一樣, 資源也需要象人那樣的身份證號碼 URI

URI

在設計資源URI 的時候,

  • 一是要注意它們是名詞,
  • 二是要注意區分單復數
  • 三是要注意 URI 有長度限制, 建議小于1k
  • 四是要注意在 URI 中不要放未經加密的敏感信息, 即使使用TLS/HTTPS

我們可以用

常用方法

緩存控制

我們可以利用一些 HTTP Header 來控制資源的緩存以及防止并發問題

  • ETag 實體標簽: 一般為資源實體的哈希值
  • Expires 過期的時間: Expires: Thu, 01 Feb 2015 17:00:00 GMT
  • Cache-Control 可以有如下屬性
    • public 公有緩存
    • private 私有緩存
    • no-cache 不可緩存
    • max-age 緩存的最大時間, 單位為秒, 一般來說 max-age是相對時間, 比 Expires 的絕對時間要好, 不會有客戶端和服務器時間誤差的問題, 優先使用它
  • Age 緩存了多少秒
  • Last-Modified 資源的最后修改時間
  • If-None-Match 如果不匹配的話
  • If-Modified-Since 從何時起資源有更新
  • If-Match 如果匹配的話
  • if-Unmodified-Since 從何時起資源無更新

當服務器發現Http請求的 Header 中有 If-None-Match, 就取出它的值, 與緩存中的資源的Etag 比較, 如果一致的話, 返回 304 Not Modified, 節省從數據庫查詢和網絡傳輸成本

當服務器發現Http請求的 Header 中 If-Modified-Since, 就取出它的值, 與緩存中的資源的Last-Modified 比較, 如果 If-Modified-Since中指示的最后修改時間大于或等于資源的Last-Modified時間的話, 也返回 304 Not Modified, 即它是從資源最后一次修改之后獲取的, 最近無更改, 無需重新查詢

當然, 如果不一致的話, 則得重新查詢數據庫并刷新緩存, 返回最新的資源信息, 狀態為 200 OK

并發控制

如果多個請求對資源進行修改, 會出現丟失修改或者無效刪除的情況

試想, 張三和李四都是公司的會計, 張三管考勤, 發現王二上個月遲到了三次, 要扣王二三百元錢, 李四管績效, 要給王二增加一千元獎金, 假設王二工資為八千元.

張三修改王二工資為 8000 - 300 = 7700

     update payroll  set salary=7700 where username="wang2" and salary=8000

李四修改王二工資為 8000 + 1000 = 9000

     update payroll  set salary=9000 where username="wang2" and salary=8000

強一致性的關系數據庫使用行級鎖, 張三和李四只有一個會成功, 另一個會修改失敗, 返回給其中一個用戶412錯誤, 讓用戶重新修改. 從而使王二的最終薪水為8000-300+1000=8700

一些不支持強事務的NOSQL存儲, 特別是一些KV系統只能根據key - username來修改數據, 就極有可能出現張三和李四都返回成功, 王二工資變成了7700或9000, 而不是正確的8700, 這時候我們就可以用下面的方法來減少這種情況的發生.

  • 更新數據

當服務器發現Http Header 中有 If-Match, 就取出它的值, 與當前資源的Etag 比較, 如果一致的話, 修改數據返回200 OK, 否則返回 412 Precondition Failed

當服務器發現Http Header 中有if-Unmodified-Since, 就取出它的值, 與當前資源的 Last-Modified 進行比較, 如果發現if-Unmodified-Since值大于或等于Last-Modified資源的的話, 修改數據返回200 OK, 否則返回 412 Precondition Failed

  • 刪除數據

當服務器發現Http Header 中有 if-Match, 就取出它的值, 與當前資源的Etag 比較, 如果一致的話,刪除數據返回 204 No Content, 否則返回 412 Precondition Failed

當服務器發現Http Header 中有 if-Unmodified-Since, 就取出它的值, 與當前資源的 Last-Modified 進行比較, 如果發現if-Unmodified-Since值大于或等于Last-Modified資源的的話, 修改數據返回 204 No Content, 否則返回 412 Precondition Failed

批量處理

例如我們想一次提交多個請求, 可以用這種方法

Request

POST /api/v1/batch
{
        "requests": [
          {
            "method": "POST",
            "path": "/phonenumbers",
           “headers”: [ {“Content-Type”: “application/json”}]
            "body": {
                       "number": "86-01012345678",
                       "type": "mobile"
                        }
          },
          {
            "method": "POST",
            "path": "/telephonydomains/$telephonyDomainID/dialnumbers",
             "body": {
                       "number": "86-01022345678",
                       "type": "office"
                        }
          }
        ]
}

Response

HTTP/1.1 200 OK
{
“response” [
{
“status”: 200,
“message”: “OK”
 “headers”: [ {“Content-Type”: “application/json”}]
“body”: {}
},
{
“status”: 412,
“message”: “Preconditionl Failed”
 “body”: {}
}
]
}

查詢條件超長或者查詢參數有敏感信息

用 POST 來代替 GET , 意謂創建一個查詢

Request:

POST /accounts/queries
{
“userIds”: [111, 222, 333]
}

Response:

HTTP/1.1200 OK
[
…accounts …
]

異步請求

與同步請求不同的是, 不是立即返回結果, 而是先給一個 taskId, 可供稍后查詢結果, 或者在請求時給一個回調URL, 稍后把結果通知回去

Request

POST https://abc.cde.com/api/v1.0/migrations HTTP 1.1
{
   pool: "china",
   notifyUri: 'https://abc.cde.com/api/v1/migrations/123'
}

Response

{
    "status": 'pending',
    "taskID": 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
}

實例: 帳號管理的微服務

光說不練假把式, 先拿python 來寫一個微服務原型, 我們平常會使用諸多網站, 帳號密碼經常忘記, 所以讓我們花一點時間寫一個帳號管理的微服務, 基本功能是記錄我們常用的帳號和密碼, 以免遺忘, 一切從簡, 不用id, 而是用sitename 作為主鍵

method description
GET /accounts 帳戶列表
GET /accounts/<siteName> 帳戶獲取
POST /accounts 帳戶創建
PUT /accounts/<siteName> 帳戶修改
DELETE /accounts/<siteName> 帳戶刪除
  • 客戶端用 httpie 來作測試
  • 服務器端用 python flask 框架來實現
  • 頁面的UI 暫且省略

先安裝python 和 virtualenv

brew install python
brew install pyenv-virtualenv

or
sudo pip install virtualenv

再運行 virtual env

virtualenv venv
source venv/bin/activate

再安裝所需的類庫

pip install flask
pip install flask-httpauth
pip install requests
pip install httpie

為簡單起見, 用 json 文件代替數據庫: account.json

{
"jianshu":{
  "userName":  "walterfan",
  "password": "password",
  "siteName": "jianshu",
  "siteUrl": "http://www.lxweimin.com/users/e0b365801f48"
},

"weibo":{
  "userName":  "fanyamin",
  "password": "password",
  "siteName": "weibo",
  "siteUrl": "http://weibo.com/fanyamin"
}
}

源碼如下, 不算空行, 100行之內搞定: account.py, 可讀寫json file, 并對其中的記錄進行增刪改查, 暫不考慮性能和其他異常及并發處理, 差強人意, 僅供演示, 個人日常使用也行

import os
import json
import requests
from flask_httpauth import HTTPBasicAuth
from flask import make_response
from flask import Flask
from flask import request
from werkzeug.exceptions import NotFound, ServiceUnavailable

app = Flask(__name__)

current_path = os.path.dirname(os.path.realpath(__file__))

auth = HTTPBasicAuth()

users = {
    "walter": "pass1"
}

json_file = "{}/account.json".format(current_path)

def read_data():
    json_fp = open(json_file, "r")
    return json.load(json_fp)

def save_data(accounts):
    json_fp = open(json_file, "w")
    json.dump(accounts, json_fp, sort_keys = True, indent = 4)

@auth.get_password
def get_pw(username):
    if username in users:
        return users.get(username)
    return None

def generate_response(arg, response_code=200):
    response = make_response(json.dumps(arg, sort_keys = True, indent=4))
    response.headers['Content-type'] = "application/json"
    response.status_code = response_code
    return response

@app.route("/", methods=['GET'])
@auth.login_required
def index():
    return generate_response({
        "username": auth.username(),
        "description": "account micro service /accounts"
    })

@auth.login_required
@app.route("/accounts", methods=['GET'])
def list_account():
    accounts = read_data()
    return generate_response(accounts)

#Create account
@auth.login_required
@app.route('/accounts', methods=['POST'])
def create_account():
    account = request.json
    sitename = account["siteName"]
    accounts = read_data()
    if sitename in accounts:
        return generate_response({"error": "conflict"}, 409)
    accounts[sitename] = account
    save_data(accounts)
    return generate_response(account)

#Retrieve account
@auth.login_required
@app.route('/accounts/<sitename>', methods=['GET'])
def retrieve_account(sitename):
    accounts = read_data()
    if sitename not in accounts:
        return generate_response({"error": "not found"}, 404)

    return generate_response(accounts[sitename])

#Update account
@auth.login_required
@app.route('/accounts/<sitename>', methods=['PUT'])
def update_account(sitename):
    accounts = read_data()
    if sitename not in accounts:
        return generate_response({"error": "not found"}, 404)

    account = request.json
    print(account)
    accounts[sitename] = account
    save_data(accounts)
    return generate_response(account)

#Delete account
@auth.login_required
@app.route('/accounts/<sitename>', methods=['DELETE'])
def delete_account(sitename):
    accounts = read_data()
    if sitename not in accounts:
        return generate_response({"error": "not found"}, 404)

    del(accounts[sitename])
    save_data(accounts)
    return generate_response("", 204)

if __name__ == "__main__":
    app.run(port=5000, debug=True)

直接運行 python account.py 這個帳戶管理的RESTful 微服務就啟動了, 用 httpie 測試一下

  • list accounts
(venv) $ http --json --auth walter:pass GET http://localhost:5000/accounts
HTTP/1.0 200 OK
Content-Length: 347
Content-type: application/json
Date: Sat, 10 Dec 2016 15:43:53 GMT
Server: Werkzeug/0.11.11 Python/3.5.1

{
    "jianshu": {
        "password": "password",
        "siteName": "jianshu",
        "siteUrl": "http://www.lxweimin.com/users/e0b365801f48",
        "userName": "walterfan"
    },
    "weibo": {
        "password": "password",
        "siteName": "weibo",
        "siteUrl": "http://weibo.com/fanyamin",
        "userName": "fanyamin"
    }
}
  • create account
http --auth walter:pass --json POST http://localhost:5000/accounts userName=walter password=pass siteName=163 siteUrl=http://163.com
HTTP/1.0 200 OK
Content-Length: 108
Content-type: application/json
Date: Sat, 10 Dec 2016 15:48:59 GMT
Server: Werkzeug/0.11.11 Python/3.5.1

{
    "password": "pass",
    "siteName": "163",
    "siteUrl": "http://163.com",
    "userName": "walter"
}
  • retrieve account
http --auth walter:pass --json GET http://localhost:5000/accounts/163
HTTP/1.0 200 OK
Content-Length: 108
Content-type: application/json
Date: Sat, 10 Dec 2016 15:49:21 GMT
Server: Werkzeug/0.11.11 Python/3.5.1

{
    "password": "pass",
    "siteName": "163",
    "siteUrl": "http://163.com",
    "userName": "walter"
}
  • update account
http --auth walter:pass --json PUT http://localhost:5000/accounts/163 userName=walter password=pass123 siteName=163 siteUrl=http://163.com
HTTP/1.0 200 OK
Content-Length: 111
Content-type: application/json
Date: Sat, 10 Dec 2016 15:49:47 GMT
Server: Werkzeug/0.11.11 Python/3.5.1

{
    "password": "pass123",
    "siteName": "163",
    "siteUrl": "http://163.com",
    "userName": "walter"
}
  • delete account
http --auth walter:pass --json DELETE http://localhost:5000/accounts/163
HTTP/1.0 204 NO CONTENT
Content-Length: 0
Content-type: application/json
Date: Sat, 10 Dec 2016 15:50:18 GMT
Server: Werkzeug/0.11.11 Python/3.5.

參考文檔與鏈接

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 一說到REST,我想大家的第一反應就是“啊,就是那種前后臺通信方式?!钡窃谝笤敿氈v述它所提出的各個約束,以及如...
    時待吾閱讀 3,449評論 0 19
  • API定義規范 本規范設計基于如下使用場景: 請求頻率不是非常高:如果產品的使用周期內請求頻率非常高,建議使用雙通...
    有涯逐無涯閱讀 2,586評論 0 6
  • 本篇文章篇幅比較長,先來個思維導圖預覽一下。 一、概述 1.計算機網絡體系結構分層 2.TCP/IP 通信傳輸流 ...
    滌生_Woo閱讀 55,218評論 24 557
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,868評論 18 139
  • 原文鏈接:http://tech110.blog.51cto.com/438717/549764 Http的Cac...
    五鮮譜閱讀 786評論 0 2