简介
Protocol Buffers是什么?
protocol buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小、更快、更为简单。你可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言进行编写和读取结构数据。你甚至可以更新数据结构,而不破坏根据旧数据结构编译而成并且已部署的程序。
1 . 使用protobuf实现节点间通信, 编码报文以提高传输效率;
2 . protobuf全程Protocol Buffers, 是Google开发的一种数据描述语言;
3 . Protobuf是一种轻便高效的结构化数据存储格式;
4 . Protobuf跟存储格式,语言, 平台无关;
5 . protobuf可扩展可序列化;
6 . protobuf以二进制方式存储, 占用内存空间小;
protobuf广泛地应用于远程过程调用(PRC)的二进制传输,使用protobuf的目的是为了获得更高的性能。传输前使用protobuf编码,接收方再进行解码,可显著地降低二进制传输数据的大小。另外,protobuf非常适合传输结构化数据,便于通信字段的扩展。
用途
1 . 可以轻松引入新字段, 中间服务器不需要检查数据, 可以简单解析他并传递数据而无需了解所有字段;
2 . 格式更具有自我描述性, 可以用各种语言处理(C++,Java等)
随着系统发展, 他获得了其他功能和用途:
3 . 自动生成的序列化和反序列化代码避免了手动解析的需要;
**4 . 除了用于短期RPC(远程过程调用)请求之外, 人们还开始使用 protocol buffers作为一种方便的自描述格式, **用于持久存储数据(例如在 Bigtable中);
5 . 服务器RPC接口开始被声明为协议文件的一部分, protocol编译器生成存根类, 用户可以使用服务器接口的实际实现来覆盖这些类;
它是如何工作的?
你可以通过在 .proto 文件中定义 protocol buffer message 类型,来指定你想如何对序列化信息进行结构化。每一个 protocol buffer message 是一个信息的小逻辑记录,包含了一系列的 name-value 对。这里有一个非常基础的 .proto 文件样例,它定义了一个包含 "person" 相关信息的 message:
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
正如你所见,message 格式很简单 - 每种 message 类型都有一个或多个具有唯一编号的字段,每个字段都有一个名称和一个值类型,其中值类型可以是数字(整数或浮点数),布尔值,字符串,原始字节,甚至(如上例所示)其它 protocol buffer message 类型,这意味着允许你分层次地构建数据。你可以指定 optional 字段,required 字段和 repeated 字段。 你可以在 Protocol Buffer 语言指南 中找到有关编写 .proto 文件的更多信息。
proto3 已舍弃 required 字段,optional 字段也无法显示使用(因为缺省默认就设置为 optional)
一旦定义了 messages,就可以在 .proto 文件上运行 protocol buffer 编译器来生成指定语言的数据访问类。这些类为每个字段提供了简单的访问器(如 name()和 set_name()),以及将整个结构序列化为原始字节和解析原始字节的方法 - 例如,如果你选择的语言是 C++,则运行编译器上面的例子将生成一个名为 Person 的类。然后,你可以在应用程序中使用此类来填充,序列化和检索 Person 的 messages。于是你可以写一些这样的代码:
Person person;
person.set_name("John Doe");
person.set_id(1234);
person.set_email("jdoe@example.com");
fstream output("myfile", ios::out | ios::binary);
person.SerializeToOstream(&output);
之后,我们可以重新读取解析 message
**
fstream input("myfile", ios::in | ios::binary);
Person person;
person.ParseFromIstream(&input);
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;
你可以在 message 格式中添加新字段,而不会破坏向后兼容性;旧的二进制文件在解析时只是忽略新字段。因此,如果你的通信协议使用 protocol buffers 作为其数据格式,则可以扩展协议而无需担心破坏现有代码。
为什么不适用XML?
对于序列化结构数据, protocol buffers 比XML更具优势, Protocol buffers:
1 . 更简单
2 . 小3 - 10倍
3 . 快20 - 100 倍
4 . 更加清晰明确
5 . 自动生成更易于以编程方式使用的数据访问类;
**
例如:
假设你想要为具有姓名和电子邮件的人建模, 在xml中, 我们需要:
<person>
<name>John Doe</name>
<email>jdoe@example.com</email>
</person>
**
而相对应的Protocol Buffer Message 格式是:
# Textual representation of a protocol buffer.
# This is *not* the binary format used on the wire.
person {
name: "John Doe"
email: "jdoe@example.com"
}
当此消息编码为protocol buffer 二进制格式 时(上面的文本格式只是为了调试和编辑的方便而用人类可读的形式表示),它可能是 28 个字节长,需要大约 100-200 纳秒来解析。如果删除空格,XML版本至少为 69 个字节,并且需要大约 5,000-10,000 纳秒才能解析。
此外,比起 XML,操作 protocol buffer 更为容易:
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;
** 而使用XML, 必须执行如下操作:**
**
cout << "Name: "
<< person.getElementsByTagName("name")->item(0)->innerText()
<< endl;
cout << "E-mail: "
<< person.getElementsByTagName("email")->item(0)->innerText()
<< endl;
但是,protocol buffers 并不总是比 XML 更好的解决方案 - 例如,protocol buffers 不是使用标记(例如 HTML)对基于文本的文档建模的好方法,因为你无法轻松地将结构与文本交错。此外,XML 是人类可读的和人类可编辑的;protocol buffers,至少它们的原生格式,并不具有这样的特点。XML 在某种程度上也是自我描述的。只有拥有 message 定义(.proto文件)时,protocol buffer 才有意义;
准备使用的包
Protoc
protoc是protobuf文件(.proto)的编译器,使用protoc工具可以将.proto文件转换为各种编程语言对应的源码,包含数据类型定义和调用接口等;
从 https://github.com/protocolbuffers/protobuf/releases 中下载最新的protobuf安装包 protoc-3.15.6-win64.zip
解压压缩包后将bin目录下的protoc.exe文件移动到$GOPATH/bin目录下,注意$GOPATH/bin需要提前添加到环境变量Path目录下;
$ protoc -help
$ protoc --version
libprotoc 3.15.6
Protobuf
protobuf对于Golang有两个可选用的包分别是官方的goprotobuf和gogoprotobuf,gogoprotobuf是完全兼容Google Protobuf的,只是生成的代码质量要比goprotbuf要高。
安装
go get github.com/golang/protobuf/proto
go get github.com/gogo/protobuf/proto
Protoc-gen-go
protoc-gen-go 是 protobuf 编译插件系列中的Go版本,protoc-gen-to 使用Golang编写。
在Golang中使用protobuf需提前安装 protoc-gen-to工具,用于将.proto文件转换为Golang代码。
go get -u github.com/golang/protobuf/protoc-gen-go
protoc-gen-go将自动安装到$GOPATH/bin目录下
protobuf会在.proto文件中定义需要处理的结构化数据,通过protoc工具可将.proto文件转换为C、C++、Golang、Java、Python等多种语言的代码,因此兼容性好且易于使用;
protoc --go_out=. *.proto
命令之后理论上会将当前目录下的所有的.proto
文件生成.pb.go
文件,但实际测试发现报错,不推荐使用;
Protoc-gen-gogo
gogoprotobuf有两个插件可用分别是protoc-gen-gogo和protoc-gen-gofast,protoc-gen-gogo生成的文件和protoc-gen-go一样性能略快,protoc-gen-gofast生成的Golang文件更为复杂,但性能却高出5~7倍;
安装
go get github.com/gogo/protobuf/protoc-gen-gogo
protoc *.proto --gogo_out=.
protoc-gen-gofast
安装
go get github.com/gogo/protobuf/protoc-gen-gofast
protoc *.proto --gofast_out=.
# 执行后会将当前目录下的所有.proto文件生成.pd.go文件
语法
Protobuf协议规定:使用Protobuf协议进行数据序列化和反序列化操作时,首先需要定义传输数据的格式,并命名以.proto为扩展名的消息定义文件;
使用message定义一个消息;
指定消息字段类型
分配标识符,在消息字段中每个字段都有唯一的一个标识符,最小标识号可以从1开始,最大到536870911。不可以使用[19000 ~ 19999]之间的标号;
指定字段规则,字段修饰符包括required、optional、repeated三种类型,注意required弊大于利;
使用
1 . 按照protobuf语法, 在.proto文件中定义数据结构, 同时使用protoc工具生成Golang代码;
2 . 在项目代码中引用生成的Golang代码;
定义消息类型
syntax = "proto3";
package proto;
message User{
string name = 1;
bool male = 2;
repeated int32 balance = 3;
}
protoc *.proto --gogo_out=.
标量类型
Protobuf类型 | Golang类型 | 描述 |
---|---|---|
int32 | int32 | 变长编码,对于负值效率较低。若域可能存在负值可使用sint64替代。 |
int64 | int64 | - |
uint32 | uint32 | 变长编码 |
uint64 | uint64 | 变长编码 |
sint32 | int32 | 变长编码,在负值时比int32高效。 |
sint64 | int64 | 变长编码,有符号整型值。编码时比int64高效。 |
fixed32 | uint32 | 固长编码,4个字节,若数值大于2^28则比uint32高效。 |
fixed64 | uint64 | 固长编码,8个字节,若数值大于2^56则比uint64高效。 |
sfixed32 | int32 | 固长编码,4个字节。 |
sfixed64 | int64 | 固长编码,8个字节。 |
float | float32 | - |
double | float64 | - |
bool | bool | 默认false |
bytes | []byte | 任意字节序列,长度不超过2^32,默认空数组。 |
string | string | UTF8编码或7-bit ASCII编码的文本,长度不超过2^32。 |
标量类型如果没有被赋值则不会被序列化,解析时会赋予默认值
标量类型 | 默认值 |
---|---|
strings | 空字符串 |
bytes | 空序列 |
bools | false |
数值类型 | 0 |
**
文件
1 . 文件名使用小写下划线的命名风格,例如lower_snake_case.proto;
2 . 每行不超过80个字符;
3 . 使用2个空格缩进;
包
包名应该和目录结构对应,例如文件在my/package/目录下,则包名为my.package;
消息
1 . 消息名使用首字母大写驼峰风格(CamelCase),例如message PlayerRequest{...};
2 . 字段名使用小写下划线风格,例如string user_id = 1;
3 . 枚举类型中枚举名使用首字母大写驼峰风格,例如enum FooBar,枚举值使用全大写下划线分割的风格(CAPITALS_WITH_UNDERSCORES),例如FOO_DEFAULT = 1;
服务
RPC服务名和方法名均使用首字母大写驼峰风格, 例如 service FooService{rpc GetSomething()};
案例
创建 .proto 文件
cat test1.proto
syntax = "proto3";
package pb;
message Player {
string user_id = 1;
string name = 2;
string icon = 3;
int32 point = 4;
int32 seat = 5;
int32 identity = 6;
int32 status = 7;
}
# 生成.pd.go文件
protoc test1.proto --gogo_out=.
创建测试代码
package main
import (
"fmt"
"github.com/gogo/protobuf/proto"
"proto_demo1/pb"
)
func main() {
player := &pb.Player{
UserId: "1",
Name: "admin",
}
//序列化
buf, err := proto.Marshal(player)
if err != nil {
panic(err)
}
fmt.Printf("%v
", buf) // [10 1 49 18 5 97 100 109 105 110]
fmt.Printf("%s
", buf) // 1admin
//反序列化
obj := &pb.Player{}
err = proto.Unmarshal(buf, obj)
if err != nil {
panic(err)
}
fmt.Printf("%v
", obj) // user_id:"1" name:"admin"
fmt.Printf("%v
", obj.GetName()) // admin
}
/*
result
proto_demo1 % go run main.go
[10 1 49 18 5 97 100 109 105 110]
1admin
user_id:"1" name:"admin"
admin
*/