無縫銜接 gRPC 與 dubbo-go

最近我們 dubbo-go 社區裡面,呼聲很大的一個 feature 就是對 gRPC 的支持。在某位大佬的不懈努力之下,終於弄出來了。

今天我就給大家分析一下大佬是怎麼連接 dubbo-go 和 gRPC 。

gRPC

先來簡單介紹一下 gRPC 。它是 Google 推出來的一個 RPC 框架。gRPC是通過 IDL ( Interface Definition Language )——接口定義語言——編譯成不同語言的客戶端來實現的。可以說是RPC理論的一個非常非常標準的實現。

因而 gRPC 天然就支持多語言。這幾年,它幾乎成為了跨語言 RPC 框架的標準實現方式了,很多優秀的rpc框架,如 Spring Cloud 和 dubbo ,都支持 gRPC 。

server 端

在 Go 裡面,server 端的用法是:

無縫銜接 gRPC 與 dubbo-go

它的關鍵部分是:s := grpc.NewServer()和pb.RegisterGreeterServer(s, &server{})兩個步驟。第一個步驟很容易,唯獨第二個步驟RegisterGreeterServer有點麻煩。為什麼呢?

因為pb.RegisterGreeterServer(s, &server{})這個方法是通過用戶定義的protobuf編譯出來的。

好在,這個編譯出來的方法,本質上是:

無縫銜接 gRPC 與 dubbo-go

也就是說,如果我們在 dubbo-go 裡面拿到這個 _Greeter_serviceDesc ,就可以實現這個 server 的註冊。因此,可以看到,在 dubbo-go 裡面,要解決的一個關鍵問題就是如何拿到這個 serviceDesc 。

Client 端

Client 端的用法是:

無縫銜接 gRPC 與 dubbo-go

這個東西要複雜一點:1、創建連接:conn, err := grpc.Dial(address)2、創建client:c := pb.NewGreeterClient(conn)3、調用方法:r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})

第一個問題其實挺好解決的,畢竟我們可以從用戶的配置裡面讀出 address ;

第二個問題就是最難的地方了。如同 RegisterGreeterServer 是被編譯出來的那樣,這個 NewGreeterClient 也是被編譯出來的。

而第三個問題,乍一看是用反射就能解決,但是我們打開 SayHello 就能看到:

無縫銜接 gRPC 與 dubbo-go

結合 greetClient 的定義,很容易看到,我們的關鍵就在於 err := c.cc.Invoke ( ctx, "/helloworld.Greeter/SayHello", in, out, opts... )。換言之,我們只需要創建出來連接,並且拿到方法、參數就能通過類似的調用來模擬出 c.SayHello 。

通過對 gRPC 的簡單分析,我們大概知道要怎麼弄了。還剩下一個問題,就是我們的解決方案怎麼和 dubbo-go 結合起來呢?

設計

我們先來看一下 dubbo-go 的整體設計,思考一下,如果我們要做 gRPC 的適配,應該是在哪個層次上做適配。

無縫銜接 gRPC 與 dubbo-go

我們根據前面介紹的 gRPC 的相關特性可以看出來,gRPC 已經解決了 codec 和 transport 兩層的問題。

而從 cluster 往上,顯然 gRPC 沒有涉及。於是,從這個圖裡面我們就可以看出來,要做這種適配,那麼 protocol 這一層是最合適的。即,我們可以如同 dubbo protocol 那般,擴展出來一個 grpc protocol 。

這個 gRPC protocol 大體上相當於一個適配器,將底層的 gRPC 的實現和我們自身的 dubbo-go 連接在一起。

無縫銜接 gRPC 與 dubbo-go

實現

在 dubbo-go 裡面,和 gRPC 相關的主要是:

無縫銜接 gRPC 與 dubbo-go

我們直接進去看看在 gRPC 小節裡面提到的要點是如何實現的。

server端

無縫銜接 gRPC 與 dubbo-go

這樣看起來,還是很清晰的。如同 dubbo- go 其它的 protocol 一樣,先拿到 service ,而後通過 service 來拿到 serviceDesc ,完成服務的註冊。

注意一下上圖我紅線標準的 ds, ok := service.(DubboGrpcService) 這一句。

為什麼我說這個地方有點奇怪呢?是因為理論上來說,我們這裡註冊的這個 service 實際上就是 protobuf 編譯之後生成的 gRPC 服務端的那個 service ——很顯然,單純的編譯一個 protobuf 接口,它肯定不會實現 DubboGrpcService 接口:

無縫銜接 gRPC 與 dubbo-go

那麼 ds, ok := service.(DubboGrpcService) 這一句,究竟怎麼才能讓它能夠執行成功呢?

我會在後面給大家揭曉這個謎底。

Client端

dubbo-go 設計了自身的 Client ,作為對 gRPC 裡面 Client 的一種模擬與封裝:

無縫銜接 gRPC 與 dubbo-go

注意看,這個 Client 的定義與前面 greetClient 的定義及其相似。再看下面的 NewClient 方法,裡面也無非就是創建了連接 conn ,而後利用 conn 裡創建了一個 Client 實例。

注意的是,這裡面維護的 invoker 實際上是一個 stub 。

當真正發起調用的時候:

無縫銜接 gRPC 與 dubbo-go

紅色框框框住的就是關鍵步驟。利用反射從 invoker ——也就是 stub ——裡面拿到調用的方法,而後通過反射調用。

代碼生成

前面提到過 ds, ok := service.(DubboGrpcService) 這一句,面臨的問題是如何讓 protobuf 編譯生成的代碼能夠實現 DubboGrpcService 接口呢?

有些小夥伴可能也注意到,在我貼出來的一些代碼裡面,反射操作會根據名字來獲取method實例,比如NewClient方法裡面的method := reflect.ValueOf(impl).MethodByName("GetDubboStub")這一句。這一句的impl,即指服務的實現,也是 protobuf 裡面編譯出來的,怎麼讓 protobuf 編譯出來的代碼裡面含有這個 GetDubboStub 方法呢?

到這裡,答案已經呼之欲出了:修改 protobuf 編譯生成代碼的邏輯!

慶幸的是,在 protobuf 裡面允許我們通過插件的形式擴展我們自己的代碼生成的邏輯。

所以我們只需要註冊一個我們自己的插件:

無縫銜接 gRPC 與 dubbo-go

然後這個插件會把我們所需要的代碼給嵌入進去。比如說嵌入GetDubboStub方法:

無縫銜接 gRPC 與 dubbo-go

還有DubboGrpcService接口:

無縫銜接 gRPC 與 dubbo-go

這個東西,屬於難者不會會者不難。就是如果你不知道可以通過plugin的形式來修改生成的代碼,那就是真難;但是如果知道了,這個東西就很簡單了——無非就是水磨工夫罷了。


查看更多:https://yq.aliyun.com/articles/742946?utm_content=g_1000103688


上雲就看雲棲號:更多雲資訊,上雲案例,最佳實踐,產品入門,訪問:https://yqh.aliyun.com/


分享到:


相關文章: