Protocol Buffers 介绍
Protobuf 是 Protocol Buffers 的缩写,是 Google 推出的一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储(Bigtable)等。可类比 xml、json 等数据传输方式,其特点为轻量、高效,压缩率高,向后兼容性好。
Protobuf 主要用于不同的编程语言的协作 RPC 场景下,定义需要序列化的数据格式。Protobuf 本质上仅仅是一种用于交互的结构式定义,从功能上和 XML、JSON 等各种其他的交互形式都并无本质不同,只负责定义不负责数据编解码。
其官方介绍如下:
Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.
历史
2001 年初,为了解决服务器端新旧协议(高低版本)兼容性问题,Protobuf 首先在 Google 内部创建, 我们把它称之为 proto1,一直以来在 Google 的内部使用,其中也不断的演化,根据使用者的需求也添加很多新的功能,一些内部库依赖它。几乎每个 Google 的开发者都会使用到它。Google 开始开源它的内部项目时,因为依赖的关系,所以他们决定首先把 Protobuf 开源出去。
proto1 在演化的过程中有些混乱,所以 Protobuf 的开发者重写了 Protobuf 的实现,保留了 proto1 的大部分设计,以及 proto1 的很多的想法。开源的 proto2 不依赖任何的 Google 的库,代码也相当的清晰。
2008年7月7日,Protobuf 开始公布出来。Protobuf 公布出来也得到了大家的广泛的关注, 逐步地也得到了大家的认可,很多项目也采用 Protobuf 进行消息的通讯,还有基于 Protobuf 的微服务框架 gRPC。
在使用的过程中,大家也提出了很多的意见和建议,Protobuf 也在演化,于 2016 年推出了 Proto3。 Proto3 简化了 proto2 的开发,提高了开发的效能,但是也带来了版本不兼容的问题。由于很多公司很早的就采用了 Protobuf,所以很多项目还在使用 proto2 协议,目前是proto2和proto3同时在使用的状态。
Protocol Buffer 名称来自于初期一个主要的类的名称 ProtocolBuffer。
Protocol buffers 的多语言支持
protobuf 是支持多种编程语言的,即多种编程语言的类型数据可以转换成 protobuf 定义的类型数据,各种语言的类型对应可以看此介绍。
我们介绍一下 protobuf 对多语言的支持原理。protobuf 有个程序叫 protoc,它是一个编译程序,负责把 proto 文件编译成对应语言的文件,它已经支持了C++、C#、Java、Python,而对于 Go 和 Dart 需要安装插件才能配合生成对于语言的文件。
对于 C++,protoc 可以把a.proto
,编译成a.pb.h
和a.pb.cc
。
对于 Go,protoc 需要使用插件 protoc-gen-go,把a.proto
,编译成a.pb.go
,其中包含了定义的数据类型,它的序列化和反序列化函数等。
对 Go 语言,protoc 只负责利用 protoc-gen-go 把 proto 文件编译成 Go 语言文件,并不负责序列化和反序列化,生成的 Go 语言文件中的序列化和反序列化操作都是只是wrapper。
那 Go 语言对 protobuf 的序列化和反序列化,是由谁完成的?
由github.com/golang/protobuf/proto
完成,它负责把结构体等序列化成 proto 数据([]byte
),把 proto 数据反序列化成 Go 结构体。
OK,原理部分就铺垫这些,看一个简单样例,了解protoc和 protoc-gen-go 的使用,以及进行序列化和反序列化操作。
Hello World 样例
根据上面的介绍,Go 语言使用 protobuf 我们要先安装 2 个工具:protoc 和 protoc-gen-go。
安装 protoc 和 protoc-gen-go
首先去下载页下载符合你系统的 protoc,本文示例版本如下:
➜ protoc-3.9.0-osx-x86_64 tree .
.
├── bin
│ └── protoc
├── include
│ └── protobuf
│ ├── any.proto
│ ├── api.proto
│ ├── compiler
│ │ └── plugin.proto
│ ├── descriptor.proto
│ ├── duration.proto
│ ├── empty.proto
│ ├── field_mask.proto
│ ├── source_context.proto
│ ├── struct.proto
│ ├── timestamp.proto
│ ├── type.proto
│ └── wrappers.proto
└── readme.txt
5 directories, 14 files
protoc 的安装步骤在 readme.txt 中:
To install, simply place this binary somewhere in your PATH.
把protoc-3.9.0-osx-x86_64/bin
加入到PATH。
If you intend to use the included well known types then don’t forget tocopy the contents of the ‘include’ directory somewhere as well, for exampleinto ‘/usr/local/include/‘.
如果使用已经定义好的类型,即上面 include 目录*.proto
文件中的类型,把 include 目录下文件,拷贝到/usr/local/include/
。
安装 protoc-gen-go:
go get –u github.com/golang/protobuf/protoc-gen-go
检查安装,应该能查到这 2 个程序的位置:
which protoc
/usr/local/bin/protoc
which protoc-gen-go
/Users/wenbo.han/go/bin/protoc-gen-go
Hello world
创建了一个使用protoc的小玩具,项目地址:
golang_step_by_step/protobuf/helloworld1 at master · Shitaibin/golang_step_by_step
它的目录结构如下:
➜ protobuf git:(master) tree helloworld1
helloworld1
├── main.go
├── request.proto
└── types
└── request.pb.go
定义proto文件
使用proto3,定义一个Request,request.proto
内容如下:
// file: request.proto
syntax = "proto3";
package helloworld;
option go_package="./types";
message Request {
string data = 1;
}
- syntax:protobuf 版本,现在是 proto3
- package:不完全等价于 Go 的 package,最好另行设定
go_package
,指定根据 protoc 文件生成的 go 语言文件的 package 名称。 - message:会编译成 Go 的
struct
。
-
string data = 1
:代表 request 的成员 data 是 string 类型,该成员的 id 是 1,protoc 给每个成员都定义一个编号,编解码的时候使用编号代替使用成员名称,压缩数据量。
编译 proto 文件
$ protoc --go_out=. ./request.proto
-
-go_out
指明了要把./request.proto
编译成 Go 语言文件,生成的是./types/request.pb.go
,注意观察一下为 Request 结构体生产的2个方法XXX_Unmarshal
和XXX_Marshal
,文件内容如下: - request.pb.go
// file: ./types/request.pb.go
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: request.proto
package types
import (
fmt "fmt"
math "math"
proto "github.com/golang/protobuf/proto"
)
// 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 Request struct {
Data string `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`
// 以下是 protobuf 自动填充的字段,protobuf 需要使用
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Request) Reset() { *m = Request{} }
func (m *Request) String() string { return proto.CompactTextString(m) }
func (*Request) ProtoMessage() {}
func (*Request) Descriptor() ([]byte, []int) {
return fileDescriptor_7f73548e33e655fe, []int{0}
}
// 反序列化函数
func (m *Request) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Request.Unmarshal(m, b)
}
// 序列化函数
func (m *Request) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Request.Marshal(b, m, deterministic)
}
func (m *Request) XXX_Merge(src proto.Message) {
xxx_messageInfo_Request.Merge(m, src)
}
func (m *Request) XXX_Size() int {
return xxx_messageInfo_Request.Size(m)
}
func (m *Request) XXX_DiscardUnknown() {
xxx_messageInfo_Request.DiscardUnknown(m)
}
var xxx_messageInfo_Request proto.InternalMessageInfo
// 获取字段
func (m *Request) GetData() string {
if m != nil {
return m.Data
}
return ""
}
func init() {
proto.RegisterType((*Request)(nil), "helloworld.Request")
}
func init() { proto.RegisterFile("request.proto", fileDescriptor_7f73548e33e655fe) }
var fileDescriptor_7f73548e33e655fe = []byte{
// 91 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2d, 0x4a, 0x2d, 0x2c,
0x4d, 0x2d, 0x2e, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0xca, 0x48, 0xcd, 0xc9, 0xc9,
0x2f, 0xcf, 0x2f, 0xca, 0x49, 0x51, 0x92, 0xe5, 0x62, 0x0f, 0x82, 0x48, 0x0a, 0x09, 0x71, 0xb1,
0xa4, 0x24, 0x96, 0x24, 0x4a, 0x30, 0x2a, 0x30, 0x6a, 0x70, 0x06, 0x81, 0xd9, 0x4e, 0x9c, 0x51,
0xec, 0x7a, 0xfa, 0x25, 0x95, 0x05, 0xa9, 0xc5, 0x49, 0x6c, 0x60, 0xcd, 0xc6, 0x80, 0x00, 0x00,
0x00, 0xff, 0xff, 0x2e, 0x52, 0x69, 0xb5, 0x4d, 0x00, 0x00, 0x00,
}
编写Go语言程序
下面这段测试程序就是创建了一个请求,序列化又反序列化的过程。
// file: main.go
package main
import (
"fmt"
"./types"
"github.com/golang/protobuf/proto"
)
func main() {
req := &types.Request{Data: "Hello LIB"}
// Marshal
encoded, err := proto.Marshal(req)
if err != nil {
fmt.Printf("Encode to protobuf data error: %v", err)
}
// Unmarshal
var unmarshaledReq types.Request
err = proto.Unmarshal(encoded, &unmarshaledReq)
if err != nil {
fmt.Printf("Unmarshal to struct error: %v", err)
}
fmt.Printf("req: %v\n", req.String())
fmt.Printf("unmarshaledReq: %v\n", unmarshaledReq.String())
}
运行结果:
➜ helloworld1 git:(master) go run main.go
req: data:"Hello LIB"
unmarshaledReq: data:"Hello LIB"