原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch4-rpc/ch4-06-grpc-ext.html
目前開源社區(qū)已經(jīng)圍繞 Protobuf 和 gRPC 開發(fā)出眾多擴展,形成了龐大的生態(tài)。本節(jié)我們將簡單介紹驗證器和 REST 接口擴展。
到目前為止,我們接觸的全部是第三版的 Protobuf 語法。第二版的 Protobuf 有個默認(rèn)值特性,可以為字符串或數(shù)值類型的成員定義默認(rèn)值。
我們采用第二版的 Protobuf 語法創(chuàng)建文件:
syntax = "proto2";
package main;
message Message {
optional string name = 1 [default = "gopher"];
optional int32 age = 2 [default = 10];
}
內(nèi)置的默認(rèn)值語法其實是通過 Protobuf 的擴展選項特性實現(xiàn)。在第三版的 Protobuf 中不再支持默認(rèn)值特性,但是我們可以通過擴展選項自己模擬默認(rèn)值特性。
下面是用 proto3 語法的擴展特性重新改寫上述的 proto 文件:
syntax = "proto3";
package main;
import "google/protobuf/descriptor.proto";
extend google.protobuf.FieldOptions {
string default_string = 50000;
int32 default_int = 50001;
}
message Message {
string name = 1 [(default_string) = "gopher"];
int32 age = 2[(default_int) = 10];
}
其中成員后面的方括號內(nèi)部的就是擴展語法。重新生成 Go 語言代碼,里面會包含擴展選項相關(guān)的元信息:
var E_DefaultString = &proto.ExtensionDesc{
ExtendedType: (*descriptor.FieldOptions)(nil),
ExtensionType: (*string)(nil),
Field: 50000,
Name: "main.default_string",
Tag: "bytes,50000,opt,name=default_string,json=defaultString",
Filename: "helloworld.proto",
}
var E_DefaultInt = &proto.ExtensionDesc{
ExtendedType: (*descriptor.FieldOptions)(nil),
ExtensionType: (*int32)(nil),
Field: 50001,
Name: "main.default_int",
Tag: "varint,50001,opt,name=default_int,json=defaultInt",
Filename: "helloworld.proto",
}
我們可以在運行時通過類似反射的技術(shù)解析出 Message 每個成員定義的擴展選項,然后從每個擴展的相關(guān)聯(lián)的信息中解析出我們定義的默認(rèn)值。
在開源社區(qū)中,github.com/mwitkow/go-proto-validators 已經(jīng)基于 Protobuf 的擴展特性實現(xiàn)了功能較為強大的驗證器功能。要使用該驗證器首先需要下載其提供的代碼生成插件:
$ go get github.com/mwitkow/go-proto-validators/protoc-gen-govalidators
然后基于 go-proto-validators 驗證器的規(guī)則為 Message 成員增加驗證規(guī)則:
syntax = "proto3";
package main;
import "github.com/mwitkow/go-proto-validators/validator.proto";
message Message {
string important_string = 1 [
(validator.field) = {regex: "^[a-z]{2,5}$"}
];
int32 age = 2 [
(validator.field) = {int_gt: 0, int_lt: 100}
];
}
在方括弧表示的成員擴展中,validator.field 表示擴展是 validator 包中定義的名為 field 擴展選項。validator.field 的類型是 FieldValidator 結(jié)構(gòu)體,在導(dǎo)入的 validator.proto 文件中定義。
所有的驗證規(guī)則都由 validator.proto 文件中的 FieldValidator 定義:
syntax = "proto2";
package validator;
import "google/protobuf/descriptor.proto";
extend google.protobuf.FieldOptions {
optional FieldValidator field = 65020;
}
message FieldValidator {
// Uses a Golang RE2-syntax regex to match the field contents.
optional string regex = 1;
// Field value of integer strictly greater than this value.
optional int64 int_gt = 2;
// Field value of integer strictly smaller than this value.
optional int64 int_lt = 3;
// ... more ...
}
從 FieldValidator 定義的注釋中我們可以看到驗證器擴展的一些語法:其中 regex 表示用于字符串驗證的正則表達式,int_gt 和 int_lt 表示數(shù)值的范圍。
然后采用以下的命令生成驗證函數(shù)代碼:
protoc \
--proto_path=${GOPATH}/src \
--proto_path=${GOPATH}/src/github.com/google/protobuf/src \
--proto_path=. \
--govalidators_out=. --go_out=plugins=grpc:.\
hello.proto
windows: 替換 ?
${GOPATH}
? 為 ?%GOPATH%
? 即可.
以上的命令會調(diào)用 protoc-gen-govalidators 程序,生成一個獨立的名為 hello.validator.pb.go 的文件:
var _regex_Message_ImportantString = regexp.MustCompile("^[a-z]{2,5}$")
func (this *Message) Validate() error {
if !_regex_Message_ImportantString.MatchString(this.ImportantString) {
return go_proto_validators.FieldError("ImportantString", fmt.Errorf(
`value '%v' must be a string conforming to regex "^[a-z]{2,5}$"`,
this.ImportantString,
))
}
if !(this.Age> 0) {
return go_proto_validators.FieldError("Age", fmt.Errorf(
`value '%v' must be greater than '0'`, this.Age,
))
}
if !(this.Age < 100) {
return go_proto_validators.FieldError("Age", fmt.Errorf(
`value '%v' must be less than '100'`, this.Age,
))
}
return nil
}
生成的代碼為 Message 結(jié)構(gòu)體增加了一個 Validate 方法,用于驗證該成員是否滿足 Protobuf 中定義的條件約束。無論采用何種類型,所有的 Validate 方法都用相同的簽名,因此可以滿足相同的驗證接口。
通過生成的驗證函數(shù),并結(jié)合 gRPC 的截取器,我們可以很容易為每個方法的輸入?yún)?shù)和返回值進行驗證。
gRPC 服務(wù)一般用于集群內(nèi)部通信,如果需要對外暴露服務(wù)一般會提供等價的 REST 接口。通過 REST 接口比較方便前端 JavaScript 和后端交互。開源社區(qū)中的 grpc-gateway 項目就實現(xiàn)了將 gRPC 服務(wù)轉(zhuǎn)為 REST 服務(wù)的能力。
grpc-gateway 的工作原理如下圖:
圖 4-2 gRPC-Gateway 工作流程
通過在 Protobuf 文件中添加路由相關(guān)的元信息,通過自定義的代碼插件生成路由相關(guān)的處理代碼,最終將 REST 請求轉(zhuǎn)給更后端的 gRPC 服務(wù)處理。
路由擴展元信息也是通過 Protobuf 的元數(shù)據(jù)擴展用法提供:
syntax = "proto3";
package main;
import "google/api/annotations.proto";
message StringMessage {
string value = 1;
}
service RestService {
rpc Get(StringMessage) returns (StringMessage) {
option (google.api.http) = {
get: "/get/{value}"
};
}
rpc Post(StringMessage) returns (StringMessage) {
option (google.api.http) = {
post: "/post"
body: "*"
};
}
}
我們首先為 gRPC 定義了 Get 和 Post 方法,然后通過元擴展語法在對應(yīng)的方法后添加路由信息。其中 “/get/{value}” 路徑對應(yīng)的是 Get 方法,{value}
部分對應(yīng)參數(shù)中的 value 成員,結(jié)果通過 json 格式返回。Post 方法對應(yīng) “/post” 路徑,body 中包含 json 格式的請求信息。
然后通過以下命令安裝 protoc-gen-grpc-gateway 插件:
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
再通過插件生成 grpc-gateway 必須的路由處理代碼:
$ protoc -I/usr/local/include -I. \
-I$GOPATH/src \
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--grpc-gateway_out=. --go_out=plugins=grpc:.\
hello.proto
windows: 替換 ?
${GOPATH}
? 為 ?%GOPATH%
? 即可.
插件會為 RestService 服務(wù)生成對應(yīng)的 RegisterRestServiceHandlerFromEndpoint 函數(shù):
func RegisterRestServiceHandlerFromEndpoint(
ctx context.Context, mux *runtime.ServeMux, endpoint string,
opts []grpc.DialOption,
) (err error) {
...
}
RegisterRestServiceHandlerFromEndpoint 函數(shù)用于將定義了 Rest 接口的請求轉(zhuǎn)發(fā)到真正的 gRPC 服務(wù)。注冊路由處理函數(shù)之后就可以啟動 Web 服務(wù)了:
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
mux := runtime.NewServeMux()
err := RegisterRestServiceHandlerFromEndpoint(
ctx, mux, "localhost:5000",
[]grpc.DialOption{grpc.WithInsecure()},
)
if err != nil {
log.Fatal(err)
}
http.ListenAndServe(":8080", mux)
}
啟動 grpc 服務(wù) , 端口 5000
type RestServiceImpl struct{}
func (r *RestServiceImpl) Get(ctx context.Context, message *StringMessage) (*StringMessage, error) {
return &StringMessage{Value: "Get hi:" + message.Value + "#"}, nil
}
func (r *RestServiceImpl) Post(ctx context.Context, message *StringMessage) (*StringMessage, error) {
return &StringMessage{Value: "Post hi:" + message.Value + "@"}, nil
}
func main() {
grpcServer := grpc.NewServer()
RegisterRestServiceServer(grpcServer, new(RestServiceImpl))
lis, _ := net.Listen("tcp", ":5000")
grpcServer.Serve(lis)
}
首先通過 runtime.NewServeMux() 函數(shù)創(chuàng)建路由處理器,然后通過 RegisterRestServiceHandlerFromEndpoint 函數(shù)將 RestService 服務(wù)相關(guān)的 REST 接口中轉(zhuǎn)到后面的 gRPC 服務(wù)。grpc-gateway 提供的 runtime.ServeMux 類也實現(xiàn)了 http.Handler 接口,因此可以和標(biāo)準(zhǔn)庫中的相關(guān)函數(shù)配合使用。
當(dāng) gRPC 和 REST 服務(wù)全部啟動之后,就可以用 curl 請求 REST 服務(wù)了:
$ curl localhost:8080/get/gopher
{"value":"Get: gopher"}
$ curl localhost:8080/post -X POST --data '{"value":"grpc"}'
{"value":"Post: grpc"}
在對外公布 REST 接口時,我們一般還會提供一個 Swagger 格式的文件用于描述這個接口規(guī)范。
$ go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
$ protoc -I. \
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--swagger_out=. \
hello.proto
然后會生成一個 hello.swagger.json 文件。這樣的話就可以通過 swagger-ui 這個項目,在網(wǎng)頁中提供 REST 接口的文檔和測試等功能。
最新的 Nginx 對 gRPC 提供了深度支持。可以通過 Nginx 將后端多個 gRPC 服務(wù)聚合到一個 Nginx 服務(wù)。同時 Nginx 也提供了為同一種 gRPC 服務(wù)注冊多個后端的功能,這樣可以輕松實現(xiàn) gRPC 負載均衡的支持。Nginx 的 gRPC 擴展是一個較大的主題,感興趣的讀者可以自行參考相關(guān)文檔。
![]() | ![]() |
更多建議: