簡易RPC框架:需求與設計

1 需求分析

RPC 全稱 Remote Procedure Call ,簡單地來說,它能讓使用者像調用本地方法一樣,調用遠程的接口,而不需要關注底層的具體細節。

例如車輛違章代辦功能,如果車輛因爲某種原因違章,只需要通過這個違章代辦功能(它也許是個APP),我們就能動動手指,而省去了一些跑腿的工作。

不像微服務背景下大家所說的 RPC 框架,如 Dubbo 之類。這個 RPC 框架不提供過多的關於服務註冊、服務發現、服務管理等功能。它針對的是這樣的一些場景:在內部網絡,或者局域網內,兩個屬於同個業務的系統之間需要通信,而我們又覺得去設計多一種二進制網絡協議過於繁瑣並且沒有必要,這時候如果給客戶端開發者一些明確的接口,讓他知道實現什麼功能該調用什麼接口,那麼省去的工作量以及開發效率上的提升不言而喻。

這個 RPC 系統基於 Java 語言實現,需求如下:

  • RPC 服務端可以通過一條長連接發佈多個接口(Interface),客戶端按需生成對應接口的代理。
  • RPC 客戶端也可以發佈接口,以便在必要的時候,服務端可以主動調用客戶端的接口實現
  • 客戶端與服務端之間保持長連接並且維持心跳
  • 服務端針對不同的接口實現,可以指定不同的線程池去處理
  • 序列化協議支持擴展
  • 通信協議與具體編程語言無關
  • 支持併發調用,一個RPC客戶端實例要求是線程安全的

2. 通信協議設計

高效的通信協議一般是二進制格式的,比較常見的還有文本協議比如說HTTP,爲了追求效率,這個 RPC 框架就採用二進制格式。

協議的基本要素

魔數

要瞭解到,報文是在網絡上傳輸的,安全性比較低,因此有必要採取一些措施使得並不是任何人都可以隨隨便便往我們的端口上發東西,因此我們對報文要有一個初步的識別功能,這時候“魔數(magic number)”就派上用場了。魔數並不受任何規範約束,沒有人可以要求你的魔數應該遵循什麼規範,實際上魔數只是我們通信雙方都約定的一個“暗號”,不知道這個暗號的人就無法參與進通信中。例如 Java 源文件編譯後的 class 文件開頭就有一個魔數:0xCAFEBABE,隨隨便便打開一個class文件用十六進制編輯器查看,就能看到。

class文件

Java 虛擬機加載 class 的時候會先驗證魔數。如果不是 CAFEBABE 就認爲是不合法的 class 文件,並拒絕加載。

不過魔數起到的安全防範作用是非常有限的,“有心人”可以通過抓取網絡包就識別出魔數了。因此魔數這個東西其實是“防君子不防小人”。

協議版本

一個協議可能也會有多個版本,例如說 HTTP1.0 和 HTTP1.1,不同版本的協議元素可能發生了改變,解析方式也會發生改變,因此協議設計這一塊,需要預留出地方聲明協議的版本,通信雙方在解析協議或者拼裝協議的時候纔有跡可循。

報文類型

對於RPC框架來說,報文可能有多種類型:心跳類型報文、認證類型報文、請求類型報文、響應類型報文等。

上下文 ID

RPC 調用其實是一個“請求-響應”的過程,並且跨物理機器,因此每次請求和響應,都必須帶上上下文 ID,通信雙方纔能把請求和響應對應起來。

狀態

狀態用來標識一次調用時正常結束還是異常結束,通常由被調用方置狀態。

請求數據

即發送到服務端的調用請求,通常是序列化後的二進制流,長度不定。

長度編碼字段

收報文的一方怎麼知道發報文的那一方發了多少字節呢?因此發送方必須在協議裏告訴接收方需要接受多少字節纔算一個完整的報文。

保留字段

協議一旦被設計,並非一成不變的,日後可能有變動的可能,因此還需要考慮保留一些字節空間作爲保留字段,以備日後協議的擴展。

協議設計

結合以上的一些設計原則,具體協議設計如下:

 ------------------------------------------------------------------------
 | magic (2bytes) | version (1byte) |  type (1byte)  | reserved (7bits) | 
 ------------------------------------------------------------------------
 | status (1byte) |    id (8bytes)    |        body length (4bytes)     |
 ------------------------------------------------------------------------
 |                                                                      |
 |                   body ($body_length bytes)                          |
 |                                                                      |
 ------------------------------------------------------------------------

3. 鏈路可靠性

客戶端與服務端之間的連接採用 TCP 長連接,一個客戶端與服務端之間保持至少一條長連接。接口調用請求的發送,在多條連接之間進行負載均衡。

每條連接在空閒的時候,由客戶端主動向服務端發送心跳報文,並且客戶端在發現連接失效或斷開的時候,自動進行重連。

每個客戶端向服務端建立連接後,在正式發起接口調用請求之前,都需要進行check in 操作, check in 操作主要是將客戶端的身份標識(identifier)和客戶端的心跳間隔告訴服務端。利用 netty 的 handler 責任鏈機制和自帶的 IdleStateHandler,自動檢測出連接是否空閒,並在空閒時觸發心跳報文的發送。而服務端在客戶端 checkin 後,根據客戶端的心跳頻率,在自己的 handler pipeline 上動態加入一個 IdleStateHandler,來檢測出客戶端是否已經失聯,如果是,則主動關閉連接。

同時,客戶端本地將會起一個定時執行任務的線程,定期檢查連接是否失效,如果失效,則關閉舊連接,並進行連接的重建。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章