原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch4-rpc/ch4-07-pbgo.html
pbgo 是我們專門針對本節(jié)內(nèi)容設(shè)計的較為完整的迷你框架,它基于 Protobuf 的擴展語法,通過插件自動生成 rpc 和 rest 相關(guān)代碼。在本章第二節(jié)我們已經(jīng)展示過如何定制一個 Protobuf 代碼生成插件,并生成了 rpc 部分的代碼。在本節(jié)我們將重點講述 pbgo 中和 Protobuf 擴展語法相關(guān)的 rest 部分的工作原理。
目前 Protobuf 相關(guān)的很多開源項目都使用到了 Protobuf 的擴展語法。在前一節(jié)中提到的驗證器就是通過給結(jié)構(gòu)體成員增加擴展元信息實現(xiàn)驗證。在 grpc-gateway 項目中,則是通過為服務(wù)的每個方法增加 Http 相關(guān)的映射規(guī)則實現(xiàn)對 Rest 接口的支持。pbgo 也是通過 Protobuf 的擴展語法來為 rest 接口增加元信息。
pbgo 的擴展語法在 github.com/chai2010/pbgo/pbgo.proto
文件定義:
syntax = "proto3";
package pbgo;
option go_package = "github.com/chai2010/pbgo;pbgo";
import "google/protobuf/descriptor.proto";
extend google.protobuf.MethodOptions {
HttpRule rest_api = 20180715;
}
message HttpRule {
string get = 1;
string put = 2;
string post = 3;
string delete = 4;
string patch = 5;
}
pbgo.proto 文件是 pbgo 框架的一個部分,需要被其他的 proto 文件導(dǎo)入。Protobuf 本身自有一套完整的包體系,在這里包的路徑就是 pbgo。Go 語言也有自己的一套包體系,我們需要通過 go_package 的擴展語法定義 Protobuf 和 Go 語言之間包的映射關(guān)系。定義 Protobuf 和 Go 語言之間包的映射關(guān)系之后,其他導(dǎo)入 pbgo.ptoto 包的 Protobuf 文件在生成 Go 語言時,會生成 pbgo.proto 映射的 Go 語言包路徑。
Protobuf 擴展語法有五種類型,分別是針對文件的擴展信息、針對 message 的擴展信息、針對 message 成員的擴展信息、針對 service 的擴展信息和針對 service 方法的擴展信息。在使用擴展前首先需要通過 extend 關(guān)鍵字定義擴展的類型和可以用于擴展的成員。擴展成員可以是基礎(chǔ)類型,也可以是一個結(jié)構(gòu)體類型。pbgo 中只定義了 service 的方法的擴展,只定義了一個名為 rest_api 的擴展成員,類型是 HttpRule 結(jié)構(gòu)體。
定義好擴展之后,我們就可以從其他的 Protobuf 文件中使用 pbgo 的擴展。創(chuàng)建一個 hello.proto 文件:
syntax = "proto3";
package hello_pb;
import "github.com/chai2010/pbgo/pbgo.proto";
message String {
string value = 1;
}
service HelloService {
rpc Hello (String) returns (String) {
option (pbgo.rest_api) = {
get: "/hello/:value"
};
}
}
首先我們通過導(dǎo)入 github.com/chai2010/pbgo/pbgo.proto
文件引入擴展定義,然后在 HelloService 的 Hello 方法中使用了 pbgo 定義的擴展。Hello 方法擴展的信息表示該方法對應(yīng)一個 REST 接口,只有一個 GET 方法對應(yīng) "/hello/:value" 路徑。在 REST 方法的路徑中采用了 httprouter 路由包的語法規(guī)則,":value" 表示路徑中的該字段對應(yīng)的是參數(shù)中同名的成員。
在本章的第二節(jié)我們已經(jīng)簡單講述過 Protobuf 插件的工作原理,并且展示了如何生成 RPC 必要的代碼。插件是一個 generator.Plugin 接口:
type Plugin interface {
// Name identifies the plugin.
Name() string
// Init is called once after data structures are built but before
// code generation begins.
Init(g *Generator)
// Generate produces the code generated by the plugin for this file,
// except for the imports, by calling the generator's methods P, In,
// and Out.
Generate(file *FileDescriptor)
// GenerateImports produces the import declarations for this file.
// It is called after Generate.
GenerateImports(file *FileDescriptor)
}
我們需要在 Generate 和 GenerateImports 函數(shù)中分別生成相關(guān)的代碼。而 Protobuf 文件的全部信息都在 *generator.FileDescriptor
類型函數(shù)參數(shù)中描述,因此我們需要從函數(shù)參數(shù)中提前擴展定義的元數(shù)據(jù)。
pbgo 框架中的插件對象是 pbgoPlugin,在 Generate 方法中首先需要遍歷 Protobuf 文件中定義的全部服務(wù),然后再遍歷每個服務(wù)的每個方法。在得到方法結(jié)構(gòu)之后再通過自定義的 getServiceMethodOption 方法提取 rest 擴展信息:
func (p *pbgoPlugin) Generate(file *generator.FileDescriptor) {
for _, svc := range file.Service {
for _, m := range svc.Method {
httpRule := p.getServiceMethodOption(m)
...
}
}
}
在講述 getServiceMethodOption 方法之前我們先回顧下方法擴展的定義:
extend google.protobuf.MethodOptions {
HttpRule rest_api = 20180715;
}
pbgo 為服務(wù)的方法定義了一個 rest_api 名字的擴展,在最終生成的 Go 語言代碼中會包含一個 pbgo.E_RestApi 全局變量,通過該全局變量可以獲取用戶定義的擴展信息。
下面是 getServiceMethodOption 方法的實現(xiàn):
func (p *pbgoPlugin) getServiceMethodOption(
m *descriptor.MethodDescriptorProto,
) *pbgo.HttpRule {
if m.Options != nil && proto.HasExtension(m.Options, pbgo.E_RestApi) {
ext, _ := proto.GetExtension(m.Options, pbgo.E_RestApi)
if ext != nil {
if x, _ := ext.(*pbgo.HttpRule); x != nil {
return x
}
}
}
return nil
}
首先通過 proto.HasExtension 函數(shù)判斷每個方法是否定義了擴展,然后通過 proto.GetExtension 函數(shù)獲取用戶定義的擴展信息。在獲取到擴展信息之后,我們再將擴展轉(zhuǎn)型為 pbgo.HttpRule 類型。
有了擴展信息之后,我們就可以參考第二節(jié)中生成 RPC 代碼的方式生成 REST 相關(guān)的代碼。
pbgo 框架同時也提供了一個插件用于生成 REST 代碼。不過我們的目的是學(xué)習(xí) pbgo 框架的設(shè)計過程,因此我們先嘗試手寫 Hello 方法對應(yīng)的 REST 代碼,然后插件再根據(jù)手寫的代碼構(gòu)造模板自動生成代碼。
HelloService 只有一個 Hello 方法,Hello 方法只定義了一個 GET 方式的 REST 接口:
message String {
string value = 1;
}
service HelloService {
rpc Hello (String) returns (String) {
option (pbgo.rest_api) = {
get: "/hello/:value"
};
}
}
為了方便最終的用戶,我們需要為 HelloService 構(gòu)造一個路由。因此我們希望有個一個類似 HelloServiceHandler 的函數(shù),可以基于 HelloServiceInterface 服務(wù)的接口生成一個路由處理器:
type HelloServiceInterface interface {
Hello(in *String, out *String) error
}
func HelloServiceHandler(svc HelloServiceInterface) http.Handler {
var router = httprouter.New()
_handle_HelloService_Hello_get(router, svc)
return router
}
代碼中選擇的是開源中比較流行的 httprouter 路由引擎。其中_handle_HelloService_Hello_get 函數(shù)用于將 Hello 方法注冊到路由處理器:
func _handle_HelloService_Hello_get(
router *httprouter.Router, svc HelloServiceInterface,
) {
router.Handle("GET", "/hello/:value",
func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
var protoReq, protoReply String
err := pbgo.PopulateFieldFromPath(&protoReq, fieldPath, ps.ByName("value"))
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := svc.Hello(&protoReq, &protoReply); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := json.NewEncoder(w).Encode(&protoReply); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
},
)
}
首先通過 router.Handle 方法注冊路由函數(shù)。在路由函數(shù)內(nèi)部首先通過 ps.ByName("value")
從 URL 中加載 value 參數(shù),然后通過 pbgo.PopulateFieldFromPath 輔助函數(shù)設(shè)置 value 參數(shù)對應(yīng)的成員。當(dāng)輸入?yún)?shù)準(zhǔn)備就緒之后就可以調(diào)用 HelloService 服務(wù)的 Hello 方法,最終將 Hello 方法返回的結(jié)果用 json 編碼返回。
在手工構(gòu)造完成最終代碼的結(jié)構(gòu)之后,就可以在此基礎(chǔ)上構(gòu)造插件生成代碼的模板。完整的插件代碼和模板在 protoc-gen-pbgo/pbgo.go
文件,讀者可以自行參考。
雖然從頭構(gòu)造 pbgo 框架的過程比較繁瑣,但是使用 pbgo 構(gòu)造 REST 服務(wù)卻是異常簡單。首先要構(gòu)造一個滿足 HelloServiceInterface 接口的服務(wù)對象:
import (
"github.com/chai2010/pbgo/examples/hello.pb"
)
type HelloService struct{}
func (p *HelloService) Hello(request *hello_pb.String, reply *hello_pb.String) error {
reply.Value = "hello:" + request.GetValue()
return nil
}
和 RPC 代碼一樣,在 Hello 方法中簡單返回結(jié)果。然后調(diào)用該服務(wù)對應(yīng)的 HelloServiceHandler 函數(shù)生成路由處理器,并啟動服務(wù):
func main() {
router := hello_pb.HelloServiceHandler(new(HelloService))
log.Fatal(http.ListenAndServe(":8080", router))
}
然后在命令行測試 REST 服務(wù):
$ curl localhost:8080/hello/vgo
這樣一個超級簡單的 pbgo 框架就完成了!
更多建議: