天天看點

從零開始實作一個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/rickyes/kiritobuf

更多文章請通路

數瀾社群

,歡迎大家來一起學習~

繼續閱讀