當前位置:
首頁 > 科技 > gRPC客戶端創建和調用原理解析

gRPC客戶端創建和調用原理解析

接收程序員的 8 點技術早餐

作者|李林鋒

編輯|張浩

本文選自「深入淺出 gRPC」系列文章(6 篇共 45000 字),更多 gRPC 相關內容請點擊下方圖片詳細了解。

背景

gRPC 是在 HTTP/2 之上實現的 RPC 框架,HTTP/2 是第 7 層(應用層)協議,它運行在 TCP(第 4 層 - 傳輸層)協議之上,相比於傳統的 REST/JSON 機制有諸多的優點:

基於 HTTP/2 之上的二進位協議(Protobuf 序列化機制);

一個連接上可以多路復用,並發處理多個請求和響應;

多種語言的類庫實現;

服務定義文件和自動代碼生成(.proto 文件和 Protobuf 編譯工具)。

此外,gRPC 還提供了很多擴展點,用於對框架進行功能定製和擴展,例如,通過開放負載均衡介面可以無縫的與第三方組件進行集成對接(Zookeeper、域名解析服務、SLB 服務等)。

一個完整的 RPC 調用流程示例如下:

gRPC 的 RPC 調用與上述流程相似,下面我們一起學習下 gRPC 的客戶端創建和服務調用流程。

客戶端調用總體流程

gRPC 的客戶端調用總體流程如下圖所示:

gRPC 的客戶端調用流程如下:

客戶端 Stub(GreeterBlockingStub) 調用 sayHello(request),發起 RPC 調用;

通過 DnsNameResolver 進行域名解析,獲取服務端的地址信息(列表),隨後使用默認的 LoadBalancer 策略,選擇一個具體的 gRPC 服務端實例;

如果與路由選中的服務端之間沒有可用的連接,則創建 NettyClientTransport 和 NettyClientHandler,發起 HTTP/2 連接;

對請求消息使用 PB(Protobuf)做序列化,通過 HTTP/2 Stream 發送給 gRPC 服務端;

接收到服務端響應之後,使用 PB(Protobuf)做反序列化;

回調 GrpcFuture 的 set(Response) 方法,喚醒阻塞的客戶端調用線程,獲取 RPC 響應。

需要指出的是,客戶端同步阻塞 RPC 調用阻塞的是調用方線程(通常是業務線程),底層 Transport 的 I/O 線程(Netty 的 NioEventLoop)仍然是非阻塞的。

基於 Netty 的 HTTP/2 Client 創建流程

gRPC 客戶端底層基於 Netty4.1 的 HTTP/2 協議棧框架構建,以便可以使用 HTTP/2 協議來承載 RPC 消息,在滿足標準化規範的前提下,提升通信性能。

gRPC HTTP/2 協議棧(客戶端)的關鍵實現是 NettyClientTransport 和 NettyClientHandler,客戶端初始化流程如下所示:

流程關鍵技術點解讀:

NettyClientHandler 的創建:級聯創建 Netty 的 Http2FrameReader、Http2FrameWriter 和 Http2Connection,用於構建基於 Netty 的 gRPC HTTP/2 客戶端協議棧。

HTTP/2 Client 啟動:仍然基於 Netty 的 Bootstrap 來初始化並啟動客戶端,但是有兩個細節需要注意:

NettyClientHandler(實際被包裝成 ProtocolNegotiator.Handler,用於 HTTP/2 的握手協商)創建之後,不是由傳統的 ChannelInitializer 在初始化 Channel 時將 NettyClientHandler 加入到 pipeline 中,而是直接通過 Bootstrap 的 handler 方法直接加入到 pipeline 中,以便可以立即接收發送任務。

客戶端使用的 work 線程組並非通常意義的 EventLoopGroup,而是一個 EventLoop:即 HTTP/2 客戶端使用的 work 線程並非一組線程(默認線程數為 CPU 內核 * 2),而是一個 EventLoop 線程。這個其實也很容易理解,一個 NioEventLoop 線程可以同時處理多個 HTTP/2 客戶端連接,它是多路復用的,對於單個 HTTP/2 客戶端,如果默認獨佔一個 work 線程組,將造成極大的資源浪費,同時也可能會導致句柄溢出(並發啟動大量 HTTP/2 客戶端)。

WriteQueue 創建:Netty 的 NioSocketChannel 初始化並向 Selector 註冊之後(發起 HTTP 連接之前),立即由 NettyClientHandler 創建 WriteQueue,用於接收並處理 gRPC 內部的各種 Command,例如鏈路關閉指令、發送 Frame 指令、發送 Ping 指令等。

HTTP/2 Client 創建完成之後,即可由客戶端根據協商策略發起 HTTP/2 連接。如果連接創建成功,後續即可復用該 HTTP/2 連接,進行 RPC 調用。

RPC 請求消息發送流程

gRPC 默認基於 Netty HTTP/2 + PB 進行 RPC 調用,請求消息發送流程如下所示:

流程關鍵技術點解讀:

ClientCallImpl 的 sendMessage 調用,主要完成了請求對象的序列化(基於 PB)、HTTP/2 Frame 的初始化;

ClientCallImpl 的 halfClose 調用將客戶端準備就緒的請求 Frame 封裝成自定義的 SendGrpcFrameCommand,寫入到 WriteQueue 中;

WriteQueue 執行 flush() 將 SendGrpcFrameCommand 寫入到 Netty 的 Channel 中,調用 Channel 的 write 方法,被 NettyClientHandler 攔截到,由 NettyClientHandler 負責具體的發送操作;

NettyClientHandler 調用 Http2ConnectionEncoder 的 writeData 方法,將 Frame 寫入到 HTTP/2 Stream 中,完成請求消息的發送。

客戶端源碼分析

gRPC 客戶端調用原理並不複雜,但是代碼卻相對比較繁雜。下面圍繞關鍵的類庫,對主要功能點進行源碼分析。

ProtocolNegotiator 功能和源碼分析

ProtocolNegotiator 用於 HTTP/2 連接創建的協商,gRPC 支持三種策略並有三個實現子類:

gRPC 的 ProtocolNegotiator 實現類完全遵循 HTTP/2 相關規範,以 PlaintextUpgradeNegotiator 為例,通過設置 Http2ClientUpgradeCodec,用於 101 協商和協議升級,相關代碼如下所示(PlaintextUpgradeNegotiator 類):

RPC 請求調用源碼分析

請求調用主要有兩步:請求 Frame 構造和 Frame 發送,請求 Frame 構造代碼如下所示(ClientCallImpl 類):

使用 PB 對請求消息做序列化,生成 InputStream,構造請求 Frame:

Frame 發送代碼如下所示:

NettyClientHandler 接收到發送事件之後,調用 Http2ConnectionEncoder 將 Frame 寫入 Netty HTTP/2 協議棧(NettyClientHandler 類):

作者介紹

李林鋒 《Netty 權威指南》和《分散式服務框架原理與實踐》作者。有多年 Java NIO、平台中間件、PaaS 平台、API 網關設計和開發經驗。精通 Netty、Mina、分散式服務框架、雲計算等,目前從事軟體公司的 API 開放相關的架構和設計工作。

我模擬了一個用瀏覽器挖礦的代碼


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 InfoQ 的精彩文章:

介於公有雲與私有雲之間 專有雲緣何興起?
大齡程序員都去哪了?

TAG:InfoQ |