Protobuf 是 Protocol Buffers 的简称,是一种与语言、平台无关,可扩展的序列化结构化数据的数据描述语言,Protobuf作为接口规范的描述语言,可以作为设计安全的跨语言PRC接口的基础工具。
基本语法
hello.proto 文件
syntax = "proto3"; package main; message String { string value = 1; } 复制代码
- 第一行声明使用 proto3 语法。否则,默认使用 proto2 语法,目前主流推荐使用 v3 版本。此声明必须是文件的非空、非注释的第一行。
- package 指令指明当前是 main 包,用户也可以针对不同的语言定制对应的包路径和名称。
- message 关键字定义一个 String 类型消息体,在最终生成的Go语言代码中对应一个 String 结构体。每一个消息体的字段包含三个属性:类型、字段名称、字段编号。在消息体的定义上,除类型以外均不可重复。此处 String 类型中只有一个字符串类型的 value 成员,该成员编码时用1编号代替名字。
- Protobuf 中最基本的数据单元是 message,类似 Go 语言中的结构体。在 message 中可以嵌套 message 或其它的基础数据类型的成员。
关于标识号
消息体中字段定义了唯一的数字值。这些数字是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。
最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]的标识号, Protobuf 协议实现中对这些进行了预留。如果非要在 .proto 文件中使用这些预留标识号,编译时就会报警。类似地,你不能使用之前保留的任何标识符。
添加注释
.proto 文件添加注释,可以使用C/C++风格的 // 和 /* … */ 语法格式
保留字段
如果从前面定义的消息中删除了 和 字段,应保留其字段编号,使用关键字 reserved
:
syntax "proto3"; message Stock { reserved 3, 4; // ... } 复制代码
还可以将 reserved 关键字用作将来可能添加的字段的占位符。 可以使用 to
关键字将连续字段号占位。
syntax "proto3"; message Info { reserved 2, 9 to 11, 15; // ... } 复制代码
生成相应的Go代码
Protobuf 核心的工具集是 C++ 语言开发的,官方的 protoc 编译器中并不支持Go语言。要想基于上面 的 hello.proto 文件生成相应的Go代码,需要安装相应的插件。
- 安装官方的 protoc 工具,可以从 github.com/google/prot… 下载。
- 安装针对Go语言的代码生成插件,通过 go get github.com/golang/protobuf/protoc-gen-go 命令安装。
通过以下命令生成相应的Go代码:
$ protoc --go_out=. hello.proto
- go_out 参数告知 protoc 编译器去加载对应的 protoc-gen-go 工具,生成的代码放到当前目录。最后是一系列要处理的protobuf文件的列表。
- plugins=plugin1+plugin2:指定要加载的子插件列表,我们定义的 proto 文件是涉及了 RPC 服务的,而默认是不会生成 RPC 代码的,因此需要在 go_out 中给出 plugins 参数传递给 protoc-gen-go,告诉编译器,请支持 RPC(这里指定了内置的 grpc 插件)。
基本数据类型
protobuf 所生成出来的数据类型并非与原始的类型完全一致,下面是一些常见的类型映射:
生成的 hello.pb.go 文件
pb.go 文件是对 proto 文件所生成的对应的 Go 代码,在实际应用中将会引用到此文件。
// Code generated by protoc-gen-go. DO NOT EDIT. // source: hello.proto package main import ( fmt "fmt" proto "github.com/golang/protobuf/proto" math "math" ) // 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.ProtoPackageIsVersion3 // please upgrade the proto package type String struct { Value *String `protobuf:"bytes,1,opt,name=value,proto3" 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_61ef911816e0a8ce, []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 (m *String) XXX_Merge(src proto.Message) { xxx_messageInfo_String.Merge(m, 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 nil } func init() { proto.RegisterType((*String)(nil), "main.String") } func init() { proto.RegisterFile("hello.proto", fileDescriptor_61ef911816e0a8ce) } var fileDescriptor_61ef911816e0a8ce = []byte{ // 84 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, 0xd2, 0xe1, 0x62, 0x0b, 0x2e, 0x29, 0xca, 0xcc, 0x4b, 0x17, 0x52, 0xe2, 0x62, 0x2d, 0x4b, 0xcc, 0x29, 0x4d, 0x95, 0x60, 0x54, 0x60, 0xd4, 0xe0, 0x36, 0xe2, 0xd1, 0x03, 0xc9, 0xeb, 0x41, 0x24, 0x83, 0x20, 0x52, 0x49, 0x6c, 0x60, 0xad, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0x76, 0x0c, 0x0e, 0x54, 0x49, 0x00, 0x00, 0x00, } 复制代码
- String 类型自动生成了一组方法,其中 ProtoMessage 方法表示这是一个实现了 proto.Message 接口的方法。此外 Protobuf 还为每个成员生成了一个Get方法,能够提供便捷的取值方式,并且处理了一些空指针取值的情况,还能够通过 Reset 方法来重置该参数。
- .pb.go 文件的初始化方法,注意 fileDescriptor 的相关语句。fileDescriptor_61ef911816e0a8ce 表示是一个经过编译后的 proto 文件,是对 proto 文件的整体描述,其包含了 proto 文件名、引用(import)内容、包(package)名、选项设置、所有定义的消息体(message)、所有定义的枚举(enum)、所有定义的服务( service)、所有定义的方法(rpc method)等等内容。
- 每一个 Message Type 中都包含了 Descriptor 方法,Descriptor 代指对一个消息体(message)定义的描述,而这一个方法则会在 fileDescriptor 中寻找属于自己 Message Field 所在的位置再进行返回。
Protobuf 和 RPC组合
基于 String 类型,重新实现 HelloService 服务
package main import ( "log" "net" "net/rpc" "rpc/protoc" ) // HelloService is rpc server obj type HelloService struct{} //Hello方法的输入参数和输出的参数均改用 Protobuf 定义的 String 类型表示。 //因为新的输入参数为结构体类型,因此改用指针类型作为输入参数,函数的内部代码同时也做了相应的调整。 func (p *HelloService) Hello(request *protoc.String, reply *protoc.String) error { reply.Value = "hello:" + request.GetValue() return nil } func main() { rpc.RegisterName("HelloService", new(HelloService)) listener, err := net.Listen("tcp", ":1234") if err != nil { log.Fatal("ListenTCP error:", err) } conn, err := listener.Accept() if err != nil { log.Fatal("Accept error", err) } rpc.ServeConn(conn) } 复制代码
下面是客户端请求HelloService服务的代码 client.go:
package main import ( "fmt" "log" "net/rpc" "rpc/protoc" ) func main() { client, err := rpc.Dial("tcp", "localhost:1234") if err != nil { log.Fatal("dialing err:", err) } var reply = &protoc.String{} var param = &protoc.String{ Value: "hello wekenw", } err = client.Call("HelloService.Hello", ¶m, &reply) if err != nil { log.Fatal(err) } fmt.Println(reply) } 复制代码
开启服务器端,开启客户端。客户端的执行结果如下:
$ go run client.go value:"hello:hello wekenw"