一個入門rpc框架的分析學習

參考

簡介

RPC,即 Remote Procedure Call(遠程過程調用),說得通俗一點就是:調用遠程計算機上的服務,就像調用本地服務一樣。

RPC 可基于 HTTP 或 TCP 協議,Web Service 就是基于 HTTP 協議的 RPC,
它具有良好的跨平臺性,但其性能卻不如基于 TCP 協議的 RPC。會兩方面會直接影響 RPC 的性能,一是傳輸方式,二是序列化。

眾所周知,TCP 是傳輸層協議,HTTP 是應用層協議,而傳輸層較應用層更加底層,
在數據傳輸方面,越底層越快,因此,在一般情況下,TCP 一定比 HTTP 快。
就序列化而言,Java 提供了默認的序列化方式,但在高并發的情況下,
這種方式將會帶來一些性能上的瓶頸,于是市面上出現了一系列優秀的序列化框架,比如:Protobuf、Kryo、Hessian、Jackson 等,
它們可以取代 Java 默認的序列化,
從而提供更高效的性能。

為了支持高并發,傳統的阻塞式 IO 顯然不太合適,因此我們需要異步的 IO,即 NIO。
Java 提供了 NIO 的解決方案,Java 7 也提供了更優秀的 NIO.2 支持,用 Java 實現 NIO 并不是遙不可及的事情,只是需要我們熟悉 NIO 的技術細節。

我們需要將服務部署在分布式環境下的不同節點上,通過服務注冊的方式,
讓客戶端來自動發現當前可用的服務,并調用這些服務。
這需要一種服務注冊表(Service Registry)的組件,讓它來注冊分布式環境下所有的服務地址(包括:主機名與端口號)。

應用、服務、服務注冊表之間的關系見下圖:

rpc-1.png

每臺 Server 上可發布多個 Service,這些 Service 共用一個 host 與 port,
在分布式環境下會提供 Server 共同對外提供 Service。此外,為防止 Service Registry 出現單點故障,因此需要將其搭建為集群環境。

本文將為您揭曉開發輕量級分布式 RPC 框架的具體過程,
該框架基于 TCP 協議,提供了 NIO 特性,提供高效的序列化方式,同時也具備服務注冊與發現的能力。

根據以上技術需求,我們可使用如下技術選型:

Spring:它是最強大的依賴注入框架,也是業界的權威標準。
Netty:它使 NIO 編程更加容易,屏蔽了 Java 底層的 NIO 細節。
Protostuff:它基于 Protobuf 序列化框架,面向 POJO,無需編寫 .proto 文件。
ZooKeeper:提供服務注冊與發現功能,開發分布式系統的必備選擇,同時它也具備天生的集群能力。

源代碼目錄結構

  • rpc-client
    • 實現了rpc的服務動態代理(RpcProxy)以及基于Netty封裝的一個客戶端網絡層(RpcClient)
  • rpc-common
    • 封裝了RpcRequest和RpcResponse,即rpc請求和響應的數據結構
    • 基于Netty提供了編解碼器
    • 提供了序列化反序列化等工具
  • rpc-registry
    • 提供了服務發現和注冊接口
  • rpc-registry-zookeeper
    • 基于zookeeper的服務發現和注冊接口
  • rpc-server
    • rpc服務器(RpcServer)的實現,用來監聽rpc請求以及向Zookeeper注冊服務地址
    • rpc服務本地調用
  • rpc-sample-api
    • rpc測試公共api服務接口
  • rpc-sample-client
    • rpc測試客戶端
  • rpc-sample-server
    • rpc測試服務啟動程序和服務實現

啟動順序

  • 配置Zookeeper
    • 解壓zookeeper-3.4.9
    • 進入conf目錄,重命名zoo_sample.cfg為zoo.cfg(或者復制一份重命名)并修改一些配置選項如dataDir.另外可以看到默認的clientPort是2181
    • 將bin目錄加入環境變量PATH,這樣則可直接使用zkServer命令直接啟動
  • 啟動rpc-sample-server工程的下RpcBootstrap
  • 啟動rpc-sample-client工程下的測試程序HelloClient等

關鍵實現和核心模塊分析

  • RpcBootstrap
  • 加載spring.xml實例化RpcServer
    • 兩個參數分別是rpc服務地址(127.0.0.1:8000)和基于ZooKeeper的服務注冊接口實現(使用ZkClient連接Zookeeper的2181端口)
  • 加載過程中,會首先調用setApplicationContext方法
    • 掃描com.xxx.rpc.sample.server下帶有@RpcService注解的類,本例是HelloServiceImpl和HelloServiceImpl2,即有兩個rpc服務類,其中HelloServiceImpl2加了一個版本號用來區分第一個服務類,掃描后放入handlerMap,即服務名和服務對象之間的映射map
  • 加載后,調用afterPropertiesSet方法
    • 啟動Netty服務端,監聽8000端口;channelpipeline增加編解碼器(RpcDecoder、RpcEncoder)和邏輯處理類(RpcServerHandler)
      • RpcEncoder,編碼器,消息格式為4個字節的消息長度和消息體;直接使用Protostuff進行序列化,編碼對象為RpcResponse
      • RpcDecoder,解碼器;已解決粘包問題;使用Objenesis和Protostuff繼續反序列化
      • RpcServerHandler,收到RpcRequest后直接從handlerMap找到對應的服務類反射進行方法調用(使用了CGLib);最后向客戶端寫入rpc響應,完畢則主動關閉連接(所以從這里看是短連接)
        • ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE)
          • 這行代碼相當于在rpc響應發送的這個操作完成之后關閉連接
        • 注意Netty強烈建議直接通過添加監聽器的方式獲取I/O操作結果;當I/O操作完成之后,I/O線程會回調ChannelFuture中GenericFutureListener#operationComplete方法
    • 綁定端口成功后,向Zookeeper注冊上面的兩個rpc服務

ChannelFutureListener CLOSE = new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) {
                    future.channel().close();
                }
};

  • RpcProxy
    • 初始化亦通過加載spring.xml,指定了基于zookeeper的服務發現類ZooKeeperServiceDiscovery
    • create方法,主要使用了jdk的動態代理;當代理方法執行的時候,首先根據請求的服務名利用Zookeeper的服務發現接口獲取服務的address;然后封裝rpc請求調用Netty客戶端連接服務地址并發送
      • 關于RpcClient,同Netty服務端,需要設置channelpipeline的編解碼器和邏輯處理handler
      • Channel channel = future.channel();
        channel.writeAndFlush(request).sync();
        channel.closeFuture().sync();
        return response;
      • 注意上部分代碼,發送rpc請求后等待發送完畢;發送完畢后等待連接關閉,此時線程阻塞直到服務端發送完回復消息并主動關閉連接,線程繼續;所以這個例子并沒有會有request對不上reponse的問題,因為每次rpc調用都是短連接且當前執行線程掛起;另外服務端收到request的時候,也會用requestId作為response的requestId

可改進地方

  • 本人覺得spring相對較厚重,所以將spring去掉,對象實例化和依賴注入用比較簡單的方式去處理;不過比較麻煩的是對于掃描@RpcService注解這部分需要手動處理 或者 可以直接使用注解的方式而不依賴xml
  • 目前該示例提供的兩個服務均是在同一個端口8000下的服務;如何測試不同的兩個服務在不同的端口?按照該例子的設計,一個RpcServer即一個rpc發布服務器,該監聽的端口下可以注冊不同很多服務(當然一個Netty server本身可以bind多個端口,這個暫時不考慮實現);如果需要增加不同的服務,則需要單獨啟動RpcServer并向Zookeeper注冊

其他待調研rpc框架

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373