REST API 架構約束 — HTTP 統一介面

客戶-伺服端 (Client-Server),或俗稱 主從式架構 (我不太喜歡這說法),
是基於網路應用 (network-based applications) 最常見的架構風格 (architectural styles)。
背後的約束原則是 — 關注點分離 (Separation of Concerns, SoC):

一次注意一件事

 
對於 Web 最重要的部分在
— — 分離 使用者介面 (user interface) 與 資料儲存 (data storage) 觀點。
 
 
因此:

  1. Server 能將 使用者介面 (user interface) 的功能,
    交由 Client 元件 (component) 處理,
    藉此簡化 Server 元件,使其提高 可擴展性 (scalability)。

  2. 將觀點分為 Client 與 Server,
    代表 Web 元件 之間 (i.e., User Agent、Origin Server、Proxy、Gateway),
    能各自 獨立發展 (evolve independently)。
    (e.g., 你能藉由多種平台上網 💻📱⌚️)

 

實現的條件是?

 

介面一致

 


 

統一介面 (uniform interface)

HTTP 旨在:

透過向 客戶端 (Client) 提供與 資源種類無關 的 統一介面 (uniform interface),
藉此 隱藏服務實現的細節。

 
同理:

伺服端 (Server) 不需知道每個客戶端的目的 (purpose),
單獨 的 考慮 每個 HTTP 請求,而非 僅與 特定種類 的 Client 或 應用相關。

 
若 HTTP 完全定義了 Client、Server 具體的實作細節,
不但擴充性受限,其中一方一改變,甚至會迫使對方跟著改變。
 
試想:你的瀏覽器只有 IE 可以用..😱
fear
 
 

設計原則 (Design Principles)

以 軟體設計 角度,審視 HTTP 的 統一介面 (uniform interface) 約束:
 
分離 使用者介面 (user interface) 與 資料儲存 (data storage) 觀點,
使各元件能 獨立發展 (evolve independently)。

將 「會因為相同理由而改變的東西」封裝起來,「會因為不同理由而改變的東西」分離開來,
使單一模組變更的原因只有一種 — — 單一職責原則 (Single Responsibility Principle, SRP)。

 
OCP-in-http
 
Client 依賴的是 抽象 的 介面 (HTTP) 與 表示 (representation),
而非具體的 Server 或 特定的類型 (e.g., html、txt、json),
因此,不用也不該知道 Server 如何實作資源。
 
如此一來,其中一方改變,便不會影響另一方,
例如,配置正確的前提下,Server 更換作業系統、資料庫,
並不會影響到 Client,它甚至毫不知情。

依賴於抽象,而非具體實作 — — 依賴倒置原則 (DIP)。

 
也確保 Client 元件 使用不同 的 Server 元件 時,行為可 維持不變:

對擴充開放,對修改封閉 — — 開閉原則 (OCP),
子元件 (component) 能替換 它們的 父元件 (e.g., user agent、gateway) — — Liskov 替代原則 (LSP)。

 
 

解耦 (decoupling)

當然,還有許多通用的 軟體工程/套件 設計原則、架構模式…,
應用於元件介面上,簡化了整體系統架構的複雜度,提升了彼此交互的 可見性 (visibility),
 
高內聚力 (high cohesion)、鬆散耦合 (loose coupling),
不僅僅適用於程式設計,同時是 架構 至關重要的觀念。
 
例如,Server 能因應需求,實現 技術異質性 (Technology Heterogeneity) [͵hɛtərədʒəˋniətɪ],
能將 貼文、連結、個資 存在 關連式資料庫 (e.g., M$SQL、MySQL),
也能儲存於 文件導向資料庫 (e.g., MongoDB),或將 社交關係 存於 圖學導向資料庫 (e.g., Neo4J)。
MongoDB-Logo
 
最重要的是:

有效地將 服務 與 具體實作 解耦 (decoupled)。

 


 

無狀態 (Stateless)

上述提及:

伺服端 (Server) 不需知道每個客戶端的目的 (purpose),
單獨 的 考慮 每個 HTTP 請求,而非 僅與 特定種類 的 Client 或 應用相關。

 
其背後的支持基礎在於 — — 無狀態 (stateless) 架構約束。
 
無狀態 (Stateless) 是指從 Client 到 Server 的所有請求,
皆需包含『 用於理解此請求 』的 所有 相關信息,不得利用任何儲存在 Server 的上下文,
(例如, 使用者身份、授權憑證、購買紀錄、購物車選項…)
 
假設,我剛剛登入了 A 網站:
screenshot-login
Photo by materiallogindemo.
 
欲前往其他頁面使用服務時 (e.g., 購買、訂閱、儲值),必須提供 授權憑證 (e.g., Cookie、Authorization),而非假定 Server 知道 我們已經登入:

POST /product?id=87 HTTP/1.1

Host: example.com Authorization: Bearer 51d05e71d393f7e228f38d8c8818f079e3dee425 Content-Type: application/json; charset=utf-8 User-Agent: FooBar Content-Length: 136

 

{ "status": "ok", "extended": true, "results": [ {"value": 0, "type": "int64"}, {"value": 1.0e+3, "type": "decimal"} ] }

 
HTTP 被定義為 無狀態協定 (stateless protocol),
意味著 每個請求訊息,可以被單獨地理解,而不需其他 先前知識 (Prior Knowledge)。
 
許多實作依賴於 HTTP 的無狀態設計,
以便 重用代理連線 或 跨多個 Server 的 動態負載平衡 (dynamically load balance) 請求:

會話狀態 (Session) 全部保存在 客戶端 (Client)。

 
因此,除非該連線是安全的 (i.e., HTTPS) 且特定於某 使用者代理:

Server 不得假定 同一連接 上的兩個請求來自同一個 User Agent。

 

可見性、可靠性、可伸縮性

Stateless 架構約束 也帶來了三種架構屬性。
 
提升了 可見性 (visibility):
監視系統不必為了單一請求,前往查看先前的其他請求 (e.g., 登入紀錄)。
 
改善了 可靠性 (reliability):
當系統發生局部故障時,能減輕 復原故障 所需的任務量。
 
帶來了 可伸縮性 (scalability):
不必在多個請求之間保存 (會話) 狀態,從而允許 Server 元件迅速釋放資源,減輕負載,
也因為 Server 不必管理跨請求的資源使用,進一步簡化了實作。
 
 

Server Session Problem

我們來看一下,有狀態 是怎麼一回事。
 
例如,使用者 Jason 在 第一次請求 時進行登入,且 Origin Server A 保存了此狀態:
keep-session-state
 
 
接著,使用者 Jason 想查看信箱並執行 第二次請求,
Origin Server A 剛好較忙碌,因此被分配流量至 Origin Server B:
keep-session-state-2
 
 
最後,因為 Origin Server B 並未保存過 Jason 的登入紀錄,而發生超腦殘錯誤:
show_error
Photo by sweet-alert-dialog.
 
 
別擔心,解法百百種! 你仍可以使用 Server 會話機制。
不過要分清楚:HTTP 並不禁止 Session;它只是把每個請求所需的上下文留在 Server,因而不符合 REST 的 stateless 約束。這在許多網站是合理的工程取捨,不必把 REST 當成宗教。
 
若要保留 Session,同時讓多台 Server 正確接手,可將 session store 放在共享的資料庫或快取服務,或採用可驗證、可撤銷的 token 設計。傳統把 session 寫在單機硬碟的做法,才會造成上述的擴展問題與額外 Disk I/O;是否採用,取決於安全性、撤銷需求與營運複雜度。
 
 

表示狀態傳輸
(Representational State Transfer, REST)

上面提及了 表示狀態傳輸 (Representational State Transfer, REST) (跟我唸一次)。
 
REST 是一 架構風格,源自 HTTP/1.1 的主架構師 — Dr. Roy Thomas Fielding,
於 2000 發表的著名論文 《Architectural Styles and the Design of Network-based Software Architectures》。
 
REST 為當時 Web 架構的設計基礎理論,
旨在在提倡良好的 Web Application 行為。
 
要小心的是:

REST 中的『 狀態 』,是指 資源 與 表示 的狀態,
而非 無狀態 (Stateless) 架構約束 中的 會話狀態。

 
(可惜的是,許多人穿鑿附會、混為一談了 😱)
 
其中,Client-Server、無狀態 (Stateless)、Cache、
統一介面 (uniform interface)、Layered System、可選的 Code-On-Demand,
皆為 REST 重要的 架構約束 之一。
 
而 統一介面 又包含四點 介面約束 (interface constraints),
以指導 元件 (component) 間的行為:
 

  • 資源 的識別 (identification of resources)

  • 以 表示 來操作資源 (manipulation of resources through representations)

  • 自我描述 的訊息 (self-descriptive messages)

  • 以 超媒體 做為應用狀態引擎 (hypermedia as the engine of application state)

 
快吐血的話沒關係,看過有印象即可 😉,
系列文將逐一介紹 REST 架構 與其約束之關係。
 
 

沒有銀彈 (No Silver Bullet)

無狀態 (stateless) 架構約束,同樣反映出 設計的權衡 (design trade-off),
由於 不能依賴 Server 於 共享情境 (shared context) 保存狀態資料,
因此 增加了每次的互動開銷 (一系列請求中發送的 重複資料),例如,使用者授權憑證,
這不僅降低了網路效能,還得依賴不同版本的 Client,要能正確的實作 HTTP 語意。
 
而 統一介面 (uniform interface) 架構約束,則意味著:
需使用 標準化的格式 傳輸/轉移 (transfer) 訊息。
 
因此,HTTP 對於某些形式的架構交互,不一定是最佳的介面。
統一介面 也與客製化的應用需求 產生衝突 (e.g., 非同步), 降低了效率。
 
並且,一旦介面更新,我們無法要求所有元件進行 擴展 與 改變,
版本不一,可能帶來災難性破壞,
這也是為何,Web 相容性總令開發者厭煩。
(e.g., 某些奇怪的 javascript 特性、命名 ; 非標準卻普及的表頭欄位)
 

無論是 HTTP 或背後的指導原則 REST,
皆是權衡利弊後的產物,而非架構的萬靈丹。

 
 

作者: 鄭中勝
喜愛音樂,但不知為何總在打程式 ? 期許能重新審視、整理自身所學,幫助有需要的人。

在〈REST API 架構約束 — HTTP 統一介面〉中有 3 則留言

  1. 文章寫的很用心,但中間廣告篇幅真的太大,建議可以移至右側,會好閱讀很多

發表迴響