參考
簡(jiǎn)介
RPC,即 Remote Procedure Call(遠(yuǎn)程過程調(diào)用),說得通俗一點(diǎn)就是:調(diào)用遠(yuǎn)程計(jì)算機(jī)上的服務(wù),就像調(diào)用本地服務(wù)一樣。
RPC 可基于 HTTP 或 TCP 協(xié)議,Web Service 就是基于 HTTP 協(xié)議的 RPC,
它具有良好的跨平臺(tái)性,但其性能卻不如基于 TCP 協(xié)議的 RPC。會(huì)兩方面會(huì)直接影響 RPC 的性能,一是傳輸方式,二是序列化。
眾所周知,TCP 是傳輸層協(xié)議,HTTP 是應(yīng)用層協(xié)議,而傳輸層較應(yīng)用層更加底層,
在數(shù)據(jù)傳輸方面,越底層越快,因此,在一般情況下,TCP 一定比 HTTP 快。
就序列化而言,Java 提供了默認(rèn)的序列化方式,但在高并發(fā)的情況下,
這種方式將會(huì)帶來一些性能上的瓶頸,于是市面上出現(xiàn)了一系列優(yōu)秀的序列化框架,比如:Protobuf、Kryo、Hessian、Jackson 等,
它們可以取代 Java 默認(rèn)的序列化,
從而提供更高效的性能。
為了支持高并發(fā),傳統(tǒng)的阻塞式 IO 顯然不太合適,因此我們需要異步的 IO,即 NIO。
Java 提供了 NIO 的解決方案,Java 7 也提供了更優(yōu)秀的 NIO.2 支持,用 Java 實(shí)現(xiàn) NIO 并不是遙不可及的事情,只是需要我們熟悉 NIO 的技術(shù)細(xì)節(jié)。
我們需要將服務(wù)部署在分布式環(huán)境下的不同節(jié)點(diǎn)上,通過服務(wù)注冊(cè)的方式,
讓客戶端來自動(dòng)發(fā)現(xiàn)當(dāng)前可用的服務(wù),并調(diào)用這些服務(wù)。
這需要一種服務(wù)注冊(cè)表(Service Registry)的組件,讓它來注冊(cè)分布式環(huán)境下所有的服務(wù)地址(包括:主機(jī)名與端口號(hào))。
應(yīng)用、服務(wù)、服務(wù)注冊(cè)表之間的關(guān)系見下圖:

每臺(tái) Server 上可發(fā)布多個(gè) Service,這些 Service 共用一個(gè) host 與 port,
在分布式環(huán)境下會(huì)提供 Server 共同對(duì)外提供 Service。此外,為防止 Service Registry 出現(xiàn)單點(diǎn)故障,因此需要將其搭建為集群環(huán)境。
本文將為您揭曉開發(fā)輕量級(jí)分布式 RPC 框架的具體過程,
該框架基于 TCP 協(xié)議,提供了 NIO 特性,提供高效的序列化方式,同時(shí)也具備服務(wù)注冊(cè)與發(fā)現(xiàn)的能力。
根據(jù)以上技術(shù)需求,我們可使用如下技術(shù)選型:
Spring:它是最強(qiáng)大的依賴注入框架,也是業(yè)界的權(quán)威標(biāo)準(zhǔn)。
Netty:它使 NIO 編程更加容易,屏蔽了 Java 底層的 NIO 細(xì)節(jié)。
Protostuff:它基于 Protobuf 序列化框架,面向 POJO,無需編寫 .proto 文件。
ZooKeeper:提供服務(wù)注冊(cè)與發(fā)現(xiàn)功能,開發(fā)分布式系統(tǒng)的必備選擇,同時(shí)它也具備天生的集群能力。
源代碼目錄結(jié)構(gòu)
-
rpc-client
- 實(shí)現(xiàn)了rpc的服務(wù)動(dòng)態(tài)代理(RpcProxy)以及基于Netty封裝的一個(gè)客戶端網(wǎng)絡(luò)層(RpcClient)
-
rpc-common
- 封裝了RpcRequest和RpcResponse,即rpc請(qǐng)求和響應(yīng)的數(shù)據(jù)結(jié)構(gòu)
- 基于Netty提供了編解碼器
- 提供了序列化反序列化等工具
-
rpc-registry
- 提供了服務(wù)發(fā)現(xiàn)和注冊(cè)接口
-
rpc-registry-zookeeper
- 基于zookeeper的服務(wù)發(fā)現(xiàn)和注冊(cè)接口
-
rpc-server
- rpc服務(wù)器(RpcServer)的實(shí)現(xiàn),用來監(jiān)聽rpc請(qǐng)求以及向Zookeeper注冊(cè)服務(wù)地址
- rpc服務(wù)本地調(diào)用
-
rpc-sample-api
-
rpc-sample-client
-
rpc-sample-server
- rpc測(cè)試服務(wù)啟動(dòng)程序和服務(wù)實(shí)現(xiàn)
啟動(dòng)順序
-
配置Zookeeper
- 解壓zookeeper-3.4.9
- 進(jìn)入conf目錄,重命名zoo_sample.cfg為zoo.cfg(或者復(fù)制一份重命名)并修改一些配置選項(xiàng)如dataDir.另外可以看到默認(rèn)的clientPort是2181
- 將bin目錄加入環(huán)境變量PATH,這樣則可直接使用zkServer命令直接啟動(dòng)
- 啟動(dòng)rpc-sample-server工程的下RpcBootstrap
- 啟動(dòng)rpc-sample-client工程下的測(cè)試程序HelloClient等
關(guān)鍵實(shí)現(xiàn)和核心模塊分析
-
RpcBootstrap
-
加載spring.xml實(shí)例化RpcServer
- 兩個(gè)參數(shù)分別是rpc服務(wù)地址(127.0.0.1:8000)和基于ZooKeeper的服務(wù)注冊(cè)接口實(shí)現(xiàn)(使用ZkClient連接Zookeeper的2181端口)
-
加載過程中,會(huì)首先調(diào)用setApplicationContext方法
- 掃描com.xxx.rpc.sample.server下帶有@RpcService注解的類,本例是HelloServiceImpl和HelloServiceImpl2,即有兩個(gè)rpc服務(wù)類,其中HelloServiceImpl2加了一個(gè)版本號(hào)用來區(qū)分第一個(gè)服務(wù)類,掃描后放入handlerMap,即服務(wù)名和服務(wù)對(duì)象之間的映射map
-
加載后,調(diào)用afterPropertiesSet方法
-
啟動(dòng)Netty服務(wù)端,監(jiān)聽8000端口;channelpipeline增加編解碼器(RpcDecoder、RpcEncoder)和邏輯處理類(RpcServerHandler)
- RpcEncoder,編碼器,消息格式為4個(gè)字節(jié)的消息長(zhǎng)度和消息體;直接使用Protostuff進(jìn)行序列化,編碼對(duì)象為RpcResponse
- RpcDecoder,解碼器;已解決粘包問題;使用Objenesis和Protostuff繼續(xù)反序列化
-
RpcServerHandler,收到RpcRequest后直接從handlerMap找到對(duì)應(yīng)的服務(wù)類反射進(jìn)行方法調(diào)用(使用了CGLib);最后向客戶端寫入rpc響應(yīng),完畢則主動(dòng)關(guān)閉連接(所以從這里看是短連接)
-
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE)
- 這行代碼相當(dāng)于在rpc響應(yīng)發(fā)送的這個(gè)操作完成之后關(guān)閉連接
- 注意Netty強(qiáng)烈建議直接通過添加監(jiān)聽器的方式獲取I/O操作結(jié)果;當(dāng)I/O操作完成之后,I/O線程會(huì)回調(diào)ChannelFuture中GenericFutureListener#operationComplete方法
- 綁定端口成功后,向Zookeeper注冊(cè)上面的兩個(gè)rpc服務(wù)
ChannelFutureListener CLOSE = new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) {
future.channel().close();
}
};
-
RpcProxy
- 初始化亦通過加載spring.xml,指定了基于zookeeper的服務(wù)發(fā)現(xiàn)類ZooKeeperServiceDiscovery
-
create方法,主要使用了jdk的動(dòng)態(tài)代理;當(dāng)代理方法執(zhí)行的時(shí)候,首先根據(jù)請(qǐng)求的服務(wù)名利用Zookeeper的服務(wù)發(fā)現(xiàn)接口獲取服務(wù)的address;然后封裝rpc請(qǐng)求調(diào)用Netty客戶端連接服務(wù)地址并發(fā)送
- 關(guān)于RpcClient,同Netty服務(wù)端,需要設(shè)置channelpipeline的編解碼器和邏輯處理handler
- Channel channel = future.channel();
channel.writeAndFlush(request).sync();
channel.closeFuture().sync();
return response;
- 注意上部分代碼,發(fā)送rpc請(qǐng)求后等待發(fā)送完畢;發(fā)送完畢后等待連接關(guān)閉,此時(shí)線程阻塞直到服務(wù)端發(fā)送完回復(fù)消息并主動(dòng)關(guān)閉連接,線程繼續(xù);所以這個(gè)例子并沒有會(huì)有request對(duì)不上reponse的問題,因?yàn)槊看蝦pc調(diào)用都是短連接且當(dāng)前執(zhí)行線程掛起;另外服務(wù)端收到request的時(shí)候,也會(huì)用requestId作為response的requestId
可改進(jìn)地方
- 本人覺得spring相對(duì)較厚重,所以將spring去掉,對(duì)象實(shí)例化和依賴注入用比較簡(jiǎn)單的方式去處理;不過比較麻煩的是對(duì)于掃描@RpcService注解這部分需要手動(dòng)處理
- 目前該示例提供的兩個(gè)服務(wù)均是在同一個(gè)端口8000下的服務(wù);如何測(cè)試不同的兩個(gè)服務(wù)在不同的端口?按照該例子的設(shè)計(jì),一個(gè)RpcServer即一個(gè)rpc發(fā)布服務(wù)器,該監(jiān)聽的端口下可以注冊(cè)不同很多服務(wù)(當(dāng)然一個(gè)Netty server本身可以bind多個(gè)端口,這個(gè)暫時(shí)不考慮實(shí)現(xiàn));如果需要增加不同的服務(wù),則需要單獨(dú)啟動(dòng)RpcServer并向Zookeeper注冊(cè)
其他待調(diào)研rpc框架
posted on 2017-02-13 21:41
landon 閱讀(2675)
評(píng)論(0) 編輯 收藏 所屬分類:
ServerFramework 、
NetWork