原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch4-rpc/ch4-07-pbgo.html
pbgo 是我們專門針對(duì)本節(jié)內(nèi)容設(shè)計(jì)的較為完整的迷你框架,它基于 Protobuf 的擴(kuò)展語法,通過插件自動(dòng)生成 rpc 和 rest 相關(guān)代碼。在本章第二節(jié)我們已經(jīng)展示過如何定制一個(gè) Protobuf 代碼生成插件,并生成了 rpc 部分的代碼。在本節(jié)我們將重點(diǎn)講述 pbgo 中和 Protobuf 擴(kuò)展語法相關(guān)的 rest 部分的工作原理。
目前 Protobuf 相關(guān)的很多開源項(xiàng)目都使用到了 Protobuf 的擴(kuò)展語法。在前一節(jié)中提到的驗(yàn)證器就是通過給結(jié)構(gòu)體成員增加擴(kuò)展元信息實(shí)現(xiàn)驗(yàn)證。在 grpc-gateway 項(xiàng)目中,則是通過為服務(wù)的每個(gè)方法增加 Http 相關(guān)的映射規(guī)則實(shí)現(xiàn)對(duì) Rest 接口的支持。pbgo 也是通過 Protobuf 的擴(kuò)展語法來為 rest 接口增加元信息。
pbgo 的擴(kuò)展語法在 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 框架的一個(gè)部分,需要被其他的 proto 文件導(dǎo)入。Protobuf 本身自有一套完整的包體系,在這里包的路徑就是 pbgo。Go 語言也有自己的一套包體系,我們需要通過 go_package 的擴(kuò)展語法定義 Protobuf 和 Go 語言之間包的映射關(guān)系。定義 Protobuf 和 Go 語言之間包的映射關(guān)系之后,其他導(dǎo)入 pbgo.ptoto 包的 Protobuf 文件在生成 Go 語言時(shí),會(huì)生成 pbgo.proto 映射的 Go 語言包路徑。
Protobuf 擴(kuò)展語法有五種類型,分別是針對(duì)文件的擴(kuò)展信息、針對(duì) message 的擴(kuò)展信息、針對(duì) message 成員的擴(kuò)展信息、針對(duì) service 的擴(kuò)展信息和針對(duì) service 方法的擴(kuò)展信息。在使用擴(kuò)展前首先需要通過 extend 關(guān)鍵字定義擴(kuò)展的類型和可以用于擴(kuò)展的成員。擴(kuò)展成員可以是基礎(chǔ)類型,也可以是一個(gè)結(jié)構(gòu)體類型。pbgo 中只定義了 service 的方法的擴(kuò)展,只定義了一個(gè)名為 rest_api 的擴(kuò)展成員,類型是 HttpRule 結(jié)構(gòu)體。
定義好擴(kuò)展之后,我們就可以從其他的 Protobuf 文件中使用 pbgo 的擴(kuò)展。創(chuàng)建一個(gè) 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
文件引入擴(kuò)展定義,然后在 HelloService 的 Hello 方法中使用了 pbgo 定義的擴(kuò)展。Hello 方法擴(kuò)展的信息表示該方法對(duì)應(yīng)一個(gè) REST 接口,只有一個(gè) GET 方法對(duì)應(yīng) "/hello/:value" 路徑。在 REST 方法的路徑中采用了 httprouter 路由包的語法規(guī)則,":value" 表示路徑中的該字段對(duì)應(yīng)的是參數(shù)中同名的成員。
在本章的第二節(jié)我們已經(jīng)簡(jiǎn)單講述過 Protobuf 插件的工作原理,并且展示了如何生成 RPC 必要的代碼。插件是一個(gè) 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ù)中提前擴(kuò)展定義的元數(shù)據(jù)。
pbgo 框架中的插件對(duì)象是 pbgoPlugin,在 Generate 方法中首先需要遍歷 Protobuf 文件中定義的全部服務(wù),然后再遍歷每個(gè)服務(wù)的每個(gè)方法。在得到方法結(jié)構(gòu)之后再通過自定義的 getServiceMethodOption 方法提取 rest 擴(kuò)展信息:
func (p *pbgoPlugin) Generate(file *generator.FileDescriptor) {
for _, svc := range file.Service {
for _, m := range svc.Method {
httpRule := p.getServiceMethodOption(m)
...
}
}
}
在講述 getServiceMethodOption 方法之前我們先回顧下方法擴(kuò)展的定義:
extend google.protobuf.MethodOptions {
HttpRule rest_api = 20180715;
}
pbgo 為服務(wù)的方法定義了一個(gè) rest_api 名字的擴(kuò)展,在最終生成的 Go 語言代碼中會(huì)包含一個(gè) pbgo.E_RestApi 全局變量,通過該全局變量可以獲取用戶定義的擴(kuò)展信息。
下面是 getServiceMethodOption 方法的實(shí)現(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ù)判斷每個(gè)方法是否定義了擴(kuò)展,然后通過 proto.GetExtension 函數(shù)獲取用戶定義的擴(kuò)展信息。在獲取到擴(kuò)展信息之后,我們?cè)賹U(kuò)展轉(zhuǎn)型為 pbgo.HttpRule 類型。
有了擴(kuò)展信息之后,我們就可以參考第二節(jié)中生成 RPC 代碼的方式生成 REST 相關(guān)的代碼。
pbgo 框架同時(shí)也提供了一個(gè)插件用于生成 REST 代碼。不過我們的目的是學(xué)習(xí) pbgo 框架的設(shè)計(jì)過程,因此我們先嘗試手寫 Hello 方法對(duì)應(yīng)的 REST 代碼,然后插件再根據(jù)手寫的代碼構(gòu)造模板自動(dòng)生成代碼。
HelloService 只有一個(gè) Hello 方法,Hello 方法只定義了一個(gè) GET 方式的 REST 接口:
message String {
string value = 1;
}
service HelloService {
rpc Hello (String) returns (String) {
option (pbgo.rest_api) = {
get: "/hello/:value"
};
}
}
為了方便最終的用戶,我們需要為 HelloService 構(gòu)造一個(gè)路由。因此我們希望有個(gè)一個(gè)類似 HelloServiceHandler 的函數(shù),可以基于 HelloServiceInterface 服務(wù)的接口生成一個(gè)路由處理器:
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 方法注冊(cè)到路由處理器:
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 方法注冊(cè)路由函數(shù)。在路由函數(shù)內(nèi)部首先通過 ps.ByName("value")
從 URL 中加載 value 參數(shù),然后通過 pbgo.PopulateFieldFromPath 輔助函數(shù)設(shè)置 value 參數(shù)對(duì)應(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ù)卻是異常簡(jiǎn)單。首先要構(gòu)造一個(gè)滿足 HelloServiceInterface 接口的服務(wù)對(duì)象:
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ǎn)單返回結(jié)果。然后調(diào)用該服務(wù)對(duì)應(yīng)的 HelloServiceHandler 函數(shù)生成路由處理器,并啟動(dòng)服務(wù):
func main() {
router := hello_pb.HelloServiceHandler(new(HelloService))
log.Fatal(http.ListenAndServe(":8080", router))
}
然后在命令行測(cè)試 REST 服務(wù):
$ curl localhost:8080/hello/vgo
這樣一個(gè)超級(jí)簡(jiǎn)單的 pbgo 框架就完成了!
![]() | ![]() |
更多建議: