1. 程式人生 > >從零開始實現一個IDL+RPC框架

從零開始實現一個IDL+RPC框架

一、RPC是什麼

在很久之前的單機時代,一臺電腦中跑著多個程序,程序之間沒有交流各幹各的,就這樣過了很多年。突然有一天有了新需求,A程序需要實現一個畫圖的功能,恰好鄰居B程序已經有了這個功能,偷懶的程式設計師C想出了一個辦法:A程序調B程序的畫圖功能。於是出現了IPC(Inter-process communication,程序間通訊)。就這樣程式設計師C愉快的去吃早餐去了!

又過了幾年,到了網際網路時代,每個電腦都實現了互聯互通。這時候僱主又有了新需求,當時還沒掛的A程序需要實現使用tensorflow識別出笑臉 >_< 。說巧不巧,遠在幾千裡的一臺快速執行的電腦上已經實現了這個功能,睡眼惺忪的程式媛D接手了這個A程序後借鑑之前IPC的實現,把IPC擴充套件到了網際網路上,這就是RPC(Remote Procedure Call,遠端過程呼叫)。RPC其實就是一臺電腦上的程序呼叫另外一臺電腦上的程序的工具。成熟的RPC方案大多數會具備服務註冊、服務發現、熔斷降級和限流等機制。目前市面上的RPC已經有很多成熟的了,比如Facebook家的Thrift、Google家的gRPC、阿里家的Dubbo和螞蟻家的SOFA。

二、介面定義語言

介面定義語言,簡稱IDL,是實現端對端之間可靠通訊的一套編碼方案。這裡有涉及到傳輸資料的序列化和反序列化,我們常用的http的請求一般用json當做序列化工具,定製rpc協議的時候因為要求響應迅速等特點,所以大多數會定義一套序列化協議。比如:

Protobuf:

講到Protobuf就得講到該庫作者的另一個作品Cap'n proto了,號稱效能是直接秒殺Google Protobuf,直接上官方對比:

雖然知道很多比Protobuf更快的編碼方案,但是快到這種地步也是厲害了,為啥這麼快,Cap’n Proto的文件裡面就立刻說明了,因為Cap'n Proto沒有任何序列號和反序列化步驟,Cap'n Proto編碼的資料格式跟在記憶體裡面的佈局是一致的,所以可以直接將編碼好的structure直接位元組存放到硬碟上面。貼個栗子:

我們這裡要定製的編碼方案就是基於protobuf和Cap'n Proto結合的類似的語法。因為本人比較喜歡刀劍神域裡的男主角,所以就給這個庫起了個名字—— Kiritobuf。

首先我們定義kirito的語法:

  • #開頭的是註釋

  • 保留關鍵字, service、method、struct,

  • {}裡是一個塊結構

  • ()裡有兩個引數,第一個是請求的引數結構,第二個是返回值的結構

  • @是定義引數位置的描述符,0表示在首位

  • =號左邊是引數名,右邊是引數型別

    引數型別:

    • Boolean: Bool
    • Integers: Int8, Int16, Int32, Int64
    • Unsigned integers:

    UInt8, UInt16, UInt32, UInt64

    • Floating-point: Float32, Float64
    • Blobs: Text, Data
    • Lists: List(T)

定義好了語法和引數型別,我們先過一下生成有抽象關係程式碼的流程:

取到.kirito字尾的檔案,讀取全部字元,通過詞法分析器生成token,得到的token傳入語法分析器生成AST (抽象語法樹)。

首先我們新建一個kirito.js檔案:

定義好了一些必要的字面量,接下來首先是詞法分析階段。

1、詞法解析

我們設計詞法分析得到的Token是這樣子的:

詞法分析步驟:

  • 把獲取到的kirito程式碼串按照\n分割組合成陣列A,陣列的每個元素就是一行程式碼

  • 遍歷陣列A,將每行程式碼逐個字元去讀取

  • 在讀取的過程中定義匹配規則,比如註釋、保留字、變數、符號、陣列等

  • 將每個匹配的字元或字串按照對應型別新增到tokens陣列中

程式碼如下:

2、語法分析

得到上面的詞法分析的token後,我們就可以對該token做語法分析,我們需要最終生成的AST的格式如下:

看上圖我們能友好的得到結構、引數、資料型別、函式之間的依賴和關係,步驟:

1、遍歷詞法分析得到的token陣列,通過呼叫分析函式提取token之間的依賴節點

2、分析函式內部定義token提取規則,比如:

  • 服務保留字 服務名 { 函式保留字 函式名 ( 入參,返回引數 ) }
  • 引數結構保留字 結構名 { 引數位置 引數名 引數資料型別 } 3、遞迴呼叫分析函式提取對應節點依賴關係,將節點新增到AST中

程式碼如下:

3、轉換器

得到了語法分析的AST後我們需要進一步對AST轉換為更易操作的js物件。格式如下:

通過上面這個格式,我們可以更容易的知道有幾個service、service裡有多少個函式以及函式的引數。

程式碼如下:

三、傳輸協議

RPC協議有多種,可以是json、xml、http2,相對於http1.x這種文字協議,http2.0這種二進位制協議更適合作為RPC的應用層通訊協議。很多成熟的RPC框架一般都會定製自己的協議已滿足各種變化莫測的需求。

比如Thrift的TBinaryProtocol、TCompactProto-col等,使用者可以自主選擇適合自己的傳輸協議。 (除了按位元組編址還有按字編址和按位編址),我們這裡只討論位元組編址。每個機器因為不同的系統或者不同的CPU對記憶體地址的編碼有不一樣的規則,一般分為兩種位元組序:大端序和小端序。

  • 大端序: 資料的高位元組儲存在低地址
  • 小端序: 資料的低位元組儲存在高地址

舉個栗子:

比如一個整數:258,用16進製表示為0x0102,我們把它分為兩個位元組0x01和ox02,對應的二進位制為0000 0001和0000 0010。在大端序的電腦上存放形式如下:

小端序則相反。為了保證在不同機器之間傳輸的資料是一樣的,開發一個通訊協議時會首先約定好使用一種作為通訊方案。java虛擬機器採用的是大端序。在機器上我們稱為主機位元組序,網路傳輸時我們稱為網路位元組序。網路位元組序是TCP/IP中規定好的一種資料表示格式,它與具體的CPU型別、作業系統等無關,從而可以保證資料在不同主機之間傳輸時能夠被正確解釋。網路位元組序採用大端排序方式。

我們這裡就不造新應用層協議的輪子了,我們直接使用MQTT協議作為我們的預設應用層協議。MQTT(Message Queuing Telemetry Tran-sport,訊息佇列遙測傳輸協議),是一種基於釋出/訂閱(publish/subscribe)模式的“輕量級”通訊協議,採用大端序的網路位元組序傳輸,該協議構建於TCP/IP協議上。

四、實現通訊

先貼下實現完的程式碼呼叫流程,首先是server端:

client端:

無論是server端定義函式或者client端呼叫函式都是比較簡潔的步驟。接下來我們慢慢剖析具體的邏輯實現。

貼下具體的呼叫流程架構圖:

呼叫流程總結:

  • client端解析kirito檔案,繫結kirito的service到client物件
  • server端解析kirito檔案,將kiritod的service與呼叫函式繫結新增到server物件
  • client端呼叫kirito service 裡定義的函式,註冊回撥事件,發起MQTT請求
  • server端接收MQTT請求,解析請求body,呼叫對應的函式執行完後向client端發起MQTT請求
  • client端接收到MQTT請求後,解析body和error,並從回撥事件佇列裡取出對應的回撥函式並賦值執行

說完了呼叫流程,現在開始講解具體的實現。

server:

定義protocol介面,加上這一層是為了以後的多協議,mqtt只是預設使用的協議:

接下來是server端的暴露出去的介面:

client:

定義protocol介面:

最後是client端暴露的介面:

就這樣,一個簡單的IDL+RPC框架就這樣搭建完成了。這裡只是描述RPC的原理和常用的呼叫方式,要想用在企業級的開發上,還得加上服務發現、註冊,服務熔斷,服務降級等,讀者如果有興趣可以在Github上fork下來或者提PR來改進這個框架,有什麼問題也可以提Issue, 當然PR是最好的 : ) 。

倉庫地址:

RPC: https://github.com/polixjs/polix-rpc

IDL:https://github.com/