RPC-知乎高分回答
連結:https://www.zhihu.com/question/25536695/answer/221638079
來源:知乎
本地過程呼叫RPC就是要像呼叫本地的函式一樣去調遠端函式。在研究RPC前,我們先看看本地呼叫是怎麼調的。
假設我們要呼叫函式Multiply來計算lvalue * rvalue的結果:
1 int Multiply(int l, int r) { 2int y = l * r; 3return y; 4 } 5 6 int lvalue = 10; 7 int rvalue = 20; 8 int l_times_r = Multiply(lvalue, rvalue);
那麼在第8行時,我們實際上執行了以下操作:
1.將 lvalue 和 rvalue 的值壓棧
2.進入Multiply函式,取出棧中的值10 和 20,將其賦予 l 和 r
3.執行第2行程式碼,計算 l * r ,並將結果存在 y
4.將 y 的值壓棧,然後從Multiply返回
5.第8行,從棧中取出返回值 200 ,並賦值給 l_times_r
以上5步就是執行本地呼叫的過程。
(20190116注:以上步驟只是為了說明原理。事實上編譯器經常會做優化,對於引數和返回值少的情況會直接將其存放在暫存器,而不需要壓棧彈棧的過程,甚至都不需要呼叫call,而直接做inline操作。僅就原理來說,這5步是沒有問題的。)
遠端過程呼叫帶來的新問題在遠端呼叫時,我們需要執行的函式體是在遠端的機器上的,也就是說,Multiply是在另一個程序中執行的。這就帶來了幾個新問題:
Call ID對映。
我們怎麼告訴遠端機器我們要呼叫Multiply,而不是Add或者FooBar呢?在本地呼叫中,函式體是直接通過函式指標來指定的,我們呼叫Multiply,編譯器就自動幫我們呼叫它相應的函式指標。但是在遠端呼叫中,函式指標是不行的,因為兩個程序的地址空間是完全不一樣的。所以,在RPC中,所有的函式都必須有自己的一個ID。這個ID在所有程序中都是唯一確定的。客戶端在做遠端過程呼叫時,必須附上這個ID。然後我們還需要在客戶端和服務端分別維護一個 {函式 <--> Call ID} 的對應表。兩者的表不一定需要完全相同,但相同的函式對應的Call ID必須相同。當客戶端需要進行遠端呼叫時,它就查一下這個表,找出相應的Call ID,然後把它傳給服務端,服務端也通過查表,來確定客戶端需要呼叫的函式,然後執行相應函式的程式碼。
序列化和反序列化。
客戶端怎麼把引數值傳給遠端的函式呢?在本地呼叫中,我們只需要把引數壓到棧裡,然後讓函式自己去棧裡讀就行。但是在遠端過程呼叫時,客戶端跟服務端是不同的程序,不能通過記憶體來傳遞引數。甚至有時候客戶端和服務端使用的都不是同一種語言(比如服務端用C++,客戶端用Java或者Python)。這時候就需要客戶端把引數先轉成一個位元組流,傳給服務端後,再把位元組流轉成自己能讀取的格式。這個過程叫序列化和反序列化。同理,從服務端返回的值也需要序列化反序列化的過程。
網路傳輸。
遠端呼叫往往用在網路上,客戶端和服務端是通過網路連線的。所有的資料都需要通過網路傳輸,因此就需要有一個網路傳輸層。網路傳輸層需要把Call ID和序列化後的引數位元組流傳給服務端,然後再把序列化後的呼叫結果傳回客戶端。只要能完成這兩者的,都可以作為傳輸層使用。因此,它所使用的協議其實是不限的,能完成傳輸就行。儘管大部分RPC框架都使用TCP協議,但其實UDP也可以,而gRPC乾脆就用了HTTP2。Java的Netty也屬於這層的東西。
有了這三個機制,就能實現RPC了,具體過程如下:
// Client端
//int l_times_r = Call(ServerAddr, Multiply, lvalue, rvalue)
1. 將這個呼叫對映為Call ID。這裡假設用最簡單的字串當Call ID的方法
2. 將Call ID,lvalue和rvalue序列化。可以直接將它們的值以二進位制形式打包
3. 把2中得到的資料包傳送給ServerAddr,這需要使用網路傳輸層
4. 等待伺服器返回結果
5. 如果伺服器呼叫成功,那麼就將結果反序列化,並賦給l_times_r
// Server端
1. 在本地維護一個Call ID到函式指標的對映call_id_map,可以用std::map<std::string, std::function<>>
2. 等待請求
3. 得到一個請求後,將其資料包反序列化,得到Call ID
4. 通過在call_id_map中查詢,得到相應的函式指標
5. 將lvalue和rvalue反序列化後,在本地呼叫Multiply函式,得到結果
6. 將結果序列化後通過網路返回給Client
所以要實現一個RPC框架,其實只需要按以上流程實現就基本完成了。其中:Call ID對映可以直接使用函式字串,也可以使用整數ID。對映表一般就是一個雜湊表。序列化反序列化可以自己寫,也可以使用Protobuf或者FlatBuffers之類的。網路傳輸庫可以自己寫socket,或者用asio,ZeroMQ,Netty之類。當然,這裡面還有一些細節可以填充,比如如何處理網路錯誤,如何防止攻擊,如何做流量控制,等等。但有了以上的架構,這些都可以持續加進去。