Netty 與 RPC(貳)

Netty RPC 實現

概念

RPC,即 Remote Procedure Call(遠程過程調用),調用遠程計算機上的服務,就像調用本地服務一樣。RPC 可以很好的解耦系統,如 WebService 就是一種基於 Http 協議的 RPC。這個 RPC 整體框架如下:
關鍵技術
1. 服務發佈與訂閱:服務端使用 Zookeeper 註冊服務地址,客戶端從 Zookeeper 獲取可用的服務地址。
2. 通信:使用 Netty 作爲通信框架。
3. Spring:使用 Spring 配置服務,加載 Bean,掃描註解。
4. 動態代理:客戶端使用代理模式透明化服務調用。
5. 消息編解碼:使用 Protostuff 序列化和反序列化消息。
核心流程
1. 服務消費方(client)調用以本地調用方式調用服務;
2. client stub 接收到調用後負責將方法、參數等組裝成能夠進行網絡傳輸的消息體;
3. client stub 找到服務地址,並將消息發送到服務端;
4. server stub 收到消息後進行解碼;
5. server stub 根據解碼結果調用本地的服務;
6. 本地服務執行並將結果返回給 server stub;
7. server stub 將返回結果打包成消息併發送至消費方;
8. client stub 接收到消息,並進行解碼;
9. 服務消費方得到最終結果。
RPC 的目標就是要將 2~8 這些步驟都封裝起來,讓用戶對這些細節透明。JAVA 一般使用動態代理方式實現遠程調用
消息編解碼
消息數據結構(接口名稱+方法名+參數類型和參數值+超時時間+ requestID
客戶端的請求消息結構一般需要包括以下內容:
1. 接口名稱:在我們的例子裏接口名是“HelloWorldService”,如果不傳,服務端就不知道調用哪個接口了;
2. 方法名:一個接口內可能有很多方法,如果不傳方法名服務端也就不知道調用哪個方法;
3. 參數類型和參數值:參數類型有很多,比如有 bool、int、long、double、string、map、list,甚至如 struct(class);以及相應的參數值;
4. 超時時間:
5. requestID,標識唯一請求 id,在下面一節會詳細描述 requestID 的用處。
6. 服務端返回的消息 : 一般包括以下內容。返回值+狀態 code+requestID。
序列化
目前互聯網公司廣泛使用 Protobuf、Thrift、Avro 等成熟的序列化解決方案來搭建 RPC 框架,這些都是久經考驗的解決方案。
通訊過程
核心問題(線程暫停、消息亂序)
如果使用 netty 的話,一般會用 channel.writeAndFlush()方法來發送消息二進制串,這個方法調用後對於整個遠程調用(從發出請求到接收到結果)來說是一個異步的,即對於當前線程來說,將請求發送出來後,線程就可以往後執行了,至於服務端的結果,是服務端處理完成後,再以消息的形式發送給客戶端的。於是這裏出現以下兩個問題:
1. 怎麼讓當前線程“暫停”,等結果回來後,再向後執行?
2. 如果有多個線程同時進行遠程方法調用,這時建立在 client server 之間的 socket 連接上會有很多雙方發送的消息傳遞,前後順序也可能是隨機的,server 處理完結果後,將結果消息發送給 client,client 收到很多消息,怎麼知道哪個消息結果是原先哪個線程調用的?如下圖所示,線程 A 和線程 B 同時向 client socket 發送請求 requestA 和 requestB,socket 先後將 requestB 和 requestA 發送至 server而 server 可能將 responseB 先返回,儘管 requestB 請求到達時間更晚。我們需要一種機制保證 responseA 丟給ThreadA,responseB 丟給 ThreadB。
通訊流程
requestID 生成-AtomicLong
1. client 線程每次通過 socket 調用一次遠程接口前,生成一個唯一的 ID,即 requestID(requestID 必需保證在一個 Socket 連接裏面是唯一的),一般通常使用 AtomicLong從 0 開始累計數字生成唯一 ID;
存放回調對象 callback 到全局 ConcurrentHashMap
2. 將 處 理 結 果 的 回 調 對 象 callback , 存 放 到 全 局 ConcurrentHashMap 裏面put(requestID, callback)
synchronized 獲取回調對象 callback 的鎖並自旋 wait
3. 當線程調用 channel.writeAndFlush()發送消息後,緊接着執行 callback 的 get()方法試圖獲取遠程返回的結果。在 get()內部,則使用 synchronized 獲取回調對象 callback 的鎖,再先檢測是否已經獲取到結果,如果沒有,然後調用 callback 的 wait()方法,釋放callback 上的鎖,讓當前線程處於等待狀態
監聽消息的線程收到消息,找到 callback 上的鎖並喚醒
4. 服務端接收到請求並處理後,將 response 結果(此結果中包含了前面的 requestID)發送給客戶端,客戶端 socket 連接上專門監聽消息的線程收到消息,分析結果,取到requestID , 再 從 前 面 的 ConcurrentHashMap 裏 面 get(requestID) , 從 而 找 到callback 對象,再用 synchronized 獲取 callback 上的鎖將方法調用結果設置到callback 對象裏,再調用 callback.notifyAll()喚醒前面處於等待狀態的線程
 public Object get() {
     synchronized (this) { // 旋鎖
         while (true) { // 是否有結果了
             If (!isDone){
                 wait(); //沒結果釋放鎖,讓當前線程處於等待狀態
             }else{//獲取數據並處理
             }
         }
     }
 }
private void setDone(Response res) {
     this.res = res;
     isDone = true;
     synchronized (this) { //獲取鎖,因爲前面 wait()已經釋放了 callback 的鎖了
         notifyAll(); // 喚醒處於等待的線程
     }
}

 

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