mirror of
https://github.com/chai2010/advanced-go-programming-book.git
synced 2025-05-24 12:32:21 +00:00
ch4-02: done
This commit is contained in:
parent
312d84f4ca
commit
9b1941f961
@ -33,7 +33,7 @@
|
|||||||
* [3.8. 补充说明](ch3-asm/ch3-08-faq.md)
|
* [3.8. 补充说明](ch3-asm/ch3-08-faq.md)
|
||||||
* [第四章 RPC和Protobuf](ch4-rpc/readme.md)
|
* [第四章 RPC和Protobuf](ch4-rpc/readme.md)
|
||||||
* [4.1. RPC入门](ch4-rpc/ch4-01-rpc-intro.md)
|
* [4.1. RPC入门](ch4-rpc/ch4-01-rpc-intro.md)
|
||||||
* [4.2. Protobuf简介(TODO)](ch4-rpc/ch4-02-pb-intro.md)
|
* [4.2. Protobuf](ch4-rpc/ch4-02-pb-intro.md)
|
||||||
* [4.3. protorpc(TODO)](ch4-rpc/ch4-03-protorpc.md)
|
* [4.3. protorpc(TODO)](ch4-rpc/ch4-03-protorpc.md)
|
||||||
* [4.4. grpc(TODO)](ch4-rpc/ch4-04-grpc.md)
|
* [4.4. grpc(TODO)](ch4-rpc/ch4-04-grpc.md)
|
||||||
* [4.5. 反向rpc(TODO)](ch4-rpc/ch4-05-reverse-rpc.md)
|
* [4.5. 反向rpc(TODO)](ch4-rpc/ch4-05-reverse-rpc.md)
|
||||||
|
@ -1,19 +1,430 @@
|
|||||||
# 4.2. Protobuf简介
|
# 4.2. Protobuf
|
||||||
|
|
||||||
TODO
|
Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,并于2008年对外开源。Protobuf刚开源时的定位类似于XML、JSON等数据描述语言,通过附带工具生成等代码提实现将结构化数据序列化的功能。但是我们更关注的是Protobuf作为接口规范的描述语言,可以作为设计安全的跨语言PRC接口的基础工具。
|
||||||
|
|
||||||
<!--
|
## Protobuf入门
|
||||||
|
|
||||||
pb基本用法,pb其实就是json,我们不关心底层的编码和性能
|
对于没有用过Protobuf读者,建议先从官网了解下基本用法。这里我们尝试如何将Protobuf和RPC结合在一起使用,通过Protobuf来最终保证RPC的接口规范和完全。Protobuf中最基本的数据单元是message,是类似Go语言中结构体的存在。在message中可以嵌套message或其它的基础数据类型的成员。
|
||||||
|
|
||||||
pb2和pb3区别
|
首先创建hello.proto文件,其中包装HelloService服务中用到的字符串类型:
|
||||||
|
|
||||||
pb和json的区别,pb有些限制,比如 map 只能嵌套,field名字不能随机变化
|
```protobuf
|
||||||
嵌套时,只能时msg或map,到一些限制
|
syntax = "proto3";
|
||||||
|
|
||||||
rpc 语法
|
package main;
|
||||||
|
|
||||||
扩展语法
|
message String {
|
||||||
|
string value = 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
开头的syntax语句表示采用Protobuf第三版本的语法。第三版的Protobuf对语言进行的提炼简化,所有成员均采用类似Go语言中的零值初始化(不在支持自定义默认值),同时消息成员也不再支持required特性。然后package指令指明当前是main包(这样可以和Go的包明保持一致),当然用户也可以针对不同的语言定制对应的包路径和名称。最后message关键字定义一个新的String类型,在最终生成的Go语言代码中对应一个String结构体。String类型中只有一个字符串类型的value成员,该成员的Protobuf编码时的成员编号为1。
|
||||||
|
|
||||||
|
在XML或JSON成数据描述语言中,一遍通过成员的名字来绑定对应的数据。但是Protobuf编码却是通过成员的唯一编号来绑定对应的数据,因此Protobuf编码后数据的体积会比较小,但是也非常不便于人类查阅。我们目前并不关注Protobuf的编码技术,最终生成的Go结构体可以自由采用JSON或gob等编码格式,因此大家可以暂时忽略Protobuf的成员编号部分。
|
||||||
|
|
||||||
|
Protobuf核心的工具集是C++语言开发的,在官方的protoc编译器中并不支持Go语言。要想基于上面的hello.proto文件生成相应的Go代码,需要安装相应的工具。首先是安装官方的protoc工具,可以从 https://github.com/google/protobuf/releases 下载。然后是安装针对Go语言的代码生成插件,可以通过`go get github.com/golang/protobuf/protoc-gen-go`命令按安装。
|
||||||
|
|
||||||
|
然后通过以下命令生成相应的Go代码:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ protoc --go_out=. hello.proto
|
||||||
|
```
|
||||||
|
|
||||||
|
其中`go_out`参数告知protoc编译器取加载对应的protoc-gen-go工具,然后通过该工具生成代码,生成代码放到当前目录。最后是一系列要处理的protobuf文件的列表。
|
||||||
|
|
||||||
|
这里只生成了一个hello.pb.go文件,其中String结构体内容如下:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type String struct {
|
||||||
|
Value string `protobuf:"bytes,1,opt,name=value" json:"value,omitempty"`
|
||||||
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *String) Reset() { *m = String{} }
|
||||||
|
func (m *String) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*String) ProtoMessage() {}
|
||||||
|
func (*String) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_hello_069698f99dd8f029, []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *String) GetValue() string {
|
||||||
|
if m != nil {
|
||||||
|
return m.Value
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
生成的结构体中有一些以`XXX_`为前缀名字的成员,目前可以忽略这些成员。同时String类型还自动生成了一组方法,其中ProtoMessage方法表示这是一个实现了proto.Message接口的方法。此外Protobuf还为每个成员生成了一个Get方法,Get方法不仅可以处理空指针类型,而且可以和Protobuf第三版的方法保持一致(第二版的自定义默认值特性依赖这类方法)。
|
||||||
|
|
||||||
|
基于新的String类型,我们可以重新实现HelloService:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type HelloService struct{}
|
||||||
|
|
||||||
|
func (p *HelloService) Hello(request String, reply *String) error {
|
||||||
|
reply.Value = "hello:" + request.GetValue()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
其中Hello方法的输入参数和返回的参数均该用Protobuf定义的String类型表示,函数的内部代码同时也做了相应的调整。
|
||||||
|
|
||||||
|
至此,我们初步实现了Protobuf和RPC组合工作。在启动RPC服务时,我们依然可以选择默认的gob或手工指定json编码,甚至可以重新基于protobuf编码实现一个插件。虽然做了这么多工作,但是似乎并没有看到什么收益!
|
||||||
|
|
||||||
|
回顾第一章中更安全的PRC接口部分的内容,当时我们花费了极大的力气去给RPC服务增加安全的保障。最终得到的更安全的PRC接口的代码本书就非常繁琐比利于手工维护,同时全部安全相关的代码只适用于Go语言环境!既然使用了Protobuf定义的输入和输出参数,那么RPC服务接口是否也可以通过Protobuf定义呢?其实用Protobuf定义语言无关的PRC服务接口才是它真正的价值所在!
|
||||||
|
|
||||||
|
下面更新hello.proto文件,通过Protobuf来定义HelloService服务:
|
||||||
|
|
||||||
|
```protobuf
|
||||||
|
service HelloService {
|
||||||
|
rpc Hello (String) returns (String);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
但是重新生成的Go代码并没有发生变化。这是因为世界上的RPC实现有千万种,protoc编译器并不知道改如何为HelloService服务生成代码。
|
||||||
|
|
||||||
|
不过在protoc-gen-go内部已经集成了一个叫grpc的插件,可以针对grpc生成代码:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ protoc --go_out=plugins=grpc:. hello.proto
|
||||||
|
```
|
||||||
|
|
||||||
|
在生成的代码中多了一些类似HelloServiceServer、HelloServiceClient的新类型。这些类似是为grpc服务的,并不符合我们的RPC要求。
|
||||||
|
|
||||||
|
grpc插件为我们提供了改进思路,下面我们将探索如何为我们的RPC生成安全的代码。
|
||||||
|
|
||||||
|
|
||||||
|
## 定制代码生成插件
|
||||||
|
|
||||||
|
Protobuf的protoc编译器是通过插件机制实现对不同语言的支持。比如protoc命令出现`--xxx_out`格式的参数,那么protoc将首先查询是否有内置的xxx插件,如果没有内置的xxx插件那么将继续查询当前系统中是否存在protoc-gen-xxx命名的可执行程序,最终通过查询到的插件生成代码。对于Go语言的protoc-gen-go插件来说,里面又实现了一层静态插件系统。比如protoc-gen-go内置了一个grpc插件,用户可以通过`--go_out=plugins=grpc`参数来生成grpc相关代码,否则只会针对message生成相关代码。
|
||||||
|
|
||||||
|
参考grpc插件的代码,可以发现generator.RegisterPlugin函数可以用来注册插件。插件是一个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)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
其中Name方法返回插件的名字,这是Go语言的Protobuf实现的插件体系,和protoc插件的名字并无关系。然后Init函数是通过g参数对插件进行初始化,g参数中包含Proto文件的所有信息。最后的Generate和GenerateImports方法用于生成主体代码和对应的导入包代码。
|
||||||
|
|
||||||
|
因此我们可以设计一个netrpcPlugin插件,用于为标准库的RPC框架生成代码:
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"github.com/golang/protobuf/protoc-gen-go/generator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type netrpcPlugin struct{ *generator.Generator }
|
||||||
|
|
||||||
|
func (p *netrpcPlugin) Name() string { return "netrpc" }
|
||||||
|
func (p *netrpcPlugin) Init(g *generator.Generator) { p.Generator = g }
|
||||||
|
|
||||||
|
func (p *netrpcPlugin) GenerateImports(file *generator.FileDescriptor) {
|
||||||
|
if len(file.Service) > 0 {
|
||||||
|
p.P("// TODO: import code")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *netrpcPlugin) Generate(file *generator.FileDescriptor) {
|
||||||
|
for _, svc := range file.Service {
|
||||||
|
p.P("// TODO: service code, Name = " + svc.GetName())
|
||||||
|
_ = svc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
首先Name方法返回插件的名字。netrpcPlugin插件内置了一个匿名的`*generator.Generator`成员,然后在Init初始化的时候用参数g进行初始化,因此插件是从g参数对象继承了全部的公有方法。在GenerateImports方法中,当判断表示服务数列的`file.Service`切片非空时输出一个注释信息。在Generate方法也是才有类似的测试,但是遍历每个服务输出一个注释,并且输出服务的名字。至此一个最简陋的自定义的protoc-gen-go静态插件已经成型了。
|
||||||
|
|
||||||
|
要使用该插件需要先通过generator.RegisterPlugin函数注册插件,可以在init函数完成:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func init() {
|
||||||
|
generator.RegisterPlugin(new(netrpcPlugin))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
因为Go语言的包只能静态导入,我们无法向已经安装的protoc-gen-go添加我们新编写的插件。我们可以完整克隆protoc-gen-go对应main函数:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// copy from https://github.com/golang/protobuf/blob/master/protoc-gen-go/main.go
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/golang/protobuf/proto"
|
||||||
|
"github.com/golang/protobuf/protoc-gen-go/generator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Begin by allocating a generator. The request and response structures are stored there
|
||||||
|
// so we can do error handling easily - the response structure contains the field to
|
||||||
|
// report failure.
|
||||||
|
g := generator.New()
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
g.Error(err, "reading input")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := proto.Unmarshal(data, g.Request); err != nil {
|
||||||
|
g.Error(err, "parsing input proto")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(g.Request.FileToGenerate) == 0 {
|
||||||
|
g.Fail("no files to generate")
|
||||||
|
}
|
||||||
|
|
||||||
|
g.CommandLineParameters(g.Request.GetParameter())
|
||||||
|
|
||||||
|
// Create a wrapped version of the Descriptors and EnumDescriptors that
|
||||||
|
// point to the file that defines them.
|
||||||
|
g.WrapTypes()
|
||||||
|
|
||||||
|
g.SetPackageNames()
|
||||||
|
g.BuildTypeNameMap()
|
||||||
|
|
||||||
|
g.GenerateAllFiles()
|
||||||
|
|
||||||
|
// Send back the results.
|
||||||
|
data, err = proto.Marshal(g.Response)
|
||||||
|
if err != nil {
|
||||||
|
g.Error(err, "failed to marshal output proto")
|
||||||
|
}
|
||||||
|
_, err = os.Stdout.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
g.Error(err, "failed to write output proto")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
为了避免对protoc-gen-go插件造成干扰,我们将我们的可执行程序命名为protoc-gen-go-netrpc,表示包含了nerpc插件。然后用以下命令重新编译hello.proto文件:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ protoc --go-netrpc_out=plugins=netrpc:. hello.proto
|
||||||
|
```
|
||||||
|
|
||||||
|
其中`--go-netrpc_out`参数高中protoc编译器加载名为protoc-gen-go-netrpc的插件,插件中的`plugins=netrpc`指示启用内部名为netrpc的netrpcPlugin插件。
|
||||||
|
|
||||||
|
在新生成的hello.pb.go文件中将包含以下的代码:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// TODO: import code
|
||||||
|
// TODO: service code, Name = HelloService
|
||||||
|
```
|
||||||
|
|
||||||
|
至此,手工定制的Protobuf代码生成插件终于可以工作了。
|
||||||
|
|
||||||
|
## 自动生成完整的RPC代码
|
||||||
|
|
||||||
|
在前面的例子中我们已经构件了最小化的netrpcPlugin插件,并且通过克隆protoc-gen-go的主程序创建了新的protoc-gen-go-netrpc的插件程序。我们现在开始继续完善netrpcPlugin插件,最终目标是生成RPC安全接口。
|
||||||
|
|
||||||
|
以下是完善后的GenerateImports和Generate方法:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (p *netrpcPlugin) GenerateImports(file *generator.FileDescriptor) {
|
||||||
|
if len(file.Service) > 0 {
|
||||||
|
p.P(`import "net/rpc"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *netrpcPlugin) Generate(file *generator.FileDescriptor) {
|
||||||
|
for _, svc := range file.Service {
|
||||||
|
p.genServiceInterface(file, svc)
|
||||||
|
p.genServiceServer(file, svc)
|
||||||
|
p.genServiceClient(file, svc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
在导入部分我们增加了导入net/rpc包的语句。而在每个服务部分则通过genServiceInterface方法生成服务的接口,通过genServiceServer方法生成服务的注册函数,通过genServiceClient方法生成客户端包装代码。
|
||||||
|
|
||||||
|
首先看看genServiceInterface如何生成服务器接口:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (p *netrpcPlugin) genServiceInterface(
|
||||||
|
file *generator.FileDescriptor,
|
||||||
|
svc *descriptor.ServiceDescriptorProto,
|
||||||
|
) {
|
||||||
|
const serviceInterfaceTmpl = `
|
||||||
|
type {{.ServiceName}}Interface interface {
|
||||||
|
{{.CallMethodList}}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const callMethodTmpl = `
|
||||||
|
{{.MethodName}}(in {{.ArgsType}}, out *{{.ReplyType}}) error`
|
||||||
|
|
||||||
|
// gen call method list
|
||||||
|
var callMethodList string
|
||||||
|
for _, m := range svc.Method {
|
||||||
|
out := bytes.NewBuffer([]byte{})
|
||||||
|
t := template.Must(template.New("").Parse(callMethodTmpl))
|
||||||
|
t.Execute(out, &struct{ ServiceName, MethodName, ArgsType, ReplyType string }{
|
||||||
|
ServiceName: generator.CamelCase(svc.GetName()),
|
||||||
|
MethodName: generator.CamelCase(m.GetName()),
|
||||||
|
ArgsType: p.TypeName(p.ObjectNamed(m.GetInputType())),
|
||||||
|
ReplyType: p.TypeName(p.ObjectNamed(m.GetOutputType())),
|
||||||
|
})
|
||||||
|
callMethodList += out.String()
|
||||||
|
|
||||||
|
p.RecordTypeUse(m.GetInputType())
|
||||||
|
p.RecordTypeUse(m.GetOutputType())
|
||||||
|
}
|
||||||
|
|
||||||
|
// gen all interface code
|
||||||
|
out := bytes.NewBuffer([]byte{})
|
||||||
|
t := template.Must(template.New("").Parse(serviceInterfaceTmpl))
|
||||||
|
t.Execute(out, &struct{ ServiceName, CallMethodList string }{
|
||||||
|
ServiceName: generator.CamelCase(svc.GetName()),
|
||||||
|
CallMethodList: callMethodList,
|
||||||
|
})
|
||||||
|
p.P(out.String())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
生成服务接口时首先需要服务的名字,可以通过`svc.GetName()`获取服务在Proto文件中的名字,然后通过generator.CamelCase函数转为Go语言中修饰后的名字。
|
||||||
|
|
||||||
|
服务中svc.Method是一个表示方法信息的切片。要生成每个方法,需要知道每个方法的名字、输入参数类型、输出参数类型。其中m.GetName()是获取原始的方法名字,同样需要通过generator.CamelCase转化为Go语言中修饰后的名字。而`p.TypeName(p.ObjectNamed(m.GetInputType()))`和`p.TypeName(p.ObjectNamed(m.GetOutputType()))`分别用户获取输入参数和输出参数的类型名字。
|
||||||
|
|
||||||
|
然后是生成RPC注册方法的genServiceServer函数:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (p *netrpcPlugin) genServiceServer(
|
||||||
|
file *generator.FileDescriptor,
|
||||||
|
svc *descriptor.ServiceDescriptorProto,
|
||||||
|
) {
|
||||||
|
const serviceHelperFunTmpl = `
|
||||||
|
func Register{{.ServiceName}}(srv *rpc.Server, x {{.ServiceName}}) error {
|
||||||
|
if err := srv.RegisterName("{{.ServiceName}}", x); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
`
|
||||||
|
out := bytes.NewBuffer([]byte{})
|
||||||
|
t := template.Must(template.New("").Parse(serviceHelperFunTmpl))
|
||||||
|
t.Execute(out, &struct{ PackageName, ServiceName, ServiceRegisterName string }{
|
||||||
|
PackageName: file.GetPackage(),
|
||||||
|
ServiceName: generator.CamelCase(svc.GetName()),
|
||||||
|
})
|
||||||
|
p.P(out.String())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
genServiceServer函数的实现和生成接口的代码类似,依然是才有Go语言的模板生成目标代码。
|
||||||
|
|
||||||
|
最后是genServiceClient函数生成客户端包装代码:
|
||||||
|
|
||||||
|
```go
|
||||||
|
|
||||||
|
func (p *netrpcPlugin) genServiceClient(
|
||||||
|
file *generator.FileDescriptor,
|
||||||
|
svc *descriptor.ServiceDescriptorProto,
|
||||||
|
) {
|
||||||
|
const clientHelperFuncTmpl = `
|
||||||
|
type {{.ServiceName}}Client struct {
|
||||||
|
*rpc.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ {{.ServiceName}}Interface = (*{{.ServiceName}}Client)(nil)
|
||||||
|
|
||||||
|
func Dial{{.ServiceName}}(network, address string) (*{{.ServiceName}}Client, error) {
|
||||||
|
c, err := rpc.Dial(network, address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &{{.ServiceName}}Client{Client: c}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
{{.MethodList}}
|
||||||
|
`
|
||||||
|
const clientMethodTmpl = `
|
||||||
|
func (p *{{.ServiceName}}Client) {{.MethodName}}(in {{.ArgsType}}, out *{{.ReplyType}}) error {
|
||||||
|
return p.Client.Call("{{.ServiceName}}.{{.MethodName}}", in, out)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// gen client method list
|
||||||
|
var methodList string
|
||||||
|
for _, m := range svc.Method {
|
||||||
|
out := bytes.NewBuffer([]byte{})
|
||||||
|
t := template.Must(template.New("").Parse(clientMethodTmpl))
|
||||||
|
t.Execute(out, &struct{ ServiceName, ServiceRegisterName, MethodName, ArgsType, ReplyType string }{
|
||||||
|
ServiceName: generator.CamelCase(svc.GetName()),
|
||||||
|
ServiceRegisterName: file.GetPackage() + "." + generator.CamelCase(svc.GetName()),
|
||||||
|
MethodName: generator.CamelCase(m.GetName()),
|
||||||
|
ArgsType: p.TypeName(p.ObjectNamed(m.GetInputType())),
|
||||||
|
ReplyType: p.TypeName(p.ObjectNamed(m.GetOutputType())),
|
||||||
|
})
|
||||||
|
methodList += out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// gen all client code
|
||||||
|
out := bytes.NewBuffer([]byte{})
|
||||||
|
t := template.Must(template.New("").Parse(clientHelperFuncTmpl))
|
||||||
|
t.Execute(out, &struct{ PackageName, ServiceName, MethodList string }{
|
||||||
|
PackageName: file.GetPackage(),
|
||||||
|
ServiceName: generator.CamelCase(svc.GetName()),
|
||||||
|
MethodList: methodList,
|
||||||
|
})
|
||||||
|
p.P(out.String())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
除了模板不同,客户端的生成代码逻辑服务接口的生成函数也是类似的。
|
||||||
|
|
||||||
|
最后我们可以查看下netrpcPlugin插件生成的RPC代码:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type HelloServiceInterface interface {
|
||||||
|
Hello(in String, out *String) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterHelloService(srv *rpc.Server, x HelloService) error {
|
||||||
|
if err := srv.RegisterName("HelloService", x); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type HelloServiceClient struct {
|
||||||
|
*rpc.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ HelloServiceInterface = (*HelloServiceClient)(nil)
|
||||||
|
|
||||||
|
func DialHelloService(network, address string) (*HelloServiceClient, error) {
|
||||||
|
c, err := rpc.Dial(network, address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &HelloServiceClient{Client: c}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HelloServiceClient) Hello(in String, out *String) error {
|
||||||
|
return p.Client.Call("HelloService.Hello", in, out)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
当Protobuf的插件定制工作完成后,每次hello.proto文件中RPC服务的变化都可以自动生成代码。同时,才有类似的技术也可以为其它语言编写代码生成插件。
|
||||||
|
|
||||||
|
在掌握了定制Protobuf插件技术后,你将彻底拥有这个技术。
|
||||||
|
|
||||||
包体系结构
|
|
||||||
-->
|
|
||||||
|
0
examples/ch4-02-proto/proto-v1/Makefile
Normal file
0
examples/ch4-02-proto/proto-v1/Makefile
Normal file
105
examples/ch4-02-proto/proto-v1/hello.pb.go
Normal file
105
examples/ch4-02-proto/proto-v1/hello.pb.go
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// source: hello.proto
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import proto "github.com/golang/protobuf/proto"
|
||||||
|
import fmt "fmt"
|
||||||
|
import math "math"
|
||||||
|
|
||||||
|
import "net/rpc"
|
||||||
|
|
||||||
|
// Reference imports to suppress errors if they are not otherwise used.
|
||||||
|
var _ = proto.Marshal
|
||||||
|
var _ = fmt.Errorf
|
||||||
|
var _ = math.Inf
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the proto package it is being compiled against.
|
||||||
|
// A compilation error at this line likely means your copy of the
|
||||||
|
// proto package needs to be updated.
|
||||||
|
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
|
||||||
|
|
||||||
|
type String struct {
|
||||||
|
Value string `protobuf:"bytes,1,opt,name=value" json:"value,omitempty"`
|
||||||
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *String) Reset() { *m = String{} }
|
||||||
|
func (m *String) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*String) ProtoMessage() {}
|
||||||
|
func (*String) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_hello_5dd9d59ecabc789f, []int{0}
|
||||||
|
}
|
||||||
|
func (m *String) XXX_Unmarshal(b []byte) error {
|
||||||
|
return xxx_messageInfo_String.Unmarshal(m, b)
|
||||||
|
}
|
||||||
|
func (m *String) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||||
|
return xxx_messageInfo_String.Marshal(b, m, deterministic)
|
||||||
|
}
|
||||||
|
func (dst *String) XXX_Merge(src proto.Message) {
|
||||||
|
xxx_messageInfo_String.Merge(dst, src)
|
||||||
|
}
|
||||||
|
func (m *String) XXX_Size() int {
|
||||||
|
return xxx_messageInfo_String.Size(m)
|
||||||
|
}
|
||||||
|
func (m *String) XXX_DiscardUnknown() {
|
||||||
|
xxx_messageInfo_String.DiscardUnknown(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var xxx_messageInfo_String proto.InternalMessageInfo
|
||||||
|
|
||||||
|
func (m *String) GetValue() string {
|
||||||
|
if m != nil {
|
||||||
|
return m.Value
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
proto.RegisterType((*String)(nil), "main.String")
|
||||||
|
}
|
||||||
|
|
||||||
|
type HelloServiceInterface interface {
|
||||||
|
Hello(in String, out *String) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterHelloService(srv *rpc.Server, x HelloService) error {
|
||||||
|
if err := srv.RegisterName("HelloService", x); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type HelloServiceClient struct {
|
||||||
|
*rpc.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ HelloServiceInterface = (*HelloServiceClient)(nil)
|
||||||
|
|
||||||
|
func DialHelloService(network, address string) (*HelloServiceClient, error) {
|
||||||
|
c, err := rpc.Dial(network, address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &HelloServiceClient{Client: c}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *HelloServiceClient) Hello(in String, out *String) error {
|
||||||
|
return p.Client.Call("HelloService.Hello", in, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { proto.RegisterFile("hello.proto", fileDescriptor_hello_5dd9d59ecabc789f) }
|
||||||
|
|
||||||
|
var fileDescriptor_hello_5dd9d59ecabc789f = []byte{
|
||||||
|
// 107 bytes of a gzipped FileDescriptorProto
|
||||||
|
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0xce, 0x48, 0xcd, 0xc9,
|
||||||
|
0xc9, 0xd7, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0xc9, 0x4d, 0xcc, 0xcc, 0x53, 0x92, 0xe3,
|
||||||
|
0x62, 0x0b, 0x2e, 0x29, 0xca, 0xcc, 0x4b, 0x17, 0x12, 0xe1, 0x62, 0x2d, 0x4b, 0xcc, 0x29, 0x4d,
|
||||||
|
0x95, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x0c, 0x82, 0x70, 0x8c, 0x8c, 0xb9, 0x78, 0x3c, 0x40, 0x9a,
|
||||||
|
0x82, 0x53, 0x8b, 0xca, 0x32, 0x93, 0x53, 0x85, 0x94, 0xb9, 0x58, 0xc1, 0x7c, 0x21, 0x1e, 0x3d,
|
||||||
|
0x90, 0x7e, 0x3d, 0x88, 0x66, 0x29, 0x14, 0x5e, 0x12, 0x1b, 0xd8, 0x06, 0x63, 0x40, 0x00, 0x00,
|
||||||
|
0x00, 0xff, 0xff, 0xa1, 0x2a, 0xa2, 0x5a, 0x70, 0x00, 0x00, 0x00,
|
||||||
|
}
|
11
examples/ch4-02-proto/proto-v1/hello.proto
Normal file
11
examples/ch4-02-proto/proto-v1/hello.proto
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package main;
|
||||||
|
|
||||||
|
message String {
|
||||||
|
string value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
service HelloService {
|
||||||
|
rpc Hello (String) returns (String);
|
||||||
|
}
|
12
examples/ch4-02-proto/proto-v1/main.go
Normal file
12
examples/ch4-02-proto/proto-v1/main.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
type HelloService struct{}
|
||||||
|
|
||||||
|
func (p *HelloService) Hello(request String, reply *String) error {
|
||||||
|
reply.Value = "hello:" + request.GetValue()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
|
||||||
|
}
|
98
examples/ch4-02-proto/protoc-gen-go-netrpc/main.go
Normal file
98
examples/ch4-02-proto/protoc-gen-go-netrpc/main.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
// Go support for Protocol Buffers - Google's data interchange format
|
||||||
|
//
|
||||||
|
// Copyright 2010 The Go Authors. All rights reserved.
|
||||||
|
// https://github.com/golang/protobuf
|
||||||
|
//
|
||||||
|
// Redistribution and use in source and binary forms, with or without
|
||||||
|
// modification, are permitted provided that the following conditions are
|
||||||
|
// met:
|
||||||
|
//
|
||||||
|
// * Redistributions of source code must retain the above copyright
|
||||||
|
// notice, this list of conditions and the following disclaimer.
|
||||||
|
// * Redistributions in binary form must reproduce the above
|
||||||
|
// copyright notice, this list of conditions and the following disclaimer
|
||||||
|
// in the documentation and/or other materials provided with the
|
||||||
|
// distribution.
|
||||||
|
// * Neither the name of Google Inc. nor the names of its
|
||||||
|
// contributors may be used to endorse or promote products derived from
|
||||||
|
// this software without specific prior written permission.
|
||||||
|
//
|
||||||
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
// protoc-gen-go is a plugin for the Google protocol buffer compiler to generate
|
||||||
|
// Go code. Run it by building this program and putting it in your path with
|
||||||
|
// the name
|
||||||
|
// protoc-gen-go
|
||||||
|
// That word 'go' at the end becomes part of the option string set for the
|
||||||
|
// protocol compiler, so once the protocol compiler (protoc) is installed
|
||||||
|
// you can run
|
||||||
|
// protoc --go_out=output_directory input_directory/file.proto
|
||||||
|
// to generate Go bindings for the protocol defined by file.proto.
|
||||||
|
// With that input, the output will be written to
|
||||||
|
// output_directory/file.pb.go
|
||||||
|
//
|
||||||
|
// The generated code is documented in the package comment for
|
||||||
|
// the library.
|
||||||
|
//
|
||||||
|
// See the README and documentation for protocol buffers to learn more:
|
||||||
|
// https://developers.google.com/protocol-buffers/
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/golang/protobuf/proto"
|
||||||
|
"github.com/golang/protobuf/protoc-gen-go/generator"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Begin by allocating a generator. The request and response structures are stored there
|
||||||
|
// so we can do error handling easily - the response structure contains the field to
|
||||||
|
// report failure.
|
||||||
|
g := generator.New()
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(os.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
g.Error(err, "reading input")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := proto.Unmarshal(data, g.Request); err != nil {
|
||||||
|
g.Error(err, "parsing input proto")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(g.Request.FileToGenerate) == 0 {
|
||||||
|
g.Fail("no files to generate")
|
||||||
|
}
|
||||||
|
|
||||||
|
g.CommandLineParameters(g.Request.GetParameter())
|
||||||
|
|
||||||
|
// Create a wrapped version of the Descriptors and EnumDescriptors that
|
||||||
|
// point to the file that defines them.
|
||||||
|
g.WrapTypes()
|
||||||
|
|
||||||
|
g.SetPackageNames()
|
||||||
|
g.BuildTypeNameMap()
|
||||||
|
|
||||||
|
g.GenerateAllFiles()
|
||||||
|
|
||||||
|
// Send back the results.
|
||||||
|
data, err = proto.Marshal(g.Response)
|
||||||
|
if err != nil {
|
||||||
|
g.Error(err, "failed to marshal output proto")
|
||||||
|
}
|
||||||
|
_, err = os.Stdout.Write(data)
|
||||||
|
if err != nil {
|
||||||
|
g.Error(err, "failed to write output proto")
|
||||||
|
}
|
||||||
|
}
|
155
examples/ch4-02-proto/protoc-gen-go-netrpc/netprpc.go
Normal file
155
examples/ch4-02-proto/protoc-gen-go-netrpc/netprpc.go
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
// Copyright 2013 <chaishushan{AT}gmail.com>. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/golang/protobuf/protoc-gen-go/descriptor"
|
||||||
|
"github.com/golang/protobuf/protoc-gen-go/generator"
|
||||||
|
)
|
||||||
|
|
||||||
|
type netrpcPlugin struct{ *generator.Generator }
|
||||||
|
|
||||||
|
func (p *netrpcPlugin) Name() string { return "netrpc" }
|
||||||
|
func (p *netrpcPlugin) Init(g *generator.Generator) { p.Generator = g }
|
||||||
|
|
||||||
|
func (p *netrpcPlugin) GenerateImports(file *generator.FileDescriptor) {
|
||||||
|
if len(file.Service) > 0 {
|
||||||
|
p.P(`import "net/rpc"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *netrpcPlugin) Generate(file *generator.FileDescriptor) {
|
||||||
|
for _, svc := range file.Service {
|
||||||
|
p.genServiceInterface(file, svc)
|
||||||
|
p.genServiceServer(file, svc)
|
||||||
|
p.genServiceClient(file, svc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *netrpcPlugin) genServiceInterface(
|
||||||
|
file *generator.FileDescriptor,
|
||||||
|
svc *descriptor.ServiceDescriptorProto,
|
||||||
|
) {
|
||||||
|
const serviceInterfaceTmpl = `
|
||||||
|
type {{.ServiceName}}Interface interface {
|
||||||
|
{{.CallMethodList}}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const callMethodTmpl = `
|
||||||
|
{{.MethodName}}(in {{.ArgsType}}, out *{{.ReplyType}}) error`
|
||||||
|
|
||||||
|
// gen call method list
|
||||||
|
var callMethodList string
|
||||||
|
for _, m := range svc.Method {
|
||||||
|
out := bytes.NewBuffer([]byte{})
|
||||||
|
t := template.Must(template.New("").Parse(callMethodTmpl))
|
||||||
|
t.Execute(out, &struct{ ServiceName, MethodName, ArgsType, ReplyType string }{
|
||||||
|
ServiceName: generator.CamelCase(svc.GetName()),
|
||||||
|
MethodName: generator.CamelCase(m.GetName()),
|
||||||
|
ArgsType: p.TypeName(p.ObjectNamed(m.GetInputType())),
|
||||||
|
ReplyType: p.TypeName(p.ObjectNamed(m.GetOutputType())),
|
||||||
|
})
|
||||||
|
callMethodList += out.String()
|
||||||
|
|
||||||
|
p.RecordTypeUse(m.GetInputType())
|
||||||
|
p.RecordTypeUse(m.GetOutputType())
|
||||||
|
}
|
||||||
|
|
||||||
|
// gen all interface code
|
||||||
|
{
|
||||||
|
out := bytes.NewBuffer([]byte{})
|
||||||
|
t := template.Must(template.New("").Parse(serviceInterfaceTmpl))
|
||||||
|
t.Execute(out, &struct{ ServiceName, CallMethodList string }{
|
||||||
|
ServiceName: generator.CamelCase(svc.GetName()),
|
||||||
|
CallMethodList: callMethodList,
|
||||||
|
})
|
||||||
|
p.P(out.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *netrpcPlugin) genServiceServer(
|
||||||
|
file *generator.FileDescriptor,
|
||||||
|
svc *descriptor.ServiceDescriptorProto,
|
||||||
|
) {
|
||||||
|
const serviceHelperFunTmpl = `
|
||||||
|
func Register{{.ServiceName}}(srv *rpc.Server, x {{.ServiceName}}) error {
|
||||||
|
if err := srv.RegisterName("{{.ServiceName}}", x); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
`
|
||||||
|
{
|
||||||
|
out := bytes.NewBuffer([]byte{})
|
||||||
|
t := template.Must(template.New("").Parse(serviceHelperFunTmpl))
|
||||||
|
t.Execute(out, &struct{ PackageName, ServiceName, ServiceRegisterName string }{
|
||||||
|
PackageName: file.GetPackage(),
|
||||||
|
ServiceName: generator.CamelCase(svc.GetName()),
|
||||||
|
})
|
||||||
|
p.P(out.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *netrpcPlugin) genServiceClient(
|
||||||
|
file *generator.FileDescriptor,
|
||||||
|
svc *descriptor.ServiceDescriptorProto,
|
||||||
|
) {
|
||||||
|
const clientHelperFuncTmpl = `
|
||||||
|
type {{.ServiceName}}Client struct {
|
||||||
|
*rpc.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ {{.ServiceName}}Interface = (*{{.ServiceName}}Client)(nil)
|
||||||
|
|
||||||
|
func Dial{{.ServiceName}}(network, address string) (*{{.ServiceName}}Client, error) {
|
||||||
|
c, err := rpc.Dial(network, address)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &{{.ServiceName}}Client{Client: c}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
{{.MethodList}}
|
||||||
|
`
|
||||||
|
const clientMethodTmpl = `
|
||||||
|
func (p *{{.ServiceName}}Client) {{.MethodName}}(in {{.ArgsType}}, out *{{.ReplyType}}) error {
|
||||||
|
return p.Client.Call("{{.ServiceName}}.{{.MethodName}}", in, out)
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
// gen client method list
|
||||||
|
var methodList string
|
||||||
|
for _, m := range svc.Method {
|
||||||
|
out := bytes.NewBuffer([]byte{})
|
||||||
|
t := template.Must(template.New("").Parse(clientMethodTmpl))
|
||||||
|
t.Execute(out, &struct{ ServiceName, ServiceRegisterName, MethodName, ArgsType, ReplyType string }{
|
||||||
|
ServiceName: generator.CamelCase(svc.GetName()),
|
||||||
|
ServiceRegisterName: file.GetPackage() + "." + generator.CamelCase(svc.GetName()),
|
||||||
|
MethodName: generator.CamelCase(m.GetName()),
|
||||||
|
ArgsType: p.TypeName(p.ObjectNamed(m.GetInputType())),
|
||||||
|
ReplyType: p.TypeName(p.ObjectNamed(m.GetOutputType())),
|
||||||
|
})
|
||||||
|
methodList += out.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// gen all client code
|
||||||
|
{
|
||||||
|
out := bytes.NewBuffer([]byte{})
|
||||||
|
t := template.Must(template.New("").Parse(clientHelperFuncTmpl))
|
||||||
|
t.Execute(out, &struct{ PackageName, ServiceName, MethodList string }{
|
||||||
|
PackageName: file.GetPackage(),
|
||||||
|
ServiceName: generator.CamelCase(svc.GetName()),
|
||||||
|
MethodList: methodList,
|
||||||
|
})
|
||||||
|
p.P(out.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
generator.RegisterPlugin(new(netrpcPlugin))
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user