Loading... ## Protobuf 简介  protobuf 是 google 旗下的一款平台无关,语言无关,可扩展的序列化结构数据格式。所以很适合用做数据存储和作为不同应用,不同语言之间相互通信的数据交换格式,只要实现相同的协议格式即同一 proto 文件被编译成不同的语言版本,加入到各自的工程中去。这样不同语言就可以解析其他语言通过 protobuf 序列化的数据。 目前官网提供了 C++,Python,JAVA,GO 等语言的支持。google 在 2008 年 7 月 7 号将其作为开源项目对外公布。 Google Protocol Buffer (简称 Protobuf )是一种轻便高效的结构化数据存储格式,平台无关、语言无关、可扩展,可用于通讯协议和数据存储等领域。 ## 数据交互的格式比较 数据交互**xml**、**json**、**protobuf** 格式比较 - **json**:一般的web项目中,最流行的主要还是 json。因为浏览器对于 json 数据支持非常好,有很多内建的函数支持。 - **xml**:在 webservice 中应用最为广泛,但是相比于 json,它的数据更加冗余,因为需要成对的闭合标签。**json 使用了键值对的方式**,不仅压缩了一定的数据空间,同时也具有可读性。 - **protobuf**:是后起之秀,是谷歌开源的一种数据格式,适合高性能,对响应速度有要求的数据传输场景。因为 profobuf 是二进制数据格式,需要编码和解码。数据本身不具有可读性。因此只能反序列化之后得到真正可读的数据。 相对于其它,protobuf 更具有优势: - 序列化后体积相比 Json 和 XML 很小,适合网络传输 - 支持跨平台多语言 - 消息格式升级和兼容性还不错 - 序列化反序列化速度很快,快于 Json 的处理速速 ## ProtoBuf 的优点 Protobuf 犹如 XML,不过它更小、更快、也更简单。你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。你甚至可以在无需重新部署程序的情况下更新数据结构。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。 它有一个非常棒的特性,即“向后”兼容性好,人们不必破坏已部署的、依靠“老”数据格式的程序就可以对数据结构进行升级。 Protobuf 语义更清晰,无需类似 XML 解析器的东西(因为 Protobuf 编译器会将 .proto 文件编译生成对应的数据访问类以对 Protobuf 数据进行序列化、反序列化操作)。 使用 Protobuf 无需学习复杂的文档对象模型,Protobuf 的编程模式比较友好,简单易学,同时它拥有良好的文档和示例,对于喜欢简单事物的人们而言,Protobuf 比其他的技术更加有吸引力。 ## ProtoBuf 的不足 Protobuf 与 XML 相比也有不足之处。它功能简单,无法用来表示复杂的概念。 XML 已经成为多种行业标准的编写工具,Protobuf 只是 Google 公司内部使用的工具,在通用性上还差很多。 由于文本并不适合用来描述数据结构,所以 Protobuf 也不适合用来对基于文本的标记文档(如 HTML)建模。 另外,由于 XML 具有某种程度上的自解释性,它可以被人直接读取编辑,在这一点上 Protobuf 不行,它以二进制的方式存储,除非你有 `.proto` 定义,否则你没法直接读出 Protobuf 的任何内容。 ## ProtoBuf 的安装 ### 安装 ProtoBuf 这里是旧版本(如果新版有问题,建议使用我下载好的):[https://cloud.189.cn/t/rIJrUbb2UnIj](https://cloud.189.cn/t/rIJrUbb2UnIj) (访问码:e8uo) ```shell # 下载 protoBuf: $ git clone https://github.com/protocolbuffers/protobuf.git # 或者下载刚才的压缩包后直接将压缩包拖入后解压 unzip protobuf.zip # 安装依赖库 $ sudo apt install autoconf automake libtool curl make g++ unzip libffi-dev -y # 安装 $ cd protobuf/ $ git submodule update --init --recursive $ ./autogen.sh $ ./configure $ make $ sudo make install # 刷新共享库 很重要的一步 $ sudo ldconfig # 成功后需要使用命令测试(最好重启一下) $ protoc –h ``` ### 获取 Proto 包 这里是旧版本(如果新版有问题,建议使用我下载好的):[https://cloud.189.cn/t/nIZRNnIbIbQv](https://cloud.189.cn/t/nIZRNnIbIbQv) (访问码:wgd9) ```shell # Go语言的 proto API 接口 # -v -u:-v 显示做了哪些操作; -u 下载对应依赖 $ go get -v -u github.com/golang/protobuf/proto ``` ### 安装 protoc-gen-go 插件 它是一个 go 程序,编译它之后将可执行文件复制到 `\bin` 目录。 ```shell # 安装 $ go get -v -u github.com/golang/protobuf/protoc-gen-go # 编译 $ cd $GOPATH/src/github.com/golang/protobuf/protoc-gen-go/ $ go build # 将生成的 protoc-gen-go 可执行文件,放在 /bin 目录下 $ sudo cp protoc-gen-go /bin/ # 如何检查? 输入部分命令, 按 TAB 键可以自动补全就可以了 ``` 如果下载不了上面两个(大概率下载不好 = - =),可以这样下载: ```shell $ git clone https://github.com/golang/protobuf.git ``` 将下载好的 `github.com-golang-protobuf.zip` 文件解压到 `$GOPATH/src/github.com/golang` 下,如果是 `git` 下载的话就直接移动到 `$GOPATH/src/github.com/golang` 下,如下图所示:  移动完成之后,进入到 `$GOPATH/src/github.com/golang/protobuf/protoc-gen-go/` 里面,输入以下命令: ```shell go build ``` 如果出现以下报错:  解决方案: 在 `$GOPATH/go/src` 目录下面创建一个 `golang.org/x` 目录 ```shell $ cd $GOPATH/src $ mkdir -p golang.org/x ``` 进入 `golang.org/x`下载三个包(好像不下也可以?反正早晚都要用,就先下载了吧,主要是配置后面的环境变量) ```shell $ cd golang.org/x $ git clone https://github.com/golang/crypto.git $ git clone https://github.com/golang/sys.git $ git clone https://github.com/golang/net.git $ go install net ``` 然后配置一下环境变量 打开 `profile` 文件 ```bash sudo vim /etc/profile ``` 打开之后添加如下内容: ```bash export GOPROXY=https://goproxy.cn/,direct export GO111MODULE=on ``` 改完 `profile` 文件之后执行下面命令使之生效: ```bash source /etc/profile ``` 然后再进入到 `$GOPATH/src/github.com/golang/protobuf/protoc-gen-go/` 里面,执行最初的命令 ```shell $ cd $GOPATH/src/github.com/golang/protobuf/protoc-gen-go/ $ go build ``` 将生成的 `protoc-gen-go` 可执行文件,放在 `/bin` 目录下 ```shell $ sudo cp protoc-gen-go /bin/ ```  ## ProtoBuf 语法 要想使用 protobuf 必须得先定义 proto 文件。所以得先熟悉 protobuf 的消息定义的相关语法。 ### 定义一个消息类型 ```protobuf syntax = "proto3"; // 版本为 3 message ConanRequest { // 发送的请求 string name = 1; // 数字表示顺序 int32 age = 2; repeated string hobbies = 3; // repeated:切片 } ``` **注:** ConanRequest 消息格式有3个字段,在消息中承载的数据分别对应于每一个字段。其中每个字段都有一个名字和一种类型。 文件的第一行指定了你正在使用 proto3 语法:如果你没有指定这个,编译器会使用proto2。这个指定语法行必须是文件的非空非注释的第一个行。 在上面的例子中,所有字段都是标量类型:两个string类型(name 和 hobbies),一个整型(age)。 Repeated 关键字表示**重复的**,那么在 go 语言中用切片进行代表 正如上述文件格式,在消息定义中,每个字段都有唯一的一个标识符。 ### 添加更多消息类型 在一个 `.proto` 文件中可以定义多个消息类型。在定义多个相关的消息的时候,这一点特别有用。例如,如果想定义与 `SearchResponse` 消息类型对应的回复消息格式的话,你可以将它添加到相同的 `.proto` 文件中 ```protobuf syntax = "proto3"; message ConanRequest { string name = 1; int32 age = 2; repeated string hobbies = 3; } message ConanResponse { ... } ``` ### 添加注释 向 `.proto` 文件添加注释,可以使用 `C/C++/java/Go` 风格的**双斜杠(//)** 语法格式,如: ```protobuf syntax = "proto3"; message ConanRequest { string name = 1; // 姓名 int32 age = 2; // 年龄 repeated string hobbies = 3; // 爱好 } message ConanResponse { ... } ``` ### 从 .proto 文件生成了什么 当用 protocol buffer 编译器来运行 `.proto` 文件时,编译器将生成所选择语言的代码,这些代码可以操作在 `.proto` 文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。 对 `C++` 来说,编译器会为每个 `.proto` 文件生成一个 `.h` 文件和一个 `.cc` 文件,`.proto` 文件中的每一个消息有一个对应的类。 对 `Python` 来说,有点不太一样。Python 编译器为 `.proto` 文件中的每个消息类型生成一个含有静态描述符的模块,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的 Python 数据访问类。 对 go 来说,编译器会为每个消息类型生成了一个 `.pd.go` 文件。 ### 标准数据类型 一个标量消息字段可以含有一个如下的类型。该表格展示了定义于 `.proto` 文件中的类型,以及与之对应的、在自动生成的访问类中定义的类型: | .proto Type | 备注 | C++ Type | Python Type | Go Type | | - | - | - | - | - | | double | | double | float | float64 | | float | | float | float | float32 | | int32 | 使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用 sint64 替代 | int32 | int | int32 | | uint32 | 使用变长编码 | uint32 | int/long | uint32 | | uint64 | 使用变长编码 | uint64 | int/long | uint64 | | sint32 | 使用变长编码,这些编码在负值时比 int32 高效的多 | int32 | int | int32 | | sint64 | 使用变长编码,有符号的整型值。编码时比通常的 int64 高效。 | int64 | int/long | int64 | | fixed32 | 总是4个字节,如果数值总是比总是比 228 大的话,这个类型会比 uint32 高效。 | uint32 | int | uint32 | | fixed64 | 总是 8 个字节,如果数值总是比总是比 256 大的话,这个类型会比 uint64 高效。 | uint64 | int/long | uint64 | | sfixed32 | 总是 4 个字节 | int32 | int | int32 | | sfixed32 | 总是 4 个字节 | int32 | int | int32 | | sfixed64 | 总是 8 个字节 | | | | | bool | | bool | bool | bool | | string | 一个字符串必须是 UTF-8 编码或者 7-bit ASCII 编码的文本。 | string | str/unicode | string | | bytes | 可能包含任意顺序的字节数据。 | string | str | []byte | **注:如果有一些文件,在通过 protobuf 进行传输的时候, 用其它类型都不好用的情况下,就把他转换成二进制类型([]byte)** ### 默认值 当一个消息被解析的时候,如果被编码的信息不包含一个特定的元素,被解析的对象锁对应的域被设置位一个默认值,对于不同类型指定如下: - 对于 strings,默认是一个空 string - 对于 bytes,默认是一个空的 bytes - 对于 bools,默认是 false - 对于数值类型,默认是 0 ### 使用其它消息类型 你可以将其他消息类型用作字段类型。例如,假设在每一个 PersonInfo 消息中包含 Person 消息,此时可以在相同的 `.proto` 文件中定义一个 Person 消息类型,然后在 PersonInfo 消息中指定一个 Person 类型的字段: ```protobuf syntax = "proto3"; message PersonInfo { repeated Person info = 1; } message Person { string name = 1; int32 age = 2; repeated string hobbies = 3; } ``` ### 使用 proto2 消息类型 在你的 proto3 消息中导入 proto2 的消息类型也是可以的,反之亦然,然后 proto2 枚举不可以直接在 proto3 的标识符中使用(如果仅仅在 proto2 消息中使用是可以的)。 ### 嵌套类型 你可以在其他消息类型中定义、使用消息类型,在下面的例子中,Person 消息就定义在PersonInfo 消息内,如: ```protobuf syntax = "proto3"; message PersonInfo { message Person { string name = 1; int32 age = 2; repeated string hobbies = 3; } repeated Person info = 1; } ``` 如果你想在它的父消息类型的外部重用这个消息类型,你需要以 PersonInfo.Person 的形式使用它,如: ```protobuf syntax = "proto3"; message PersonInfo { message Person { string name = 1; int32 age = 2; repeated string hobbies = 3; } repeated Person info = 1; } message PersonMessage { PersonInfo.Person info = 1; } ``` 当然,你也可以将消息嵌套任意多层,如: ```protobuf syntax = "proto3"; message Grandpa {// Level 0 message Father {// Level 1 message son {// Level 2 string name = 1; int32 age = 2; } } message Uncle {// Level 1 message Son {// Level 2 string name = 1; int32 age = 2; } } } ``` ### 定义服务(service) 如果想要将消息类型用在 RPC (远程方法调用)系统中,可以在 `.proto` 文件中定义一个 RPC 服务接口,`protocol buffer` 编译器将会根据所选择的不同语言生成服务接口代码及存根。如,想要定义一个 RPC 服务并具有一个方法,该方法能够接收 `SearchRequest` 并返回一个 `SearchResponse`,此时可以在 `.proto `文件中进行如下定义: ```protobuf service SearchService { // rpc 服务的函数名 (传入参数) 返回 (返回参数) rpc Search (SearchRequest) returns (SearchResponse); } ``` 最直观的使用 `protocol buffer` 的 RPC 系统是 `gRPC`,一个由谷歌开发的语言和平台中的开源的 RPC 系统,gRPC 在使用 `protocl buffer` 时非常有效,如果使用特殊的 `protocol buffer` 插件可以直接为您从 `.proto `文件中产生相关的 RPC 代码。 如果你不想使用 gRPC,也可以使用 `protocol buffer` 用于自己的 RPC 实现。 ### 生成访问类 可以通过定义好的 `.proto` 文件来生成 Java, Python, C++, Ruby, JavaNano, Objective-C, 或者 C# 代码,需要基于 `.proto` 文件运行 `protocol buffer` 编译器 protoc。 通过如下方式调用 protocol 编译器: ```protobuf protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR path/to/file.proto ``` IMPORT_PATH 声明了一个 `.proto` 文件所在的解析 import 具体目录。如果忽略该值,则使用当前目录。如果有多个目录则可以多次调用 `--proto_path`,它们将会顺序的被访问并执行导入。`-I=IMPORT_PATH` 是 `--proto_path` 的简化形式。 **当然也可以提供一个或多个输出路径:** `--cpp_out` 在目标目录 DST_DIR 中产生 C++ 代码,可以在 C++ 代码生成参考中查看更多。 `--python_out` 在目标目录 DST_DIR 中产生 Python 代码,可以在 Python 代码生成参考中查看更多。 `--go_out` 在目标目录 DST_DIR 中产生 Go 代码,可以在 GO 代码生成参考中查看更多。 作为一个方便的拓展,如果 DST_DIR 以 `.zip` 或者 `.jar` 结尾,编译器会将输出写到一个 ZIP 格式文件或者符合 JAR 标准的 `.jar` 文件中。注意如果输出已经存在则会被覆盖,编译器还没有智能到可以追加文件。 你必须提议一个或多个 `.proto` 文件作为输入,多个 `.proto `文件可以只指定一次。虽然文件路径是相对于当前目录的,每个文件必须位于其 IMPORT_PATH 下,以便每个文件可以确定其规范的名称。 ## ProtoBuf 测试 protobuf 的使用方法是将数据结构写入到 `.proto` 文件中,使用 `protoc` 编译器编译(间接使用了插件)得到一个新的 go 包,里面包含 go 中可以使用的数据结构和一些辅助方法。 编写 `prototest.proto` 文件,文件内容: ```protobuf syntax = "proto3"; option go_package = ".;prototest"; message Test { // 姓名 string name = 1; // 年龄 int32 age = 2; // 爱好 repeated string hobbies = 3; } ``` 关于 `go_package` 的详情请去这里查看:[https://developers.google.com/protocol-buffers/docs/reference/go-generated#package](https://developers.google.com/protocol-buffers/docs/reference/go-generated#package) 编译,执行 ```shell $ protoc --go_out=./ *.proto ``` 生成 `prototest.pb.go` 文件 使用 ProtoBuf 做数据格式转换 编写 `test.go` 文件,文件内容: ```go package main import ( "fmt" "github.com/golang/protobuf/proto" "micro_demo/proto-learn/prototest" ) func main() { test := &prototest.Test{ Name: "李培冠", Age: 18, Hobbies: []string{"dance", "read"}, } fmt.Println("test: ", test) // 将 struct test 转换成 protobuf data, err := proto.Marshal(test) if err != nil { fmt.Print("转码失败: ", err.Error()) } fmt.Println("data: ", data) // 得到一个新的 Test 结构体:newTest newTest := &prototest.Test{} // 将 data 转换为 test 结构体 err = proto.Unmarshal(data, newTest) if err != nil { fmt.Println("转码失败:", err.Error()) return } fmt.Println(newTest.String()) // 获取字段 fmt.Println("newTest.Name: ", newTest.GetName()) fmt.Println("newTest.Age: ", newTest.GetAge()) fmt.Println("newTest.Hobbies: ", newTest.GetHobbies()) } ``` ```shell . ├── prototest │ ├── prototest.pb.go │ └── prototest.proto └── test └── test.go ``` ## 李培冠博客 [lpgit.com](https://lpgit.com) Last modification:July 31st, 2020 at 09:24 pm © 允许规范转载 Support ×Close Appreciate the author Sweeping payments Pay by AliPay Pay by WeChat