微服務(wù)架構(gòu)已成為目前互聯(lián)網(wǎng)架構(gòu)的趨勢,關(guān)于微服務(wù)的討論,幾乎占據(jù)了各種技術(shù)大會的絕大多數(shù)版面。國內(nèi)使用最多的服務(wù)治理框架非阿里開源的 dubbo 莫屬,千米網(wǎng)也選擇了 dubbo 作為微服務(wù)治理框架。另一方面,和大多數(shù)互聯(lián)網(wǎng)公司一樣,千米的開發(fā)語言是多樣的,大多數(shù)后端業(yè)務(wù)由 java 支撐,而每個業(yè)務(wù)線有各自開發(fā)語言的選擇權(quán),便出現(xiàn)了 nodejs,python,go 多語言調(diào)用的問題。
“專業(yè)、務(wù)實(shí)、高效、創(chuàng)新、把客戶的事當(dāng)成自己的事”是我們每一個人一直以來堅持追求的企業(yè)文化。 創(chuàng)新互聯(lián)是您可以信賴的網(wǎng)站建設(shè)服務(wù)商、專業(yè)的互聯(lián)網(wǎng)服務(wù)提供商! 專注于網(wǎng)站設(shè)計制作、網(wǎng)站制作、軟件開發(fā)、設(shè)計服務(wù)業(yè)務(wù)。我們始終堅持以客戶需求為導(dǎo)向,結(jié)合用戶體驗與視覺傳達(dá),提供有針對性的項目解決方案,提供專業(yè)性的建議,創(chuàng)新互聯(lián)建站將不斷地超越自我,追逐市場,引領(lǐng)市場!
跨語言調(diào)用是一個很大的話題,也是一個很有挑戰(zhàn)的技術(shù)活,目前業(yè)界經(jīng)常被提及的解決方案有如下幾種,不妨拿出來老生常談一番:
當(dāng)我們再聊跨語言調(diào)用時我們在聊什么?縱觀上述幾個較為通用,成熟的解決方案,可以得出結(jié)論:解決跨語言調(diào)用的思路無非是兩種:
如果一個新型的團(tuán)隊面臨技術(shù)選型,我認(rèn)為上述的方案都可以納入?yún)⒖?,可考慮到遺留系統(tǒng)的兼容性問題
舊系統(tǒng)的遷移成本
這也關(guān)鍵的選型因素。我們做出的第一個嘗試,便是在 RPC 協(xié)議上下功夫。
通用協(xié)議的跨語言支持
springmvc的美好時代
springmvc
springmvc
在沒有實(shí)現(xiàn)真正的跨語言調(diào)用之前,想要實(shí)現(xiàn)“跨語言”大多數(shù)方案是使用 http 協(xié)議做一層轉(zhuǎn)換,最常見的手段莫過于借助 springmvc 提供的 controller/restController,間接調(diào)用 dubbo provider。這種方案的優(yōu)勢和劣勢顯而易見
通用協(xié)議的支持
事實(shí)上,大多數(shù)服務(wù)治理框架都支持多種協(xié)議,dubbo 框架除默認(rèn)的 dubbo 協(xié)議之外,還有當(dāng)當(dāng)網(wǎng)擴(kuò)展的?rest協(xié)議和千米網(wǎng)擴(kuò)展的?json-rpc?協(xié)議可供選擇。這兩者都是通用的跨語言協(xié)議。
rest 協(xié)議為滿足 JAX-RS 2.0 標(biāo)準(zhǔn)規(guī)范,在開發(fā)過程中引入了 @Path,@POST,@GET 等注解,習(xí)慣于編寫傳統(tǒng) rpc 接口的人可能不太習(xí)慣 rest 風(fēng)格的 rpc 接口。一方面這樣會影響開發(fā)體驗,另一方面,獨(dú)樹一幟的接口風(fēng)格使得它與其他協(xié)議不太兼容,舊接口的共生和遷移都無法實(shí)現(xiàn)。如果沒有遺留系統(tǒng),rest 協(xié)議無疑是跨語言方案最簡易的實(shí)現(xiàn),絕大多數(shù)語言支持 rest 協(xié)議。
和 rest 協(xié)議類似,json-rpc 的實(shí)現(xiàn)也是文本序列化http 協(xié)議。dubbox 在 restful 接口上已經(jīng)做出了嘗試,但是 rest 架構(gòu)和 dubbo 原有的 rpc 架構(gòu)是有區(qū)別的,rest 架構(gòu)需要對資源(Resources)進(jìn)行定義, 需要用到 http 協(xié)議的基本操作 GET、POST、PUT、DELETE。在我們看來,restful 更合適互聯(lián)網(wǎng)系統(tǒng)之間的調(diào)用,而 rpc 更適合一個系統(tǒng)內(nèi)的調(diào)用。使用 json-rpc 協(xié)議使得舊接口得以兼顧,開發(fā)習(xí)慣仍舊保留,同時獲得了跨語言的能力。
千米網(wǎng)在早期實(shí)踐中采用了 json-rpc 作為 dubbo 的跨語言協(xié)議實(shí)現(xiàn),并開源了基于 json-rpc 協(xié)議下的 python 客戶端?dubbo-client-py?和 node 客戶端?dubbo-node-client,使用 python 和 nodejs 的小伙伴可以借助于它們直接調(diào)用 dubbo-provider-java 提供的 rpc 服務(wù)。系統(tǒng)中大多數(shù) java 服務(wù)之間的互相調(diào)用還是以 dubbo 協(xié)議為主,考慮到新舊協(xié)議的適配,在不影響原有服務(wù)的基礎(chǔ)上,我們配置了雙協(xié)議。
dubbo 協(xié)議主要支持 java 間的相互調(diào)用,適配老接口;json-rpc 協(xié)議主要支持異構(gòu)語言的調(diào)用。
定制協(xié)議的跨語言支持
微服務(wù)框架所謂的協(xié)議(protocol)可以簡單理解為:報文格式和序列化方案。服務(wù)治理框架一般都提供了眾多的協(xié)議配置項供使用者選擇,除去上述兩種通用協(xié)議,還存在一些定制化的協(xié)議,如 dubbo 框架的默認(rèn)協(xié)議:dubbo 協(xié)議以及 motan 框架提供的跨語言協(xié)議:motan2。
motan2協(xié)議的跨語言支持
???motan2
motan2
motan2 協(xié)議被設(shè)計用來滿足跨語言的需求主要體現(xiàn)在兩個細(xì)節(jié)中—MetaData 和 motan-go。在最初的 motan 協(xié)議中,協(xié)議報文僅由 Header+Body 組成,這樣導(dǎo)致 path,param,group 等存儲在 Body 中的數(shù)據(jù)需要反序列得到,這對異構(gòu)語言來說是很不友好的,所以在 motan2 中修改了協(xié)議的組成;weibo 開源了?motan-go?,motan-php?,motan-openresty?,并借助于 motan-go 充當(dāng)了 agent 這一翻譯官的角色,使用 simple 序列化方案來序列化協(xié)議報文的 Body 部分(simple 序列化是一種較弱的序列化方案)。
???agent
agent
仔細(xì)揣摩下可以發(fā)現(xiàn)這么做和雙協(xié)議的配置區(qū)別并不是大,只不過這里的 agent 是隱式存在的,與主服務(wù)共生。明顯的區(qū)別在于 agent 方案中異構(gòu)語言并不直接交互。
dubbo協(xié)議的跨語言支持
dubbo 協(xié)議設(shè)計之初只考慮到了常規(guī)的 rpc 調(diào)用場景,它并不是為跨語言而設(shè)計,但跨語言支持從來不是只有支持、不支持兩種選擇,而是要按難易程度來劃分。是的,dubbo 協(xié)議的跨語言調(diào)用可能并不好做,但并非無法實(shí)現(xiàn)。千米網(wǎng)便實(shí)現(xiàn)了這一點(diǎn),nodejs 構(gòu)建的前端業(yè)務(wù)是異構(gòu)語言的主戰(zhàn)場,最終實(shí)現(xiàn)了 dubbo2.js,打通了 nodejs 和原生 dubbo 協(xié)議。作為本文第二部分的核心內(nèi)容,重點(diǎn)介紹下我們使用 dubbo2.js 干了什么事。
Dubbo協(xié)議報文格式
???dubbo協(xié)議
dubbo協(xié)議
dubbo協(xié)議報文消息頭詳解:
magic:類似java字節(jié)碼文件里的魔數(shù),用來判斷是不是 dubbo 協(xié)議的數(shù)據(jù)包。魔數(shù)是常量 0xdabb
flag:標(biāo)志位, 一共8個地址位。低四位用來表示消息體數(shù)據(jù)用的序列化工具的類型(默認(rèn) hessian),高四位中,第一位為 1 表示是 request 請求,第二位為 1 表示雙向傳輸(即有返回 response),第三位為 1 表示是心跳 ping 事件。
status:狀態(tài)位, 設(shè)置請求響應(yīng)狀態(tài),dubbo 定義了一些響應(yīng)的類型。具體類型見com.alibaba.dubbo.remoting.exchange.Response
invoke id:消息 id, long 類型。每一個請求的唯一識別 id(由于采用異步通訊的方式,用來把請求 request 和返回的 response 對應(yīng)上)
body length:消息體 body 長度, int 類型,即記錄 Body Content 有多少個字節(jié)
body content:請求參數(shù),響應(yīng)參數(shù)的抽象序列化之后存儲于此。
協(xié)議報文最終都會變成字節(jié),使用 tcp 傳輸,任何語言只要支持網(wǎng)絡(luò)模塊,有類似 Socket 之類的封裝,那么通信就不成問題。那,跨語言難在哪兒?以其他語言調(diào)用 java 來說,主要有兩個難點(diǎn):
ps:dubbo 協(xié)議通訊demo( )
Protocol 還有一個實(shí)現(xiàn)分支是 AbstractProxyProtocol,如下圖所示:
從圖中我們可以看到:gRPC、HTTP、WebService、Hessian、Thrift 等協(xié)議對應(yīng)的 Protocol 實(shí)現(xiàn),都是繼承自 AbstractProxyProtocol 抽象類。
目前互聯(lián)網(wǎng)的技術(shù)棧百花齊放,很多公司會使用 Node.js、Python、Rails、Go 等語言來開發(fā) 一些 Web 端應(yīng)用,同時又有很多服務(wù)會使用 Java 技術(shù)棧實(shí)現(xiàn),這就出現(xiàn)了大量的跨語言調(diào)用的需求。Dubbo 作為一個 RPC 框架,自然也希望能實(shí)現(xiàn)這種跨語言的調(diào)用,目前 Dubbo 中使用“HTTP 協(xié)議 + JSON-RPC”的方式來達(dá)到這一目的,其中 HTTP 協(xié)議和 JSON 都是天然跨語言的標(biāo)準(zhǔn),在各種語言中都有成熟的類庫。
下面就重點(diǎn)來分析 Dubbo 對 HTTP 協(xié)議的支持。首先,會介紹 JSON-RPC 的基礎(chǔ),并通過一個示例,快速入門,然后介紹 Dubbo 中 HttpProtocol 的具體實(shí)現(xiàn),也就是如何將 HTTP 協(xié)議與 JSON-RPC 結(jié)合使用,實(shí)現(xiàn)跨語言調(diào)用的效果。
Dubbo 中支持的 HTTP 協(xié)議實(shí)際上使用的是 JSON-RPC 協(xié)議。
JSON-RPC 是基于 JSON 的跨語言遠(yuǎn)程調(diào)用協(xié)議。Dubbo 中的 dubbo-rpc-xml、dubbo-rpc-webservice 等模塊支持的 XML-RPC、WebService 等協(xié)議與 JSON-RPC 一樣,都是基于文本的協(xié)議,只不過 JSON 的格式比 XML、WebService 等格式更加簡潔、緊湊。與 Dubbo 協(xié)議、Hessian 協(xié)議等二進(jìn)制協(xié)議相比,JSON-RPC 更便于調(diào)試和實(shí)現(xiàn),可見 JSON-RPC 協(xié)議還是一款非常優(yōu)秀的遠(yuǎn)程調(diào)用協(xié)議。
在 Java 體系中,有很多成熟的 JSON-RPC 框架,例如 jsonrpc4j、jpoxy 等,其中,jsonrpc4j 本身體積小巧,使用方便,既可以獨(dú)立使用,也可以與 Spring 無縫集合,非常適合基于 Spring 的項目。
下面先來看看 JSON-RPC 協(xié)議中請求的基本格式:
JSON-RPC請求中各個字段的含義如下:
在 JSON-RPC 的服務(wù)端收到調(diào)用請求之后,會查找到相應(yīng)的方法并進(jìn)行調(diào)用,然后將方法的返回值整理成如下格式,返回給客戶端:
JSON-RPC響應(yīng)中各個字段的含義如下:
Dubbo 使用 jsonrpc4j 庫來實(shí)現(xiàn) JSON-RPC 協(xié)議,下面使用 jsonrpc4j 編寫一個簡單的 JSON-RPC 服務(wù)端示例程序和客戶端示例程序,并通過這兩個示例程序說明 jsonrpc4j 最基本的使用方式。
首先,需要創(chuàng)建服務(wù)端和客戶端都需要的 domain 類以及服務(wù)接口。先來創(chuàng)建一個 User 類,作為最基礎(chǔ)的數(shù)據(jù)對象:
接下來創(chuàng)建一個 UserService 接口作為服務(wù)接口,其中定義了 5 個方法,分別用來創(chuàng)建 User、查詢 User 以及相關(guān)信息、刪除 User:
UserServiceImpl 是 UserService 接口的實(shí)現(xiàn)類,其中使用一個 ArrayList 集合管理 User 對象,具體實(shí)現(xiàn)如下:
整個用戶管理業(yè)務(wù)的核心大致如此。下面我們來看服務(wù)端如何將 UserService 與 JSON-RPC 關(guān)聯(lián)起來。
首先,創(chuàng)建 RpcServlet 類,它是 HttpServlet 的子類,并覆蓋了 HttpServlet 的 service() 方法。我們知道,HttpServlet 在收到 GET 和 POST 請求的時候,最終會調(diào)用其 service() 方法進(jìn)行處理;HttpServlet 還會將 HTTP 請求和響應(yīng)封裝成 HttpServletRequest 和 HttpServletResponse 傳入 service() 方法之中。這里的 RpcServlet 實(shí)現(xiàn)之中會創(chuàng)建一個 JsonRpcServer,并在 service() 方法中將 HTTP 請求委托給 JsonRpcServer 進(jìn)行處理:
最后,創(chuàng)建一個 JsonRpcServer 作為服務(wù)端的入口類,在其 main() 方法中會啟動 Jetty 作為 Web 容器,具體實(shí)現(xiàn)如下:
這里使用到的 web.xml 配置文件如下:
完成服務(wù)端的編寫之后,下面再繼續(xù)編寫 JSON-RPC 的客戶端。在 JsonRpcClient 中會創(chuàng)建 JsonRpcHttpClient,并通過 JsonRpcHttpClient 請求服務(wù)端:
在 AbstractProxyProtocol 的 export() 方法中,首先會根據(jù) URL 檢查 exporterMap 緩存,如果查詢失敗,則會調(diào)用 ProxyFactory.getProxy() 方法將 Invoker 封裝成業(yè)務(wù)接口的代理類,然后通過子類實(shí)現(xiàn)的 doExport() 方法啟動底層的 ProxyProtocolServer,并初始化 serverMap 集合。具體實(shí)現(xiàn)如下:
在 HttpProtocol 的 doExport() 方法中,與前面介紹的 DubboProtocol 的實(shí)現(xiàn)類似,也要啟動一個 RemotingServer。為了適配各種 HTTP 服務(wù)器,例如,Tomcat、Jetty 等,Dubbo 在 Transporter 層抽象出了一個 HttpServer 的接口。
dubbo-remoting-http 模塊的入口是 HttpBinder 接口,它被 @SPI 注解修飾,是一個擴(kuò)展接口,有三個擴(kuò)展實(shí)現(xiàn),默認(rèn)使用的是 JettyHttpBinder 實(shí)現(xiàn),如下圖所示:
HttpBinder 接口中的 bind() 方法被 @Adaptive 注解修飾,會根據(jù) URL 的 server 參數(shù)選擇相應(yīng)的 HttpBinder 擴(kuò)展實(shí)現(xiàn),不同 HttpBinder 實(shí)現(xiàn)返回相應(yīng)的 HttpServer 實(shí)現(xiàn)。HttpServer 的繼承關(guān)系如下圖所示:
這里以 JettyHttpServer 為例簡單介紹 HttpServer 的實(shí)現(xiàn),在 JettyHttpServer 中會初始化 Jetty Server,其中會配置 Jetty Server 使用到的線程池以及處理請求 Handler:
可以看到 JettyHttpServer 收到的全部請求將委托給 DispatcherServlet 這個 HttpServlet 實(shí)現(xiàn),而 DispatcherServlet 的 service() 方法會把請求委托給對應(yīng)接端口的 HttpHandler 處理:
了解了 Dubbo 對 HttpServer 的抽象以及 JettyHttpServer 的核心之后,回到 HttpProtocol 中的 doExport() 方法繼續(xù)分析。
在 HttpProtocol.doExport() 方法中會通過 HttpBinder 創(chuàng)建前面介紹的 HttpServer 對象,并記錄到 serverMap 中用來接收 HTTP 請求。這里初始化 HttpServer 以及處理請求用到的 HttpHandler 是 HttpProtocol 中的內(nèi)部類,在其他使用 HTTP 協(xié)議作為基礎(chǔ)的 RPC 協(xié)議實(shí)現(xiàn)中也有類似的 HttpHandler 實(shí)現(xiàn)類,如下圖所示:
在 HttpProtocol.InternalHandler 中的 handle() 實(shí)現(xiàn)中,會將請求委托給 skeletonMap 集合中記錄的 JsonRpcServer 對象進(jìn)行處理:
skeletonMap 集合中的 JsonRpcServer 是與 HttpServer 對象一同在 doExport() 方法中初始化的。最后,我們來看 HttpProtocol.doExport() 方法的實(shí)現(xiàn):
介紹完 HttpProtocol 暴露服務(wù)的相關(guān)實(shí)現(xiàn)之后,下面再來看 HttpProtocol 中引用服務(wù)相關(guān)的方法實(shí)現(xiàn),即 protocolBindinRefer() 方法實(shí)現(xiàn)。該方法首先通過 doRefer() 方法創(chuàng)建業(yè)務(wù)接口的代理,這里會使用到 jsonrpc4j 庫中的 JsonProxyFactoryBean 與 Spring 進(jìn)行集成,在其 afterPropertiesSet() 方法中會創(chuàng)建 JsonRpcHttpClient 對象:
下面來看 doRefer() 方法的具體實(shí)現(xiàn):
在 AbstractProxyProtocol.protocolBindingRefer() 方法中,會通過 ProxyFactory.getInvoker() 方法將 doRefer() 方法返回的代理對象轉(zhuǎn)換成 Invoker 對象,并記錄到 Invokers 集合中,具體實(shí)現(xiàn)如下:
本文重點(diǎn)介紹了在 Dubbo 中如何通過“HTTP 協(xié)議 + JSON-RPC”的方案實(shí)現(xiàn)跨語言調(diào)用。首先介紹了 JSON-RPC 中請求和響應(yīng)的基本格式,以及其實(shí)現(xiàn)庫 jsonrpc4j 的基本使用;接下來我們還詳細(xì)介紹了 Dubbo 中 AbstractProxyProtocol、HttpProtocol 等核心類,剖析了 Dubbo 中“HTTP 協(xié)議 + JSON-RPC”方案的落地實(shí)現(xiàn)。
如果我們手動寫一個簡單的RPC調(diào)用,一般需要把服務(wù)調(diào)用的信息傳遞給服務(wù)端,包括每次服務(wù)調(diào)用的一些共用信息包括服務(wù)調(diào)用接口、方法名、方法參數(shù)類型和方法參數(shù)值等,在傳遞方法參數(shù)值時需要先序列化對象并經(jīng)過網(wǎng)絡(luò)傳輸?shù)椒?wù)端,在服務(wù)端接受后再按照客戶端序列化的順序再做一次反序列化,然后拼裝成請求對象進(jìn)行服務(wù)反射調(diào)用,最終將調(diào)用結(jié)果傳給客戶端。Dubbo的實(shí)現(xiàn)也基本是相同的原理,下圖是Dubbo一次完整RPC調(diào)用中經(jīng)過的步驟:
首先在客戶端啟動時,會從注冊中心拉取和訂閱對應(yīng)的服務(wù)列表,Cluster會把拉取的服務(wù)列表聚合成一個Invoker,每次RPC調(diào)用前會通過Directory#list獲取providers地址(已經(jīng)生成好的Invoker地址),獲取這些服務(wù)列表給后續(xù)路由和負(fù)載均衡使用。對應(yīng)上圖①中將多個服務(wù)提供者做聚合。在框架內(nèi)部實(shí)現(xiàn)Directory接口的是RegistryDirectory類,它和接口名是一對一的關(guān)系(每一個接口都有一個RegistryDirectory實(shí)例),主要負(fù)責(zé)拉取和訂閱服務(wù)提供者、動態(tài)配置和路由項。
在Dubbo發(fā)起服務(wù)調(diào)用時,所有路由和負(fù)載均衡都是在客戶端實(shí)現(xiàn)的??蛻舳朔?wù)調(diào)用首先會觸發(fā)路由操作,然后將路由結(jié)果得到的服務(wù)列表作為負(fù)載均衡參數(shù),經(jīng)過負(fù)載均衡后會選出一臺機(jī)器進(jìn)行RPC調(diào)用,這3個步驟一次對應(yīng)圖中②③④??蛻舳私?jīng)過路由和負(fù)載均衡后,會將請求交給底層IO線程池(如Netty)進(jìn)行處理,IO線程池主要處理讀寫、序列化和反序列化等邏輯,因此這里一定不能阻塞操作,Dubbo也提供參數(shù)控制(decode.in.io)參數(shù),在處理反序列化對象時會在業(yè)務(wù)線程池中處理。在⑤中包含兩種類似的線程池,一種是IO線程池(Netty),另一種是Dubbo業(yè)務(wù)線程池(承載業(yè)務(wù)方法調(diào)用)。
目前Dubbo將服務(wù)調(diào)用和Telnet調(diào)用做了端口復(fù)用,子啊編解碼層面也做了適配。在Telnet調(diào)用時,會新建立一個TCP連接,傳遞接口、方法和json格式的參數(shù)進(jìn)行服務(wù)調(diào)用,在編解碼層面簡單讀取流中的字符串(因為不是Dubbo標(biāo)準(zhǔn)頭報文),最終交給Telnet對應(yīng)的Handler去解析方法調(diào)用。如果不是Telnet調(diào)用,則服務(wù)提供方會根據(jù)傳遞過來的接口、分組和版本信息查找Invoker對應(yīng)的實(shí)例進(jìn)行反射調(diào)用。在⑦中進(jìn)行了端口復(fù)用,Telnet和正常RPC調(diào)用不一樣的地方是序列化和反序列化使用的不是Hessian方式,而是直接使用fastjson進(jìn)行處理。
講解完主要調(diào)用原理,接下來開始探討細(xì)節(jié),比如Dubbo協(xié)議、編解碼實(shí)現(xiàn)和線程模型等,本篇重點(diǎn)主要放在⑤⑥⑦。
Dubbo協(xié)議參考了現(xiàn)有的TCP/IP協(xié)議,每一次RPC調(diào)用包括協(xié)議頭和協(xié)議體兩部分。16字節(jié)長的報文頭部主要包含魔數(shù)(0xdabb),以及當(dāng)前請求報文是否是Request、Response、心跳和事件的信息,請求時也會攜帶當(dāng)前報文體內(nèi)序列化協(xié)議編號。除此之外,報文頭還攜帶了請求狀態(tài),以及請求唯一標(biāo)識和報文體長度。
在消息體中,客戶端嚴(yán)格按照序列化順序?qū)懭胂ⅲ?wù)端也會遵循相同的順序讀取消息,客戶端發(fā)起的請求消息體一次依次保存下列內(nèi)容:Dubbo版本號、服務(wù)接口名、服務(wù)接口版本、方法名、參數(shù)類型、方法參數(shù)值和請求額外參數(shù)(attachment)。
服務(wù)端返回的響應(yīng)消息體主要包含回值狀態(tài)標(biāo)記和返回值,其中回值狀態(tài)標(biāo)記包含6中:
我們知道在網(wǎng)絡(luò)通信中(TCP)需要解決網(wǎng)絡(luò)粘包/解包的問題,常用的方法比如用回車、換行、固定長度和特殊分隔符進(jìn)行處理,而Dubbo是使用特殊符號0xdabb魔法數(shù)來分割處理粘包問題。
在實(shí)際場景中,客戶端會使用多線程并發(fā)調(diào)用服務(wù),Dubbo如何做到正確響應(yīng)調(diào)用線程呢?關(guān)鍵在于協(xié)議頭的全局請求id標(biāo)識,先看原理圖:
當(dāng)客戶端多個線程并發(fā)請求時,框架內(nèi)部會調(diào)用DefaultFuture對象的get方法進(jìn)行等待。在請求發(fā)起時,框架內(nèi)部會創(chuàng)建Request對象,這時候會被分配一個唯一id,DefaultFuture可以從Request中獲取id,并將關(guān)聯(lián)關(guān)系存儲到靜態(tài)HashMap中,就是上圖中的Futures集合。當(dāng)客戶端收到響應(yīng)時,會根據(jù)Response對象中的id,從Futures集合中查找對應(yīng)DefaultFuture對象,最終會喚醒對應(yīng)的線程并通知結(jié)果??蛻舳艘矔右粋€定時掃描線程去探測超時沒有返回的請求。
先了解一下編解碼器的類關(guān)系圖:
如上,AbstractCodec主要提供基礎(chǔ)能力,比如校驗報文長度和查找具體編解碼器等。TransportCodec主要抽象編解碼實(shí)現(xiàn),自動幫我們?nèi)フ{(diào)用序列化、反序列化實(shí)現(xiàn)和自動cleanup流。我們通過Dubbo編解碼繼承結(jié)構(gòu)可以清晰看到,DubboCodec繼承自ExchageCodec,它又再次繼承了TelnetCodec實(shí)現(xiàn)。我們前面說過Telnet實(shí)現(xiàn)復(fù)用了Dubbo協(xié)議端口,其實(shí)就是在這層編解碼做了通用處理。因為流中可能包含多個RPC請求,Dubbo框架嘗試一次性讀取更多完整報文編解碼生成對象,也就是圖中的DubboCountCodec,它的實(shí)現(xiàn)思想比較簡單,依次調(diào)用DubboCodec去解碼,如果能解碼成完整報文,則加入消息列表,然后觸發(fā)下一個Handler方法調(diào)用。
編碼器的作用是將Java對象轉(zhuǎn)成字節(jié)流,主要分兩部分,構(gòu)造報文頭部,和對消息體進(jìn)行序列化處理。所有編輯碼層實(shí)現(xiàn)都應(yīng)該繼承自ExchangeCodec,當(dāng)Dubbo協(xié)議編碼請求對象時,會調(diào)用ExchangeCodec#encode方法。我們來看下這個方法是如何對請求對象進(jìn)行編碼的:
如上,是Dubbo將請求對象轉(zhuǎn)成字節(jié)流的過程,其中encodeRequestData方法是對RpcInvocation調(diào)用對象的編碼,主要是對接口、方法、方法參數(shù)類型、方法參數(shù)等進(jìn)行編碼,在DubboCodec#encodeRequestData中對此方法進(jìn)行了重寫:
如上,響應(yīng)編碼與請求編碼的邏輯基本大同小異,在編碼出現(xiàn)異常時,會將異常響應(yīng)返回給客戶端,防止客戶端只能一直等到超時。為了防止報錯對象無法在客戶端反序列化,在服務(wù)端會將異常信息轉(zhuǎn)成字符串處理。對于響應(yīng)體的編碼,在DubboCodec#encodeResponseData方法中實(shí)現(xiàn):
注意不管什么樣的響應(yīng),都會先寫入1個字節(jié)的標(biāo)識符,具體的值和含義前面已經(jīng)講過。
解碼相對更復(fù)雜一些,分為2部分,第一部分是解碼報文的頭部,第二部分是解碼報文體內(nèi)容并將其轉(zhuǎn)換成RpcInvocation對象。我們先看服務(wù)端接受到請求后的解碼過程,具體解碼實(shí)現(xiàn)在ExchangeCodec#decode方法:
可以看出,解碼過程中需要解決粘包和半包問題。接下來我們看一下DubboCodec對消息題解碼的實(shí)現(xiàn):
如上,如果默認(rèn)配置在IO線程解碼,直接調(diào)用decode方法;否則不做解碼,延遲到業(yè)務(wù)線程池中解碼。這里沒有提到的是心跳和事件的解碼,其實(shí)很簡單,心跳報文是沒有消息體的,事件又消息體,在使用Hessian2協(xié)議的情況下默認(rèn)會傳遞字符R,當(dāng)優(yōu)雅停機(jī)時會通過發(fā)送readonly事件來通知客戶端當(dāng)前服務(wù)端不可用。
接下來,我們分析一下如何把消息體轉(zhuǎn)換成RpcInvocation對象,具體在DecodeableRpcInvocation#decode方法中:
解碼請求時,嚴(yán)格按照客戶端寫數(shù)據(jù)的順序處理。
解碼響應(yīng)和解碼請求類似,調(diào)用的同樣是DubboCodec#decodeBody,就是上面省略的部分,這里就不贅述了,重點(diǎn)看下響應(yīng)體的解碼,即DecodeableRpcResult#decode方法:
如果讀者熟悉Netty,就很容易理解Dubbo內(nèi)部使用的ChannelHandler組件的原理,Dubbo內(nèi)部使用了大量的Handler組成類似鏈表,依次處理具體邏輯,包括編解碼、心跳時間戳和方法調(diào)用Handler等。因為Nettty每次創(chuàng)建Handler都會經(jīng)過ChannelPipeline,大量的事件經(jīng)過很多Pipeline會有較多開銷,因此Dubbo會將多個Handler聚合成一個Handler。(個人表示,這簡直bullshit)
Dubbo的Channelhandler有5中狀態(tài):
Dubbo針對每個特性都會實(shí)現(xiàn)對應(yīng)的ChannelHandler,在講解Handler的指責(zé)之前,我們Dubbo有哪些常用的Handler:
Dubbo提供了大量的Handler去承載特性和擴(kuò)展,這些Handler最終會和底層通信框架做關(guān)聯(lián),比如Netty等。一次完整的RPC調(diào)用貫穿了一系列的Handler,如果直接掛載到底層通信框架(Netty),因為整個鏈路比較長,則需要大量鏈?zhǔn)讲檎液褪录粌H低效,而且浪費(fèi)資源。
下圖展示了同時具有入站和出站ChannelHandler的布局,如果一個入站事件被觸發(fā),比如連接或數(shù)據(jù)讀取,那么它會從ChannelPipeline頭部一直傳播到ChannelPipeline的尾端。出站的IO事件將從ChannelPipeline最右邊開始,然后向左傳播。當(dāng)然ChannelPipeline傳播時,會檢測入站的是否實(shí)現(xiàn)了ChannelInboundHandler,出站會檢測是否實(shí)現(xiàn)了ChannelOutboundHandler,如果沒有實(shí)現(xiàn),則自動跳過。Dubbo框架中實(shí)現(xiàn)這兩個接口類主要是NettyServerHandler和NettyClientHandler。Dubbo通過裝飾者模式包裝Handler,從而不需要將每個Handler都追加到Pipeline中。因此NettyServer和NettyClient中最多有3個Handler,分別是編碼、解碼和NettyHandler。
講完Handler的流轉(zhuǎn)機(jī)制后,我們再來探討RPC調(diào)用Provider方處理Handler的邏輯,在DubboProtocol中通過內(nèi)部類繼承自ExchangeHandlerAdapter,完成服務(wù)提供方Invoker實(shí)例的查找并進(jìn)行服務(wù)的真實(shí)調(diào)用。
如上是觸發(fā)業(yè)務(wù)方法調(diào)用的關(guān)鍵,在服務(wù)暴露時服務(wù)端已經(jīng)按照特定規(guī)則(端口、接口名、接口版本和接口分組)把實(shí)例Invoker存儲到HashMap中,客戶端調(diào)用過來時必須攜帶相同信息構(gòu)造的key,找到對應(yīng)Exporter(里面持有Invoker)然后調(diào)用。
我們先跟蹤getInvoker的實(shí)現(xiàn),會發(fā)現(xiàn)服務(wù)端唯一標(biāo)識的服務(wù)由4部分組成:端口、接口名、接口版本和接口分組。
如上,Dispatcher是線程池的派發(fā)器。這里需要注意的是,Dispatcher真實(shí)的職責(zé)是創(chuàng)建有線程派發(fā)能力的ChannelHandler,比如AllChannelHandler、MessageOnlyChannelHandler和ExecutionChannelHanlder,其本身并不具備線程派發(fā)能力。
Dispatcher屬于Dubbo中的擴(kuò)展點(diǎn),這個擴(kuò)展點(diǎn)用來動態(tài)產(chǎn)生Handler,以滿足不同的場景,目前Dubbo支持一下6種策略調(diào)用:
具體需要按照使用場景不同啟用不同的策略,建議使用默認(rèn)策略,如果在TCP連接中需要做安全或校驗,則可以使用ConnectionOrderedDispatcher策略。如果引入新的線程池,則不可避免的導(dǎo)致額外的線程切換,用戶可在Dubbo配置中指定dispatcher屬性讓具體策略生效。
在Dubbo內(nèi)部,所有方法調(diào)用都被抽象成Request/Response,每次調(diào)用都會創(chuàng)建一個Request,如果是方法調(diào)用則返回一個Response對象。HeaderExceptionExchangeHandler就是用了處理這種場景,主要負(fù)責(zé)4中事情:
(1) 更新發(fā)送和讀取請求時間戳。
(2) 判斷請求格式或編解碼是否有錯,并響應(yīng)客戶端失敗的具體原因。
(3) 處理Request請求和Response正常響應(yīng)。
(4) 支持Telnet調(diào)用。
我們先來看一下HeaderExchangeHandler#received實(shí)現(xiàn):