背景
在移動端平台開發中,為了增加代碼復用,降低開發成本,通常會需要採用跨平台的開發技術,花椒也不例外。本次新的單品開發,由於時間緊,人員有限,經過調研選型,最終確定了 flutter 方案(具體選型過程不在本文討論之內)。
為了讓客戶端更專注業務實現,降低接口聯調測試成本,我們選用了 gRPC 方案。gRPC是一個高性能、通用的開源 RPC 框架,由 Google 開發並基於 HTTP/2 協議標準而設計,基於 ProtoBuf(Protocol Buffers)序列化協議開發,且支持當前主流開發語言。gRPC通過定義一個服務並指定一個可以遠程調用的帶有參數和返回類型的的方法,使客戶端可以直接調用不同機器上的服務應用的方法,就像是本地對象一樣。在服務端,服務實現這個接口並且運行 gRPC 服務處理客戶端調用。在客戶端,有一個stub提供和服務端相同的方法。
gRPC
特點
- 基於標準化的 IDL(ProtoBuf)來生成伺服器端和客戶端代碼,支持多種主流開發語言。同時可以更好的支持團隊與團隊之間的接口設計,開發,測試,協作等。
- 基於 HTTP/2 設計,支持雙向流,多路復用,頭部壓縮。
- 支持流式發送和響應,批量傳輸數據,提升性能。
- ProtoBuf 序列化數據抓包、調試難度較大。我們使用服務端注入方式提供了用戶或設備過濾,請求及返回值日誌捕獲,並開發對應後台模擬抓包展示。
- 相比 JSON, 對前端不夠友好。gRPC 生態 提供了 gateway 的方式為 gRPC 服務代理出 RESTful 接口。
- ProtoBuf 提供了非常強的擴展性,可以為 protoc 開發定製插件,從而擴展 proto 文件的功能及描述性。
gRPC-Web
gRPC-Web 為前端瀏覽器提供了 Javascript 庫用來訪問 gRPC 服務,但是需要通過 Envoy 提供代理服務。相比 JSON 的方式對前端不夠友好,同時也增加了服務端的部署成本。因此在這次項目中前端未使用 gRPC 服務,而是由 gRPC-Gateway 提供代理的 RESTful 接口。
gRPC-Gateway
grpc-gateway 是 protoc 的一個插件,它能讀取 gRPC 的服務定義並生成反向代理伺服器,將 RESTful 的 JSON 請求轉換為 gRPC 的方式。這樣無需太多工作即可實現一套基於 gRPC 服務的 RESTful 接口,方便前端使用調用接口,同時也方便開發過程中通過 Postman/Paw 之類的工具調試接口。
gateway -> gRPC 映射方式:
- HTTP 源 IP 添加到 gRPC 的 X-Forwarded-For 請求頭
- HTTP 請求 Host 添加到 gRPC 的 X-Forwarded-Host 請求頭
- HTTP 請求頭 Authorization 添加到 gRPC 的 authorization 請求頭
- HTTP 請求頭帶 Grpc-Metadata- 前綴的映射到 gRPC 的 metadata (key 名不帶前綴)
例如,gRPC 接口要求的通用的 metadata 參數(如 platform, device_id 等)在 HTTP RESTful 的傳遞方式如下:
基礎庫
dart
為了便於客戶端調用,連接復用及通用參數傳遞,我們封裝了 dart 的基礎庫。
BaseClient 維護了針對 HOST 緩存的連接池,同時也提供了接口需要傳遞的 metadata 信息。
golang
golang 後端服務需要同時支持 gRPC 和 gateway 兩種請求方式。為了簡化部署和上線依賴,gateway 和 gRPC 的功能放在了一起,並通過攔截器注入對應的功能,主要包括 gRPC 統計,訪問日誌,接口鑒權,請求參數校驗,gateway JSON 編碼等。
- 引用到的 package
- 開發流程
為了提高開發效率,方便維護及模塊復用,服務端按功能進行組件化開發。每個組件可以單獨運行一個服務,也可以和其它組件共同組成一個服務。每個組件都需要實現 Component 接口:
對應組件開發完成後,需要開發對應的服務容器,步驟如下。
- 初始化 base package
1 base.Init(context.TODO(), cfg, &global.Callback{
2 Authenticator: &auth.Callback{},
3 LogCapture: &log.Capture{},
4 })
- 如需對外提供服務,需要提供埠及 TLS 證書
base.DefaultServer.AddPublicServer(rpcPort, gatewayPort, setting.TLSConfig)
- 組件註冊
1 base.DefaultServer.RegisterComponent(&user.Component{})
2 base.DefaultServer.RegisterComponent(&push.Component{})
3 ...
- 監聽服務
base.DefaultServer.Serve()
接口定義及實現
proto 規範
gRPC 基於標準化的 IDL(ProtoBuf)來生成伺服器端和客戶端代碼,我們決定將所有的接口描述及文檔說明都放到 proto 文件中,便於查看及修改。對 proto 的接口描述及注釋的規範如下:
代碼生成
golang
1 gengo:
2 @protoc -Iproto \\
3 -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \\
4 -I${GOPATH}/src/github.com/lnnujxxy/protoc-gen-validate \\
5 -I${GOPATH}/src/github.com/youlu-cn/grpc-gen/protoc-gen-auth \\
6 --go_out=plugins=grpc:go/pb \\
7 --grpc-gateway_out=logtostderr=true:go/pb \\
8 --validate_out="lang=go:go/pb" \\
9 --auth_out="lang=go:go/pb" \\
10 proto/*.proto
- SDK 引入
golang 使用 go mod 的方式直接引入 pb 生成的 .go 文件
dart
- SDK 引入
修改 pubspec.yaml,執行 flutter packages get 或 flutter packages upgrade
1 dependencies:
2 flutter:
3 sdk: flutter
4
5 protobuf: ^0.13.4
6 grpc: ^1.0.1
7 user:
8 git:
9 url: [email protected]:project/repo.git
10 path: dart/user
- 已知問題:
- dart 在對 protobuf 生成的類型做 json 編碼時,json 中的 key 是欄位號而非名字,導致無法與其它語言交互。ISSUE (https://github.com/dart-lang/protobuf/issues/220)
文檔生成
gRPC gateway 提供了通過 proto 文件生成 swagger API 文檔,缺點是只支持 gateway 的 RESTful 接口,並且默認的展示方式有點不符合我們的常規文檔使用方式。
我們基於 protoc 插件開發了 protoc-gen-markdown 工具,可以由 proto 文件生成 markdown 文檔,提供 gRPC 接口描述,以及 RESTful 接口描述及 JSON 示例,提供全文目錄,支持錨點導航等。生成方式如下:
1gendoc:
2 @protoc -Iproto \\
3 -I${GOPATH}/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \\
4 -I${GOPATH}/src/github.com/lnnujxxy/protoc-gen-validate \\
5 -I${GOPATH}/src/github.com/youlu-cn/grpc-gen/protoc-gen-auth \\
6 --markdown_out=":doc" \\
7 proto/*.proto
文檔會在對應路徑生成接口列表 README.md,以及每個 protobuf 對應的接口文檔。
調試
傳統的 RESTful 接口在調試及問題排查時,可以通過抓包或者 MitM(中間人攻擊)的方式,配置也比較容易。而 gRPC 因為使用了 HTTP2 及 protobuf 二進位流,抓包及數據流反解難度相對較高,調試及問題排查時會比較複雜。為了解決這個問題,我們通過服務端注入的方式,配合查詢後台過濾對應的請求日誌,從而實現如下類似抓包的效果。
後續計劃
- gRPC Streaming
- 框架層集成全鏈路 Trace 支持
- 疊代優化框架,提供對應腳手架,簡化新組件/服務創建及開發流程
Go語言中文網,致力於每日分享編碼、開源等知識,歡迎關注我,會有意想不到的收穫!
本文由花椒服務端團隊原創授權發布