參考
- huangyong-rpc
- 輕量級分布式RPC框架
- 該程序是一個短連接的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方法
- ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE)
- 綁定端口成功后,向Zookeeper注冊上面的兩個rpc服務
- 啟動Netty服務端,監聽8000端口;channelpipeline增加編解碼器(RpcDecoder、RpcEncoder)和邏輯處理類(RpcServerHandler)
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注冊