新聞中心
看完今天的服務(wù)調(diào)用流程,基本上Dubbo的核心過(guò)程就完整的串聯(lián)起來(lái)了,在腦海中應(yīng)該就有 Dubbo 整體運(yùn)行的概念,這體系就建立起來(lái)了,對(duì) RPC 也會(huì)有進(jìn)一步的認(rèn)識(shí)。

創(chuàng)新互聯(lián)專注于企業(yè)營(yíng)銷型網(wǎng)站、網(wǎng)站重做改版、大城網(wǎng)站定制設(shè)計(jì)、自適應(yīng)品牌網(wǎng)站建設(shè)、H5響應(yīng)式網(wǎng)站、商城網(wǎng)站定制開(kāi)發(fā)、集團(tuán)公司官網(wǎng)建設(shè)、成都外貿(mào)網(wǎng)站制作、高端網(wǎng)站制作、響應(yīng)式網(wǎng)頁(yè)設(shè)計(jì)等建站業(yè)務(wù),價(jià)格優(yōu)惠性價(jià)比高,為大城等各大城市提供網(wǎng)站開(kāi)發(fā)制作服務(wù)。
簡(jiǎn)單的想想大致流程
在分析Dubbo 的服務(wù)調(diào)用過(guò)程前我們先來(lái)思考一下如果讓我們自己實(shí)現(xiàn)的話一次調(diào)用過(guò)程需要經(jīng)歷哪些步驟?
首先我們已經(jīng)知曉了遠(yuǎn)程服務(wù)的地址,然后我們要做的就是把我們要
然后根據(jù)這些信息找到對(duì)應(yīng)的實(shí)現(xiàn)類,然后進(jìn)行調(diào)用,調(diào)用完了之后再原路返回,然后客戶端解析響應(yīng)再返回即可。
調(diào)用具體的信息
那客戶端告知服務(wù)端的具體信息應(yīng)該包含哪些呢?
首先客戶端肯定要告知要調(diào)用是服務(wù)端的哪個(gè)接口,當(dāng)然還需要方法名、方法的參數(shù)類型、方法的參數(shù)值,還有可能存在多個(gè)版本的情況,所以還得帶上版本號(hào)。
由這么幾個(gè)參數(shù),那么服務(wù)端就可以清晰的得知客戶端要調(diào)用的是哪個(gè)方法,可以進(jìn)行精確調(diào)用!
然后組裝響應(yīng)返回即可,我這里貼一個(gè)實(shí)際調(diào)用請(qǐng)求對(duì)象列子。
data 就是我所說(shuō)的那些數(shù)據(jù),其他是框架的,包括協(xié)議版本、調(diào)用方式等等這個(gè)下面再分析。
到此其實(shí)大致的意思大家都清楚了,就是普通的遠(yuǎn)程調(diào)用,告知請(qǐng)求的參數(shù),然后服務(wù)端解析參數(shù)找到對(duì)應(yīng)的實(shí)現(xiàn)調(diào)用,再返回。
落地的調(diào)用流程
上面的是想象的調(diào)用流程,真實(shí)的落地調(diào)用流程沒(méi)有這么簡(jiǎn)單。
首先遠(yuǎn)程調(diào)用需要定義協(xié)議,也就是互相約定我們要講什么樣的語(yǔ)言,要保證雙方都能聽(tīng)得懂。
比如我會(huì)英語(yǔ)和中文,你也會(huì)英語(yǔ)、中文,我們之間要做約定,選定一個(gè)語(yǔ)言比如都用中文來(lái)談話,有人說(shuō)不對(duì)啊,你中文夾著的英文我也能聽(tīng)得懂啊。
那是因?yàn)槟愕拇竽X很智能,它能智能地識(shí)別到交流的語(yǔ)言,而計(jì)算機(jī)可不是,你想想你的代碼寫 print 1,它還能打出 2 不成?
也就是計(jì)算機(jī)是死板的,我們的程序告訴它該怎么做,它就會(huì)生硬的怎么做。
需要一個(gè)協(xié)議
所以首先需要雙方定義一個(gè)協(xié)議,這樣計(jì)算機(jī)才能解析出正確的信息。
常見(jiàn)的三種協(xié)議形式
應(yīng)用層一般有三種類型的協(xié)議形式,分別是:固定長(zhǎng)度形式、特殊字符隔斷形式、header+body 形式。
固定長(zhǎng)度形式:指的是協(xié)議的長(zhǎng)度是固定的,比如100個(gè)字節(jié)為一個(gè)協(xié)議單元,那么讀取100個(gè)字節(jié)之后就開(kāi)始解析。
優(yōu)點(diǎn)就是效率較高,無(wú)腦讀一定長(zhǎng)度就解析。
缺點(diǎn)就是死板,每次長(zhǎng)度只能固定,不能超過(guò)限制的長(zhǎng)度,并且短了還得填充,在 RPC 場(chǎng)景中不太合適,誰(shuí)曉得參數(shù)啥的要多長(zhǎng),定長(zhǎng)了浪費(fèi),定短了不夠。
特殊字符隔斷形式:其實(shí)就是定義一個(gè)特殊結(jié)束符,根據(jù)特殊的結(jié)束符來(lái)判斷一個(gè)協(xié)議單元的結(jié)束,比如用換行符等等。
這個(gè)協(xié)議的優(yōu)點(diǎn)是長(zhǎng)度自由,反正根據(jù)特殊字符來(lái)截?cái)?,缺點(diǎn)就是需要一直讀,直到讀到一個(gè)完整的協(xié)議單元之后才能開(kāi)始解析,然后假如傳輸?shù)臄?shù)據(jù)里面混入了這個(gè)特殊字符就出錯(cuò)了。
header+body 形式:也就是頭部是固定長(zhǎng)度的,然后頭部里面會(huì)填寫 body 的長(zhǎng)度, body 是不固定長(zhǎng)度的,這樣伸縮性就比較好了,可以先解析頭部,然后根據(jù)頭部得到 body 的 len 然后解析 body。
dubbo 協(xié)議就是屬于 header+body 形式,而且也有特殊的字符 0xdabb ,這是用來(lái)解決 TCP 網(wǎng)絡(luò)粘包問(wèn)題的。
Dubbo 協(xié)議
Dubbo 支持的協(xié)議很多,我們就簡(jiǎn)單的分析下 Dubbo 協(xié)議。
協(xié)議分為協(xié)議頭和協(xié)議體,可以看到 16 字節(jié)的頭部主要攜帶了魔法數(shù),也就是之前說(shuō)的 0xdabb,然后一些請(qǐng)求的設(shè)置,消息體的長(zhǎng)度等等。
16 字節(jié)之后就是協(xié)議體了,包括協(xié)議版本、接口名字、接口版本、方法名字等等。
其實(shí)協(xié)議很重要,因?yàn)閺闹锌梢缘弥芏嘈畔?,而且只有懂了協(xié)議的內(nèi)容,才能看得懂編碼器和解碼器在干嘛,我再截取一張官網(wǎng)對(duì)協(xié)議的解釋圖。
需要約定序列化器
網(wǎng)絡(luò)是以字節(jié)流的形式傳輸?shù)?/strong>,相對(duì)于我們的對(duì)象來(lái)說(shuō),我們對(duì)象是多維的,而字節(jié)流是一維的,我們需要把我們的對(duì)象壓縮成一維的字節(jié)流傳輸?shù)綄?duì)端。
然后對(duì)端再反序列化這些字節(jié)流變成對(duì)象。
序列化協(xié)議
其實(shí)從上圖的協(xié)議中可以得知 Dubbo 支持很多種序列化,我不具體分析每一種協(xié)議,就大致分析序列化的種類,萬(wàn)變不離其宗。
序列化大致分為兩大類,一種是字符型,一種是二進(jìn)制流。
字符型的代表就是 XML、JSON,字符型的優(yōu)點(diǎn)就是調(diào)試方便,它是對(duì)人友好的,我們一看就能知道那個(gè)字段對(duì)應(yīng)的哪個(gè)參數(shù)。
缺點(diǎn)就是傳輸?shù)男实?,有很多冗余的東西,比如 JSON 的括號(hào),對(duì)于網(wǎng)絡(luò)傳輸來(lái)說(shuō)傳輸?shù)臅r(shí)間變長(zhǎng),占用的帶寬變大。
還有一大類就是二進(jìn)制流型,這種類型是對(duì)機(jī)器友好的,它的數(shù)據(jù)更加的緊湊,所以占用的字節(jié)數(shù)更小,傳輸更快。
缺點(diǎn)就是調(diào)試很難,肉眼是無(wú)法識(shí)別的,必須借用特殊的工具轉(zhuǎn)換。
更深層次的就不深入了,序列化還是有很多門道的,以后有機(jī)會(huì)再談。
Dubbo 默認(rèn)用的是 hessian2 序列化協(xié)議。
所以實(shí)際落地還需要先約定好協(xié)議,然后再選擇好序列化方式構(gòu)造完請(qǐng)求之后發(fā)送。
粗略的調(diào)用流程圖
我們來(lái)看一下官網(wǎng)的圖。
簡(jiǎn)述一下就是客戶端發(fā)起調(diào)用,實(shí)際調(diào)用的是代理類,代理類最終調(diào)用的是 Client (默認(rèn)Netty),需要構(gòu)造好協(xié)議頭,然后將 Java 的對(duì)象序列化生成協(xié)議體,然后網(wǎng)絡(luò)調(diào)用傳輸。
服務(wù)端的 NettyServer接到這個(gè)請(qǐng)求之后,分發(fā)給業(yè)務(wù)線程池,由業(yè)務(wù)線程調(diào)用具體的實(shí)現(xiàn)方法。
但是這還不夠,因?yàn)?Dubbo 是一個(gè)生產(chǎn)級(jí)別的 RPC 框架,它需要更加的安全、穩(wěn)重。
詳細(xì)的調(diào)用流程
前面已經(jīng)分析過(guò)了客戶端也是要序列化構(gòu)造請(qǐng)求的,為了讓圖更加突出重點(diǎn),所以就省略了這一步,當(dāng)然還有響應(yīng)回來(lái)的步驟,暫時(shí)就理解為原路返回,下文會(huì)再做分析。
可以看到生產(chǎn)級(jí)別就得穩(wěn),因此服務(wù)端往往會(huì)有多個(gè),多個(gè)服務(wù)端的服務(wù)就會(huì)有多個(gè) Invoker,最終需要通過(guò)路由過(guò)濾,然后再通過(guò)負(fù)載均衡機(jī)制來(lái)選出一個(gè) Invoker 進(jìn)行調(diào)用。
當(dāng)然 Cluster 還有容錯(cuò)機(jī)制,包括重試等等。
請(qǐng)求會(huì)先到達(dá) Netty 的 I/O 線程池進(jìn)行讀寫和可選的序列化和反序列化,可以通過(guò) decode.in.io控制,然后通過(guò)業(yè)務(wù)線程池處理反序列化之后的對(duì)象,找到對(duì)應(yīng) Invoker 進(jìn)行調(diào)用。
調(diào)用流程-客戶端源碼分析
客戶端調(diào)用一下代碼。
- String hello = demoService.sayHello("world");
調(diào)用具體的接口會(huì)調(diào)用生成的代理類,而代理類會(huì)生成一個(gè) RpcInvocation 對(duì)象調(diào)用 MockClusterInvoker#invoke方法。
此時(shí)生成的 RpcInvocation 如下圖所示,包含方法名、參數(shù)類和參數(shù)值。
然后我們?cè)賮?lái)看一下 MockClusterInvoker#invoke 代碼。
可以看到就是判斷配置里面有沒(méi)有配置 mock, mock 的話就不展開(kāi)分析了,我們來(lái)看看 this.invoker.invoke 的實(shí)現(xiàn),實(shí)際上會(huì)調(diào)用 AbstractClusterInvoker#invoker 。
模板方法
這其實(shí)就是很常見(jiàn)的設(shè)計(jì)模式之一,模板方法。如果你經(jīng)??丛创a的話你知道這個(gè)設(shè)計(jì)模式真的是太常見(jiàn)的。
模板方法其實(shí)就是在抽象類中定好代碼的執(zhí)行骨架,然后將具體的實(shí)現(xiàn)延遲到子類中,由子類來(lái)自定義個(gè)性化實(shí)現(xiàn),也就是說(shuō)可以在不改變整體執(zhí)行步驟的情況下修改步驟里面的實(shí)現(xiàn),減少了重復(fù)的代碼,也利于擴(kuò)展,符合開(kāi)閉原則。
在代碼中就是那個(gè) doInvoke由子類來(lái)實(shí)現(xiàn),上面的一些步驟都是每個(gè)子類都要走的,所以抽到抽象類中。
路由和負(fù)載均衡得到 Invoker
我們?cè)賮?lái)看那個(gè) list(invocation),其實(shí)就是通過(guò)方法名找 Invoker,然后服務(wù)的路由過(guò)濾一波,也有再造一個(gè) MockInvoker 的。
然后帶著這些 Invoker 再進(jìn)行一波 loadbalance 的挑選,得到一個(gè) Invoker,我們默認(rèn)使用的是 FailoverClusterInvoker,也就是失敗自動(dòng)切換的容錯(cuò)方式,其實(shí)關(guān)于路由、集群、負(fù)載均衡是獨(dú)立的模塊,如果展開(kāi)講的話還是有很多內(nèi)容的,所以需要另起一篇講,這篇文章就把它們先作為黑盒使用。
稍微總結(jié)一下就是 FailoverClusterInvoker 拿到 Directory 返回的 Invoker 列表,并且經(jīng)過(guò)路由之后,它會(huì)讓 LoadBalance 從 Invoker 列表中選擇一個(gè) Invoker。
最后FailoverClusterInvoker會(huì)將參數(shù)傳給選擇出的那個(gè) Invoker 實(shí)例的 invoke 方法,進(jìn)行真正的遠(yuǎn)程調(diào)用,我們來(lái)簡(jiǎn)單的看下 FailoverClusterInvoker#doInvoke,為了突出重點(diǎn)我刪除了很多方法。
發(fā)起調(diào)用的這個(gè) invoke 又是調(diào)用抽象類中的 invoke 然后再調(diào)用子類的 doInvoker,抽象類中的方法很簡(jiǎn)單我就不展示了,影響不大,直接看子類 DubboInvoker 的 doInvoke 方法。
調(diào)用的三種方式
從上面的代碼可以看到調(diào)用一共分為三種,分別是 oneway、異步、同步。
oneway還是很常見(jiàn)的,就是當(dāng)你不關(guān)心你的請(qǐng)求是否發(fā)送成功的情況下,就用 oneway 的方式發(fā)送,這種方式消耗最小,啥都不用記,啥都不用管。
異步調(diào)用,其實(shí) Dubbo 天然就是異步的,可以看到 client 發(fā)送請(qǐng)求之后會(huì)得到一個(gè) ResponseFuture,然后把 future 包裝一下塞到上下文中,這樣用戶就可以從上下文中拿到這個(gè) future,然后用戶可以做了一波操作之后再調(diào)用 future.get 等待結(jié)果。
同步調(diào)用,這是我們最常用的,也就是 Dubbo 框架幫助我們異步轉(zhuǎn)同步了,從代碼可以看到在 Dubbo 源碼中就調(diào)用了 future.get,所以給用戶的感覺(jué)就是我調(diào)用了這個(gè)接口的方法之后就阻塞住了,必須要等待結(jié)果到了之后才能返回,所以就是同步的。
可以看到 Dubbo 本質(zhì)上就是異步的,為什么有同步就是因?yàn)榭蚣軒臀覀冝D(zhuǎn)了一下,而同步和異步的區(qū)別其實(shí)就是future.get 在用戶代碼被調(diào)用還是在框架代碼被調(diào)用。
再回到源碼中來(lái),currentClient.request 源碼如下就是組裝 request 然后構(gòu)造一個(gè) future 然后調(diào)用 NettyClient 發(fā)送請(qǐng)求。
我們?cè)賮?lái)看一下 DefaultFuture 的內(nèi)部,你有沒(méi)有想過(guò)一個(gè)問(wèn)題,因?yàn)槭钱惒剑敲催@個(gè) future 保存了之后,等響應(yīng)回來(lái)了如何找到對(duì)應(yīng)的 future 呢?
這里就揭秘了!就是利用一個(gè)唯一 ID。
可以看到 Request 會(huì)生成一個(gè)全局唯一 ID,然后 future 內(nèi)部會(huì)將自己和 ID 存儲(chǔ)到一個(gè) ConcurrentHashMap。這個(gè) ID 發(fā)送到服務(wù)端之后,服務(wù)端也會(huì)把這個(gè) ID 返回來(lái),這樣通過(guò)這個(gè) ID 再去ConcurrentHashMap 里面就可以找到對(duì)應(yīng)的 future ,這樣整個(gè)連接就正確且完整了!
我們?cè)賮?lái)看看最終接受到響應(yīng)的代碼,應(yīng)該就很清晰了。
先看下一個(gè)響應(yīng)的 message 的樣子:
看到這個(gè) ID 了吧,最終會(huì)調(diào)用 DefaultFuture#received的方法。
為了能讓大家更加的清晰,我再畫個(gè)圖:
到這里差不多客戶端調(diào)用主流程已經(jīng)很清晰了,其實(shí)還有很多細(xì)節(jié),之后的文章再講述,不然一下太亂太雜了。
發(fā)起請(qǐng)求的調(diào)用鏈如下圖所示:
處理請(qǐng)求響應(yīng)的調(diào)用鏈如下圖所示
調(diào)用流程-服務(wù)端端源碼分析
服務(wù)端接收到請(qǐng)求之后就會(huì)解析請(qǐng)求得到消息,這消息又有五種派發(fā)策略:
默認(rèn)走的是 all,也就是所有消息都派發(fā)到業(yè)務(wù)線程池中,我們來(lái)看下 AllChannelHandler 的實(shí)現(xiàn)。
就是將消息封裝成一個(gè) ChannelEventRunnable 扔到業(yè)務(wù)線程池中執(zhí)行,ChannelEventRunnable 里面會(huì)根據(jù) ChannelState 調(diào)用對(duì)于的處理方法,這里是 ChannelState.RECEIVED,所以調(diào)用 handler.received,最終會(huì)調(diào)用 HeaderExchangeHandler#handleRequest,我們就來(lái)看下這個(gè)代碼。
這波關(guān)鍵點(diǎn)看到了吧,構(gòu)造的響應(yīng)先塞入請(qǐng)求的 ID,我們?cè)賮?lái)看看這個(gè) reply 干了啥。
最后的調(diào)用我們已經(jīng)清楚了,實(shí)際上會(huì)調(diào)用一個(gè) Javassist 生成的代理類,里面包含了真正的實(shí)現(xiàn)類,之前已經(jīng)分析過(guò)了這里就不再深入了,我們?cè)賮?lái)看看getInvoker 這個(gè)方法,看看怎么根據(jù)請(qǐng)求的信息找到對(duì)應(yīng)的 invoker 的。
關(guān)鍵就是那個(gè) serviceKey, 還記得之前服務(wù)暴露將invoker 封裝成 exporter 之后再構(gòu)建了一個(gè) serviceKey將其和 exporter 存入了 exporterMap 中吧,這 map 這個(gè)時(shí)候就起作用了!
這個(gè) Key 就長(zhǎng)這樣:
找到 invoker 最終調(diào)用實(shí)現(xiàn)類具體的方法再返回響應(yīng)整個(gè)流程就完結(jié)了,我再補(bǔ)充一下之前的圖。
總結(jié)
今天的調(diào)用過(guò)程我再總結(jié)一遍應(yīng)該差不多了。
首先客戶端調(diào)用接口的某個(gè)方法,實(shí)際調(diào)用的是代理類,代理類會(huì)通過(guò) cluster 從 directory 中獲取一堆 invokers(如果有一堆的話),然后進(jìn)行 router 的過(guò)濾(其中看配置也會(huì)添加 mockInvoker 用于服務(wù)降級(jí)),然后再通過(guò) SPI 得到 loadBalance 進(jìn)行一波負(fù)載均衡。
這里要強(qiáng)調(diào)一下默認(rèn)的 cluster 是 FailoverCluster ,會(huì)進(jìn)行容錯(cuò)重試處理,這個(gè)日后再詳細(xì)分析。
現(xiàn)在我們已經(jīng)得到要調(diào)用的遠(yuǎn)程服務(wù)對(duì)應(yīng)的 invoker 了,此時(shí)根據(jù)具體的協(xié)議構(gòu)造請(qǐng)求頭,然后將參數(shù)根據(jù)具體的序列化協(xié)議序列化之后構(gòu)造塞入請(qǐng)求體中,再通過(guò) NettyClient 發(fā)起遠(yuǎn)程調(diào)用。
服務(wù)端 NettyServer 收到請(qǐng)求之后,根據(jù)協(xié)議得到信息并且反序列化成對(duì)象,再按照派發(fā)策略派發(fā)消息,默認(rèn)是 All,扔給業(yè)務(wù)線程池。
業(yè)務(wù)線程會(huì)根據(jù)消息類型判斷然后得到 serviceKey 從之前服務(wù)暴露生成的 exporterMap 中得到對(duì)應(yīng)的 Invoker ,然后調(diào)用真實(shí)的實(shí)現(xiàn)類。
最終將結(jié)果返回,因?yàn)檎?qǐng)求和響應(yīng)都有一個(gè)統(tǒng)一的 ID, 客戶端根據(jù)響應(yīng)的 ID 找到存儲(chǔ)起來(lái)的 Future, 然后塞入響應(yīng)再喚醒等待 future 的線程,完成一次遠(yuǎn)程調(diào)用全過(guò)程。
而且還小談了下模板方法這個(gè)設(shè)計(jì)模式,當(dāng)然其實(shí)隱藏了很多設(shè)計(jì)模式在其中,比如責(zé)任鏈、裝飾器等等,沒(méi)有特意挑開(kāi)來(lái)說(shuō),源碼中太常見(jiàn)了,基本上無(wú)處不在。
當(dāng)前名稱:Dubbo服務(wù)調(diào)用過(guò)程
網(wǎng)頁(yè)URL:http://m.5511xx.com/article/coseecj.html


咨詢
建站咨詢
