Go 語言 pbgo: 基于 Protobuf 的框架

2023-03-22 15:03 更新

原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch4-rpc/ch4-07-pbgo.html


4.7 pbgo: 基于 Protobuf 的框架

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 部分的工作原理。

4.7.1 Protobuf 擴(kuò)展語法

目前 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ù)中同名的成員。

4.7.2 插件中讀取擴(kuò)展信息

在本章的第二節(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)的代碼。

4.7.3 生成 REST 代碼

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 文件,讀者可以自行參考。

4.7.4 啟動(dòng) REST 服務(wù)

雖然從頭構(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 框架就完成了!



以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)