mirror of
https://github.com/chai2010/advanced-go-programming-book.git
synced 2025-05-24 20:52:22 +00:00
ch4-07: done
This commit is contained in:
parent
0785c9ec28
commit
5de4f8821d
@ -39,7 +39,8 @@
|
|||||||
* [4.4. GRPC入门](ch4-rpc/ch4-04-grpc.md)
|
* [4.4. GRPC入门](ch4-rpc/ch4-04-grpc.md)
|
||||||
* [4.5. GRPC进阶](ch4-rpc/ch4-05-grpc-hack.md)
|
* [4.5. GRPC进阶](ch4-rpc/ch4-05-grpc-hack.md)
|
||||||
* [4.6. GRPC和Protobuf扩展](ch4-rpc/ch4-06-grpc-ext.md)
|
* [4.6. GRPC和Protobuf扩展](ch4-rpc/ch4-06-grpc-ext.md)
|
||||||
* [4.7. 补充说明](ch4-rpc/ch4-07-faq.md)
|
* [4.7. pbgo: 基于Protobuf的框架](ch4-rpc/ch4-07-pbgo.md)
|
||||||
|
* [4.8. 补充说明](ch4-rpc/ch4-08-faq.md)
|
||||||
* [第五章 Go和Web](ch5-web/readme.md)
|
* [第五章 Go和Web](ch5-web/readme.md)
|
||||||
* [5.1. Web开发简介](ch5-web/ch5-01-introduction.md)
|
* [5.1. Web开发简介](ch5-web/ch5-01-introduction.md)
|
||||||
* [5.2. Router请求路由](ch5-web/ch5-02-router.md)
|
* [5.2. Router请求路由](ch5-web/ch5-02-router.md)
|
||||||
|
@ -1,117 +0,0 @@
|
|||||||
# 4.7. Protobuf扩展语法和插件
|
|
||||||
|
|
||||||
在本章第二节我们已经展示过如何定制一个Protobuf代码生成插件。本节我们将继续深入挖掘Protobuf的高级特性,通过Protobuf的扩展特性增加自定义的元数据信息。通过针对每个方法增加Rest接口元信息,实现一个基于Protobuf的迷你Rest框架。
|
|
||||||
|
|
||||||
## Protobuf扩展语法
|
|
||||||
|
|
||||||
目前Protobuf相关的很多开源项目都使用到了Protobuf的扩展语法。在前一节中提到的验证器就是通过给结构体成员增加扩展元信息实现验证。在grpc-gateway项目中,则是通过为服务的每个方法增加Http相关的映射规则实现对Rest接口的支持。这里我们将查看下Protobuf全部的扩展语法。
|
|
||||||
|
|
||||||
扩展语法也被用来实现Protobuf内置的某些特性,比如针对不同语言的扩展选项和proto2中message成员的默认值特性:其中文件扩展选项go_package为Go语言定义了当前包的路径和包的名称。message成员的default扩展为每个成员定义了默认值。go_package和proto2的default特性底层都是通过扩展语法实现。
|
|
||||||
|
|
||||||
Protobuf扩展语法有五种类型,分别是针对文件的扩展信息、针对message的扩展信息、正对message成员的扩展信息、针对service的扩展信息和针对service方法的扩展信息。在使用扩展前首先需要通过extend关键字定义扩展的类型和可以用于扩展的成员。扩展成员也可以基础类型,也可以是一个结构体类型。
|
|
||||||
|
|
||||||
为了简单,我们假设采用标准库中的StringValue作为每个扩展成员的类型:
|
|
||||||
|
|
||||||
```protobuf
|
|
||||||
import "google/protobuf/wrappers.proto";
|
|
||||||
|
|
||||||
message StringValue {
|
|
||||||
// The string value.
|
|
||||||
string value = 1;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
我们先看看如何定义文件的扩展类型:
|
|
||||||
|
|
||||||
```protobuf
|
|
||||||
import "google/protobuf/descriptor.proto";
|
|
||||||
|
|
||||||
extend google.protobuf.FileOptions {
|
|
||||||
optional google.protobuf.StringValue file_option = 50000;
|
|
||||||
}
|
|
||||||
|
|
||||||
option (file_option) = {
|
|
||||||
value: "this is a file option"
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
然后是message和message成员的扩展方式:
|
|
||||||
|
|
||||||
```protobuf
|
|
||||||
import "google/protobuf/descriptor.proto";
|
|
||||||
|
|
||||||
extend google.protobuf.MessageOptions {
|
|
||||||
optional google.protobuf.StringValue message_option = 50000;
|
|
||||||
}
|
|
||||||
extend google.protobuf.FieldOptions {
|
|
||||||
optional google.protobuf.StringValue filed_option = 50000;
|
|
||||||
}
|
|
||||||
|
|
||||||
message Message {
|
|
||||||
option (message_option) = {
|
|
||||||
value: "message option"
|
|
||||||
};
|
|
||||||
|
|
||||||
string name = 1 [
|
|
||||||
(filed_option) = {
|
|
||||||
value: ""
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
最后是service和service方法的扩展:
|
|
||||||
|
|
||||||
```protobuf
|
|
||||||
import "google/protobuf/descriptor.proto";
|
|
||||||
|
|
||||||
extend google.protobuf.ServiceOptions {
|
|
||||||
optional String service_option = 50000;
|
|
||||||
}
|
|
||||||
extend google.protobuf.MethodOptions {
|
|
||||||
optional String method_option = 50000;
|
|
||||||
}
|
|
||||||
|
|
||||||
service HelloService {
|
|
||||||
option (service_option) = {
|
|
||||||
value: "message option"
|
|
||||||
};
|
|
||||||
|
|
||||||
rpc Hello(String) returns(String) {
|
|
||||||
option (method_option) = {
|
|
||||||
value: ""
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
如果是通用的扩展类型,我们可以将扩展相关的内容放到一个独立的proto文件中。以后知道导入定义了扩展的proto文件,就可以直接使用定义的扩展定义元数据了。
|
|
||||||
|
|
||||||
## 插件中读取扩展信息
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Rest模板框架
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
<!--
|
|
||||||
|
|
||||||
基于pb扩展,打造一个自定义的rest生成
|
|
||||||
|
|
||||||
支持 url 和 url.Values
|
|
||||||
|
|
||||||
通过 grpc-gateway/runtime.PopulateFieldFromPath 和 PopulateQueryParameters 天才 protoMsg 成员
|
|
||||||
|
|
||||||
路由通过 httprouter 处理
|
|
||||||
|
|
||||||
- https://github.com/julienschmidt/httprouter
|
|
||||||
- https://github.com/grpc-ecosystem/grpc-gateway/blob/master/runtime/query.go#L20
|
|
||||||
|
|
||||||
先生成 net/rpc 接口,然后同时增加 Rest 接口
|
|
||||||
|
|
||||||
扩展的元信息需要一个独立的文件,因为在插件中需要访问。
|
|
||||||
|
|
||||||
可以新开一个github项目,便于引用
|
|
||||||
|
|
||||||
-->
|
|
223
ch4-rpc/ch4-07-pbgo.md
Normal file
223
ch4-rpc/ch4-07-pbgo.md
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
# 4.7. pbgo: 基于Protobuf的框架
|
||||||
|
|
||||||
|
pbgo是我们专门针对本节内容设计的较为完整的迷你框架,它基于Protobuf的扩展语法,通过插件自动生成rpc和rest相关代码。在本章第二节我们已经展示过如何定制一个Protobuf代码生成插件,并生成了rpc部分的代码。在本节我们将重点讲述pbgo中和Protobuf扩展语法相关的rest部分的工作原理。
|
||||||
|
|
||||||
|
## Protobuf扩展语法
|
||||||
|
|
||||||
|
目前Protobuf相关的很多开源项目都使用到了Protobuf的扩展语法。在前一节中提到的验证器就是通过给结构体成员增加扩展元信息实现验证。在grpc-gateway项目中,则是通过为服务的每个方法增加Http相关的映射规则实现对Rest接口的支持。pbgo也是通过Protobuf的扩展语法来为rest接口增加元信息。
|
||||||
|
|
||||||
|
pbgo的扩展语法在`github.com/chai2010/pbgo/pbgo.proto`文件定义:
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
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文件导入。Protobuf本身自有一套完整的包体系,在这里包的路径就是pbgo。Go语言也有自己的一套包体系,我们需要通过go_package的扩展语法定义Protobuf和Go语言之间包的映射关系。定义Protobuf和Go语言之间包的映射关系之后,其他导入pbgo.ptoto包的Protobuf文件在生成Go语言时,会生成pbgo.proto映射的Go语言包路径。
|
||||||
|
|
||||||
|
Protobuf扩展语法有五种类型,分别是针对文件的扩展信息、针对message的扩展信息、正对message成员的扩展信息、针对service的扩展信息和针对service方法的扩展信息。在使用扩展前首先需要通过extend关键字定义扩展的类型和可以用于扩展的成员。扩展成员也可以基础类型,也可以是一个结构体类型。pbgo中只定义了service的方法的扩展,只定义了一个名为rest_api的扩展成员,类型是HttpRule结构体。
|
||||||
|
|
||||||
|
定义好扩展之后,我们就可以从其他的Protobuf文件中使用pbgo的扩展。创建一个hello.proto文件:
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
首先我们通过导入`github.com/chai2010/pbgo/pbgo.proto`文件引入扩展定义,然后在HelloService的Hello方法中使用了pbgo定义的扩展。Hello方法扩展的信息表示该方法对应一个REST接口,只有一个GET方法对应"/hello/:value"路径。在REST方法的路径中采用了httprouter路由包的语法规则,":value"表示路径中的该字段对应的是参数中同名的成员。
|
||||||
|
|
||||||
|
## 插件中读取扩展信息
|
||||||
|
|
||||||
|
在本章的第二节我们已经简单讲述过Protobuf插件的工作原理,并且展示了如何生成RPC必要的代码。插件是一个generator.Plugin接口:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// A Plugin provides functionality to add to the output during Go code generation,
|
||||||
|
// such as to produce RPC stubs.
|
||||||
|
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函数中分别生成相关的代码。而Protobuf文件的全部信息都在*generator.FileDescriptor类型函数参数中描述,因此我们需要从函数参数中提前扩展定义的元数据。
|
||||||
|
|
||||||
|
pbgo框架中的插件对象是pbgoPlugin,在Generate方法中首先需要遍历Protobuf文件中定义的全部服务对应,然后再遍历每个服务对的的每个方法。在得到方法结构之后再通过自定义的getServiceMethodOption方法提取rest扩展信息:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (p *pbgoPlugin) Generate(file *generator.FileDescriptor) {
|
||||||
|
for _, svc := range file.Service {
|
||||||
|
for _, m := range svc.Method {
|
||||||
|
httpRule := p.getServiceMethodOption(m)
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在讲述getServiceMethodOption方法之前我们先回顾下方法扩展的定义:
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
extend google.protobuf.MethodOptions {
|
||||||
|
HttpRule rest_api = 20180715;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
pbgo为服务的方法定义了一个rest_api名字的扩展,在最终生成的Go语言代码中会包含一个pbgo.E_RestApi全局变量,通过该全局变量可以获取用户定义的扩展信息。
|
||||||
|
|
||||||
|
下面是getServiceMethodOption方法的实现:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (p *pbgoPlugin) getServiceMethodOption(m *descriptor.MethodDescriptorProto) *pbgo.HttpRule {
|
||||||
|
if m.Options != nil && proto.HasExtension(m.Options, pbgo.E_RestApi) {
|
||||||
|
if ext, _ := proto.GetExtension(m.Options, pbgo.E_RestApi); ext != nil {
|
||||||
|
if x, _ := ext.(*pbgo.HttpRule); x != nil {
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
首先通过proto.HasExtension函数判断每个方法是否定义了扩展,然后通过proto.GetExtension函数获取用户定义的扩展信息。在获取到扩展信息之后,我们再将扩展转型为pbgo.HttpRule类型。
|
||||||
|
|
||||||
|
有了扩展信息之后,我们就可以参考第二节中生成RPC代码的方式生成REST相关的代码。
|
||||||
|
|
||||||
|
## 生成REST代码
|
||||||
|
|
||||||
|
pbgo框架同时也提供了一个插件用于生成REST代码。不过我们的目的是学习pbgo框架的设计过程,因此我们先尝试手写Hello方法对应的REST代码,然后插件再根据手写的代码构造模板自动生成代码。
|
||||||
|
|
||||||
|
HelloService只有一个Hello方法,Hello方法只定义了一个GET方式的REST接口:
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
message String {
|
||||||
|
string value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
service HelloService {
|
||||||
|
rpc Hello (String) returns (String) {
|
||||||
|
option (pbgo.rest_api) = {
|
||||||
|
get: "/hello/:value"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
为了方便最终的用户,我们需要为HelloService构造一个路由。因此我们希望有个一个类似HelloServiceHandler的函数,可以基于HelloServiceInterface服务的接口生成一个路由处理器:
|
||||||
|
|
||||||
|
```go
|
||||||
|
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函数用于将Hello方法注册到路由处理器:
|
||||||
|
|
||||||
|
```go
|
||||||
|
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方法注册路由函数。在路由函数内部首先通过`ps.ByName("value")`从URL中加载value参数,然后通过pbgo.PopulateFieldFromPath辅助函数设置value参数对应的成员。当输入参数准备就绪之后就可以调用HelloService服务的Hello方法,最终将Hello方法返回的结果用json编码返回。
|
||||||
|
|
||||||
|
在手工构造完成最终代码的结构之后,就可以在此基础上构造插件生成代码的模板。完整的插件代码和模板在`protoc-gen-pbgo/pbgo.go`文件,读者可以自行参考。
|
||||||
|
|
||||||
|
## 启动REST服务
|
||||||
|
|
||||||
|
虽然从头构造pbgo框架的过程比较繁琐,但是使用pbgo构造REST服务缺是异常简单。首先要构造一个满足HelloServiceInterface接口的服务对象:
|
||||||
|
|
||||||
|
```go
|
||||||
|
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方法中简单返回结果。然后调用该服务对应的HelloServiceHandler函数生成路由处理器,并启动服务:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
router := hello_pb.HelloServiceHandler(new(HelloService))
|
||||||
|
log.Fatal(http.ListenAndServe(":8080", router))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在命令行测试REST服务:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ curl localhost:8080/hello/vgo
|
||||||
|
```
|
||||||
|
|
||||||
|
这样一个超级简单的pbgo框架就完成了!
|
@ -1,4 +1,4 @@
|
|||||||
# 4.7. 补充说明
|
# 4.8. 补充说明
|
||||||
|
|
||||||
本章重点讲述了Go标准库的RPC和基于Protobuf衍生的GRPC框架,同时也简单展示了如何自己定制一个RPC框架。之所以聚焦在这几个有限的主题,是因为这几个技术都是Go语言团队官方在进行维护,和Go语言契合也最为默契。不过RPC依然是一个庞大的主题,足以单独成书。目前开源世界也有很多富有特色的RPC框架,还有针对分布式系统进行深度定制的RPC系统,用户可以根据自己实际需求选择合适的工具。
|
本章重点讲述了Go标准库的RPC和基于Protobuf衍生的GRPC框架,同时也简单展示了如何自己定制一个RPC框架。之所以聚焦在这几个有限的主题,是因为这几个技术都是Go语言团队官方在进行维护,和Go语言契合也最为默契。不过RPC依然是一个庞大的主题,足以单独成书。目前开源世界也有很多富有特色的RPC框架,还有针对分布式系统进行深度定制的RPC系统,用户可以根据自己实际需求选择合适的工具。
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user