客戶-伺服端 (Client-Server),或俗稱 主從式架構 (我不太喜歡這說法),
是基於網路應用 (network-based applications) 最常見的架構風格 (architectural styles)。
背後的約束原則是 — 關注點分離 (Separation of Concerns, SoC):
一次注意一件事
對於 Web 最重要的部分在
— — 分離 使用者介面 (user interface) 與 資料儲存 (data storage) 觀點。
因此:
Server 能將 使用者介面 (user interface) 的功能,
交由 Client 元件 (component) 處理,
藉此簡化 Server 元件,使其提高 可擴展性 (scalability)。將觀點分為 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 可以用..😱
設計原則 (Design Principles)
以 軟體設計 角度,審視 HTTP 的 統一介面 (uniform interface) 約束:
分離 使用者介面 (user interface) 與 資料儲存 (data storage) 觀點,
使各元件能 獨立發展 (evolve independently)。
將 「會因為相同理由而改變的東西」封裝起來,「會因為不同理由而改變的東西」分離開來,
使單一模組變更的原因只有一種 — — 單一職責原則 (Single Responsibility Principle, SRP)。
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)。
最重要的是:
有效地將 服務 與 具體實作 解耦 (decoupled)。
無狀態 (Stateless)
上述提及:
伺服端 (Server) 不需知道每個客戶端的目的 (purpose),
單獨 的 考慮 每個 HTTP 請求,而非 僅與 特定種類 的 Client 或 應用相關。
其背後的支持基礎在於 — — 無狀態 (stateless) 架構約束。
無狀態 (Stateless) 是指從 Client 到 Server 的所有請求,
皆需包含『 用於理解此請求 』的 所有 相關信息,不得利用任何儲存在 Server 的上下文,
(例如, 使用者身份、授權憑證、購買紀錄、購物車選項…)
假設,我剛剛登入了 A 網站:
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 保存了此狀態:
接著,使用者 Jason 想查看信箱並執行 第二次請求,
Origin Server A 剛好較忙碌,因此被分配流量至 Origin Server B:
最後,因為 Origin Sever B 並未保存過 Jason 的登入紀錄,而發生超腦殘錯誤:
Photo by sweet-alert-dialog.
別擔心,解法百百種! 你仍可以使用 Server 會話機制!
儘管違背了 REST 架構風格,考量業務需求與便利性,
大部分後端框架/語言 皆支援 Session 功能 (e.g., Laraval)。
然而,許多傳統 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 則留言
separation of concerns 的 separation 拚錯了
已修正!非常感謝 😁
文章寫的很用心,但中間廣告篇幅真的太大,建議可以移至右側,會好閱讀很多