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 開放相關的架構和設計工作。
我模擬了一個用瀏覽器挖礦的代碼


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