RPC 實現(xiàn)以及相關(guān)學(xué)習(xí) ~
點擊上方藍色“小哈學(xué)Java”,選擇“設(shè)為星標(biāo)”
回復(fù)“資源”獲取獨家整理的學(xué)習(xí)資料!
作者:xavior
yuque.com/xavior.wx/point/rpc-practice
我們即希望能夠敏捷開發(fā),不做重復(fù)的勞動,用別人的勢能賦能自己;又要成為一名能夠賦能別人的人,擁有自身的勢能。
在一個擁有成千上萬大大小小的服務(wù)的公司里,每個團隊在不同的機器上部署它們自己的服務(wù),所以真實開發(fā)一個新服務(wù)的場景一定需要考慮兩個問題:
我的團隊開發(fā)一個新服務(wù),可能需要調(diào)用別人的服務(wù)。
我的團隊開發(fā)一個新服務(wù),別的團隊可能會調(diào)用。
RPC調(diào)用的變與不變
由于服務(wù)部署在不同機器,想要進行服務(wù)間的調(diào)用必須進行網(wǎng)絡(luò)通信,那服務(wù)消費方每調(diào)用一個服務(wù)都要寫一大堆網(wǎng)絡(luò)通信的東西,不僅復(fù)雜而且極易出錯。
我們知道此時我們的技術(shù)選型時很豐富的,關(guān)于各種技術(shù)的優(yōu)缺點網(wǎng)上很多文章,可以去編乎的相關(guān)問題去看看,我覺得概括的比較好一句話是良好的RPC調(diào)用是面向服務(wù)的封裝,針對的是服務(wù)的可用性和效率,減輕網(wǎng)絡(luò)服務(wù)開發(fā)和調(diào)用的復(fù)雜性。
但我們不管選擇何種進程間通信手段,http,TCP通信或是消息中間件、RPC通信,調(diào)用本身很多東西是不可能變的:
角色的定義(發(fā)起調(diào)用的是客戶端,接受調(diào)用的是服務(wù)端)
通信的機制(網(wǎng)絡(luò)IO,序列化,傳輸協(xié)議,同步異步)
那真正變的是什么?這些都是你遇到的場景以及你的目標(biāo)導(dǎo)致的,對于RPC來說,主要來說和其他相比比較大的變化在于下面兩條吧。
調(diào)用的目標(biāo)。服務(wù)透明化,目標(biāo)是讓用戶像以本地調(diào)用方式調(diào)用遠(yuǎn)程服務(wù)。
調(diào)用的方式。服務(wù)端的服務(wù)要被調(diào)用,客戶端在本地直接調(diào)用服務(wù)端提供的接口即可,而不需要調(diào)用真實的接口實現(xiàn)。于是服務(wù)端就是需要利用一些很多反射操作去完成。
RPC需要什么
想要實現(xiàn)一個基本的RPC框架,其實需要什么?
網(wǎng)絡(luò)IO,BIO\NIO\AIO,Socket編程,HTTP通信,一個就行。
序列化,JDK序列化,JSON、Hessian、Kryo、ProtoBuffer、ProtoStuff、Fst知道一個就行。
反射,JDK或者Cglib的動態(tài)代理。
那一個優(yōu)秀的RPC框架,還需要考慮什么問題?
一個服務(wù)可能有多個實例,你在調(diào)用時,要如何獲取這些實例的地址?服務(wù)注冊中心
多個實例,選哪個調(diào)用好?負(fù)載均衡
服務(wù)注冊中心每次都查?緩存相關(guān)
客戶端每次要等服務(wù)器返回結(jié)果?異步調(diào)用
服務(wù)是要升級的?版本控制
多個服務(wù)依賴,某個有問題?熔斷器
某個服務(wù)出了問題怎么辦?監(jiān)控 ...
Dubbo
其實要考慮的問題是非常多的,瞻仰一下Dubbo的流程圖和Dubbo團隊對未來的規(guī)劃圖:

自己實現(xiàn)的一個簡單RPC框架
看了很多網(wǎng)上的博客實現(xiàn)的,最后自己實現(xiàn)的地址:https://github.com/1000-7/xinrpc 在看阿里技術(shù)大學(xué)的HSF視頻課的時候,視頻的講師說想要理解RPC就把下面這張圖理解清楚就夠了,這張圖也是HSF官方文檔中介紹一次調(diào)用流程使用的圖。

借此實現(xiàn)的機會,自己又學(xué)習(xí)實踐了包括Netty、Java反射、序列化、java注解、SpringBoot等很多方面的知識。
整體調(diào)用流程
由于采用了etcd做服務(wù)注冊中心,所以整體調(diào)用流程可以被概括為下面這樣:
Server端啟動進行服務(wù)注冊到etcd;
Client端啟動獲取etcd的服務(wù)注冊信息,定期更新;
Client以本地調(diào)用方式調(diào)用服務(wù)(使用接口,例如helloService.sayHi("world"));
Client通過RpcProxy會使用對應(yīng)的服務(wù)名生成動態(tài)代理相關(guān)類,而動態(tài)代理類會將請求的對象中的方法、參數(shù)等組裝成能夠進行網(wǎng)絡(luò)傳輸?shù)南ⅢwRpcRequest;
Client通過一些的負(fù)載均衡方式確定向某臺Server發(fā)送編碼(RpcEncoder)過后的請求(netty實現(xiàn))
Server收到請求進行解碼(RpcDecoder),通過反射(cglib的FastMethod實現(xiàn))會進行本地的服務(wù)執(zhí)行
Server端writeAndFlush()將RpcResponse返回;
Clinet將返回的結(jié)果會進行解碼,得到最終結(jié)果。
Netty學(xué)習(xí)
Netty是一款異步的事件驅(qū)動的網(wǎng)絡(luò)應(yīng)用程序框架,支持快速地開發(fā)可維護的高性能的面向協(xié)議的服務(wù)器和客戶端。
Netty重要的幾個概念
Channel:這并不是Netty專有的概念,Java NIO里也有。可以看作是入站或者出戰(zhàn)數(shù)據(jù)的載體,有各種基本的read、write、connect、bind等方法,相當(dāng)于傳統(tǒng)IO的Socket,需要關(guān)注一下ServerChannel,ServerChannel負(fù)責(zé)創(chuàng)建子Channel,子Channel具體去執(zhí)行一些具體accept之后的讀寫操作。項目中用的NioSocketChannel和NioServerSocketChannel。
EventLoop和EventLoopGroup:Netty的核心抽象,channel的整個生命周期都是通過EventLoop去處理。EventLoop相當(dāng)于對Thread的封裝,一個EventLoop里面擁有一個永遠(yuǎn)都不會改變的Thread,同時任務(wù)的提交只需要通過EventLoop就可執(zhí)行;而EventLoopGroup負(fù)責(zé)為每個Channel分配一個EventLoop/
ChannelFuture:Netty所有的IO操作都是異步的原因。
ChannelHandler和ChannelPipeline:開發(fā)人員主要關(guān)注的也可能是唯一需要關(guān)注的兩個組件,用來管理數(shù)據(jù)流以及執(zhí)行應(yīng)用程序處理邏輯。
ChannelInboundHandler和ChannelOutboundHandler:兩個常見的ChannelHandler適配器,前者管理入站的數(shù)據(jù)和操作,后者管理出站的數(shù)據(jù)和操作,謹(jǐn)記:入站順序執(zhí)行,出站逆序執(zhí)行。
ChannelPipeline:一個攔截流經(jīng)某個channel的入站和出站時間的ChannelHandle實例鏈,每一個Channel剛被創(chuàng)建就會被分配一個ChannelPipeline,永久不可更改。
ChannelHandlerContext:ChannelHandle和ChannelPipeline中間管理的紐帶,每一個ChannelHandler分配一個ChannelHandlerContext用來跟其他Handler作交互。
ByteBuf:網(wǎng)絡(luò)數(shù)據(jù)的基本單位是字節(jié),Java NIO使用的ByteBuffer作為字節(jié)容器,而Netty使用ByteBuf替代ByteBuffer作為數(shù)據(jù)容器進行讀寫。
BootStrap:將各種組件拼圖進行組裝,ServerBootstrap用來引導(dǎo)服務(wù)端,Bootstrap用來引導(dǎo)客戶端。ServerBootstrap的Group一般會放入兩個EventLoopGroup,需要結(jié)合Channel去理解,ServerChannel會有子Channel,那為了處理這個Channel,你需要為每一個子Channel分配一個EventLoop,第二個EventLoopGroup是為了讓子Channel去共享一個EventLoop,避免額外的線程創(chuàng)建以及上下文切換。
ByteToMessageDecoder和MessageToByteEncoder:編解碼器的解碼器和編碼器,MessageToByteEncoder繼承了ChannelOutboundHandlerAdapter接口,ByteToMessageDecoder繼承了ChannelInboundHandlerAdapter接口。解碼器是將字節(jié)解碼為消息;編碼器是將消息編碼成字節(jié)。
Netty學(xué)習(xí)的其他問題
1.序列化和編碼都是把 Java 對象封裝成二進制數(shù)據(jù)的過程,這兩者有什么區(qū)別和聯(lián)系?序列化是把內(nèi)容變成計算機可傳輸?shù)馁Y源,而編碼則是讓程序認(rèn)識這份資源。
2.與服務(wù)端啟動相比,客戶端啟動的引導(dǎo)類少了哪些方法,為什么不需要這些方法?服務(wù)端:需要兩個線程組,NioServerSocketChannel線程模型,可以設(shè)置childHandle 客戶端:一個線程組,NioSocketChannel線程模型,只可以設(shè)置handler
3.ChannelPipeline執(zhí)行順序?
(1)InboundHandler順序執(zhí)行,OutboundHandler逆序執(zhí)行
(2)InboundHandler之間傳遞數(shù)據(jù),通過ctx.fireChannelRead(msg)
(3)InboundHandler通過ctx.write(msg),則會傳遞到outboundHandler (4) ?使用ctx.write(msg)傳遞消息,Inbound需要放在結(jié)尾,在Outbound之后,不然outboundhandler會不執(zhí)行;但是使用channel.write(msg)、pipline.write(msg)情況會不一致,都會執(zhí)行,那是因為channel和pipline會貫穿整個流。
(5) ?outBound和Inbound誰先執(zhí)行,針對客戶端和服務(wù)端而言,客戶端是發(fā)起請求再接受數(shù)據(jù),先outbound(寫)再inbound(讀),服務(wù)端則相反。
(6)outBound可以理解為數(shù)據(jù)“出航”,inBound可以理解為“歸航”,所以請求從客戶端到服務(wù)端就意味著,請求數(shù)據(jù)從客戶端出航,在服務(wù)端歸航,服務(wù)端響應(yīng)請求是從服務(wù)端到客戶端的,所以就是響應(yīng)數(shù)據(jù)從服務(wù)端出航,在客戶端歸航。
4.三種最常見的ChannelHandle的子類型?
a. 基于 ByteToMessageDecoder,我們可以實現(xiàn)自定義解碼,而不用關(guān)心 ByteBuf 的強轉(zhuǎn)和 解碼結(jié)果的傳遞。
b. 基于 SimpleChannelInboundHandler,這主要針對的最常見的一種情況,你去接收一種(泛型)解碼信息,然后對數(shù)據(jù)應(yīng)用業(yè)務(wù)邏輯然后繼續(xù)傳下去。我們可以實現(xiàn)每一種指令的處理,通過泛型不再需要強轉(zhuǎn),不再有冗長乏味的 if else 邏輯,不需要手動傳遞對象。
c. 基于 MessageToByteEncoder,我們可以實現(xiàn)自定義編碼,而不用關(guān)心 ByteBuf 的創(chuàng)建,不用每次向?qū)Χ藢?Java 對象都進行一次編碼。
5.Netty關(guān)于拆包粘包理論與解決方案?
本次使用的是LengthFieldBasedFrameDecoder。
a.固定長度的拆包器 FixedLengthFrameDecoder 如果你的應(yīng)用層協(xié)議非常簡單,每個數(shù)據(jù)包的長度都是固定的,比如 100,那么只需要把這個拆包器加到 pipeline 中,Netty 會把一個個長度為 100 的數(shù)據(jù)包 (ByteBuf) 傳遞到下一個 channelHandler。
b.行拆包器 LineBasedFrameDecoder 從字面意思來看,發(fā)送端發(fā)送數(shù)據(jù)包的時候,每個數(shù)據(jù)包之間以換行符作為分隔,接收端通過 LineBasedFrameDecoder 將粘過的 ByteBuf 拆分成一個個完整的應(yīng)用層數(shù)據(jù)包。
c.分隔符拆包器 DelimiterBasedFrameDecoder DelimiterBasedFrameDecoder 是行拆包器的通用版本,只不過我們可以自定義分隔符。
d.基于長度域拆包器 LengthFieldBasedFrameDecoder 最后一種拆包器是最通用的一種拆包器,只要你的自定義協(xié)議中包含長度域字段,均可以使用這個拆包器來實現(xiàn)應(yīng)用層拆包。由于上面三種拆包器比較簡單,讀者可以自行寫出 demo,接下來,我們就結(jié)合我們小冊的自定義協(xié)議,來學(xué)習(xí)一下如何使用基于長度域的拆包器來拆解我們的數(shù)據(jù)包。
CGLib學(xué)習(xí)
反射和動態(tài)代理
反射機制是Java語言提供的一種基礎(chǔ)功能,賦予程序在運行時 自省 (introspect,官方用語)的能力。通過反射我們可以直接操作類或者對象,比如獲取某個對象的類定義,獲取類聲明的屬性和方法,調(diào)用方法或者構(gòu)造對象,甚至可以運行時修改類定義。
動態(tài)代理是一種方便運行時動態(tài)構(gòu)建代理、動態(tài)處理代理方法調(diào)用的機制,很多場景都是利用類似機制做到的,比如用來包裝 RPC 調(diào)用、面向切面的編程(AOP)。實現(xiàn)動態(tài)代理的方式很多,比如 JDK 自身提供的動態(tài)代理,就是主要利用了上面提到的反射機制。還有其他的實現(xiàn)方式,比如利用傳說中更高性能的字節(jié)碼操作機制,類似 ASM、cglib(基于 ASM)等。
總結(jié):反射是java的一種能力,而動態(tài)代理是一種解決問題的方案。動態(tài)代理是一種代理模式。代理可以看作是對調(diào)用目標(biāo)的一個包裝,這樣我們對目標(biāo)代碼的調(diào)用不是直接發(fā)生的,而是通過代理完成。通過代理可以讓調(diào)用者與實現(xiàn)者之間解耦 。
CGLib實現(xiàn)反射
????FastClass?fastClass?=?FastClass.create(serviceClass);??
????FastMethod?fastMethod?=?fastClass.getMethod(methodName,?parameterTypes);??
????return?fastMethod.invoke(serviceBean,?parameters);??
CGLib實現(xiàn)動態(tài)代理
實現(xiàn)MethodInterceptor接口,然后使用Enhancer構(gòu)建
????public?static??T?createByCglib(Class?clazz) ?{??
????????Enhancer?enhancer?=?new?Enhancer();??
????????enhancer.setSuperclass(clazz);??
????????enhancer.setCallback(new?RpcMethodInterceptor(clazz));??
????????return?(T)?enhancer.create();??
????}??
JDK實現(xiàn)動態(tài)代理
實現(xiàn)InvocationHandler接口,然后使用Proxy創(chuàng)建
????public?static??T?create(Class?interfaceClass) ?{??
????????return?(T)?Proxy.newProxyInstance(??
????????????????interfaceClass.getClassLoader(),??
????????????????new?Class>[]{interfaceClass},??
????????????????new?RpcInvocationHandler<>(interfaceClass)??
????????);??
????}??
序列化實現(xiàn)
序列化有多種實現(xiàn)方式,不同序列化優(yōu)缺點不同,網(wǎng)上有很多比較天梯圖。我實現(xiàn)了五種,JSON,F(xiàn)ST,HESSIAN2,PROTO_STUFF,KRYO。
END
有熱門推薦??
1.?太牛逼了!項目中用了Disruptor之后,性能提升了2.5倍
最近面試BAT,整理一份面試資料《Java面試BATJ通關(guān)手冊》,覆蓋了Java核心技術(shù)、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。
獲取方式:點“在看”,關(guān)注公眾號并回復(fù)?Java?領(lǐng)取,更多內(nèi)容陸續(xù)奉上。
文章有幫助的話,在看,轉(zhuǎn)發(fā)吧。
謝謝支持喲 (*^__^*)



