protobuf简单使用

protobuf简单使用

本教程提供了一个基本的 Go 程序员介绍使用协议缓冲区,使用协议缓冲区语言的proto3版本。通过创建一个简单的示例应用程序,它向您展示了如何

  • 在文件中定义消息格式.proto
  • 使用协议缓冲区编译器。
  • 使用 Go 协议缓冲区 API 来写入和读取消息。

这不是在 Go 中使用协议缓冲区的综合指南。有关更详细的参考信息,请参阅协议缓冲区语言指南Go API 参考Go 生成代码指南编码参考

一、为什么使用协议缓冲区?

我们将要使用的示例是一个非常简单的“地址簿”应用程序,它可以在文件中读取和写入人们的联系方式。地址簿中的每个人都有一个姓名、一个 ID、一个电子邮件地址和一个联系电话号码。

你如何序列化和检索这样的结构化数据?有几种方法可以解决这个问题:

  • 使用gobs序列化 Go 数据结构。这在特定于 Go 的环境中是一个很好的解决方案,但是如果您需要与为其他平台编写的应用程序共享数据,它就不能很好地工作。
  • 您可以发明一种特殊方式将数据项编码为单个字符串——例如将 4 个整数编码为“12:3:-23:67”。这是一种简单而灵活的方法,尽管它确实需要编写一次性的编码和解析代码,并且解析会产生很小的运行时成本。这最适合编码非常简单的数据。
  • 将数据序列化为 XML。这种方法可能非常有吸引力,因为 XML(某种程度)是人类可读的,并且有许多语言的绑定库。如果您想与其他应用程序/项目共享数据,这可能是一个不错的选择。然而,众所周知,XML 是空间密集型的,对它进行编码/解码会对应用程序造成巨大的性能损失。此外,导航 XML DOM 树比导航类中的简单字段通常要复杂得多。

协议缓冲区是解决这个问题的灵活、高效、自动化的解决方案。使用协议缓冲区,您可以编写.proto要存储的数据结构的描述。由此,protocol buffer 编译器创建了一个类,该类以高效的二进制格式实现 protocol buffer 数据的自动编码和解析。生成的类为组成协议缓冲区的字段提供 getter 和 setter,并将读取和写入协议缓冲区的细节作为一个单元处理。重要的是,协议缓冲区格式支持随着时间的推移扩展格式的想法,这样代码仍然可以读取用旧格式编码的数据。

二、在哪里可以找到示例代码

我们的示例是一组用于管理地址簿数据文件的命令行应用程序,使用协议缓冲区进行编码。该命令add_person_go将新条目添加到数据文件中。该命令list_people_go解析数据文件并将数据打印到控制台。

您可以在 GitHub 存储库 的示例目录中找到完整的示例。

三、定义的协议格式

要创建地址簿应用程序,您需要从 .proto文件开始。文件中的定义.proto很简单:为要序列化的每个数据结构添加一条消息,然后为消息中的每个字段指定名称和类型。在我们的示例中,.proto定义消息的文件是 addressbook.proto.

.proto文件以包声明开头,这有助于防止不同项目之间的命名冲突。

syntax = "proto3";
package proto;

go_package选项定义了包的导入路径,该路径将包含此文件的所有生成代码。Go 包名称将是导入路径的最后一个路径组件。例如,我们的示例将使用包名“proto”。

//option go_package = "path;name";
//path 表示生成的go文件的存放地址,会自动生成目录的。
//name 表示生成的go文件所属的包名

//option go_package = "./;/proto"; // 官方不建议这样操作
option go_package = ""go-grpc-example/01helloproto/proto";

接下来,您有您的消息定义。消息只是包含一组类型字段的聚合。许多标准的简单数据类型可用作字段类型,包括boolint32floatdoublestring。您还可以通过使用其他消息类型作为字段类型来为您的消息添加更多结构。

syntax = "proto3";

//option go_package = "path;name";
//path 表示生成的go文件的存放地址,会自动生成目录的。
//name 表示生成的go文件所属的包名
option go_package="./;proto";
// 定义包名 
package proto;

// 生成pb.go命令: protoc -I .\proto\ --go_out=plugins=grpc:proto .\proto\addressbook.proto
message Person {
  string name = 1;
  int32 id = 2;  // Unique ID number for this person.
  string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;

  string last_updated = 5;
}

// Our address book file is just one of these.
message AddressBook {
  repeated Person people = 1;
}

在上面的示例中,Person消息包含 PhoneNumber消息,而AddressBook消息包含Person消息。您甚至可以定义嵌套在其他消息中的消息类型——如您所见, PhoneNumber类型是在内部定义的Person。如果您希望其中一个字段具有预定义的值列表之一,您还可以定义enum类型 - 在这里您要指定电话号码可以是MOBILEHOME或 之一WORK

每个元素上的“= 1”、“= 2”标记标识该字段在二进制编码中使用的唯一“标签”。标签编号 1-15 比更高的编号需要少一个字节来编码,因此作为一种优化,您可以决定将这些标签用于常用或重复的元素,而将标签 16 和更高的标签用于不太常用的可选元素。重复字段中的每个元素都需要重新编码标签号,因此重复字段特别适合这种优化。

如果未设置字段值, 则使用默认值:数字类型为零,字符串为空字符串,布尔值为 false。对于嵌入式消息,默认值始终是消息的“默认实例”或“原型”,没有设置任何字段。调用访问器以获取尚未显式设置的字段的值始终返回该字段的默认值。

如果字段是repeated,则该字段可以重复任意次数(包括零次)。重复值的顺序将保存在协议缓冲区中。将重复字段视为动态大小的数组。

您可以在Protocol Buffer Language Guide.proto中找到编写文件的完整指南——包括所有可能的字段类型。不过,不要去寻找类似于类继承的工具——协议缓冲区不会那样做。

三、安装gRPC和protoc-gen-go

安装gRPC:

go get -u google.golang.org/grpc

安装: protoc-gen-go:

go install  github.com/golang/protobuf/protoc-gen-go

四、生成协议缓冲区 API

addressbook.proto文件

syntax = "proto3";

// 定义包名
package proto;
//option go_package = "path;name";
//path 表示生成的go文件的存放地址,会自动生成目录的。
//name 表示生成的go文件所属的包名
//option go_package = "go-grpc-example/01helloproto/proto";
option go_package = "./;proto";

//  生成pb.go命令: protoc -I ./ --go_out=plugins=grpc:.\01helloproto\proto\ .\01helloproto\proto\addressbook.proto



message Person {
  string name = 1;
  int32 id = 2;  // Unique ID number for this person.
  string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;

  string last_updated = 5;
}

// Our address book file is just one of these.
message AddressBook {
  repeated Person people = 1;
}

执行命令生成addressbook.pb.go:

go-grpc-example> protoc -I ./ --go_out=plugins=grpc:.\01helloproto\proto\ .\01helloproto\proto\addressbook.proto

image-20220504173502407

目录结构


├─go-grpc-example
│  ├─proto
│  

image-20220504173531627

四、实例化API

4.1实例化对象

// person_test.go
package main

package helloproto

import (
	pb "go-grpc-example/01helloproto/proto"
	"testing"
)

/*
@author RandySun
@create 2022-05-04-17:44
*/

//
//  TestPerson
//  @Description: 实例化对象
//  @param t
//
func TestPerson(t *testing.T) {
	p := pb.Person{
		Id:    1234,
		Name:  "John Doe",
		Email: "jdoe@example.com",
		Phones: []*pb.Person_PhoneNumber{
			{Number: "555-4321", Type: pb.Person_HOME},
		},
	}
	t.Logf("id: %d name: %s", p.Id, p.Name)
}

image-20220504181036896

4.2写信息

使用协议缓冲区的全部目的是序列化您的数据,以便可以在其他地方对其进行解析。在 Go 中,您使用proto库的Marshal函数来序列化您的协议缓冲区数据。指向协议缓冲区消息的指针struct实现了proto.Message接口。调用proto.Marshal返回协议缓冲区,以有线格式编码。例如,我们在add_person命令中使用这个函数:

// person_test.go
package helloproto

import (
	pb "go-grpc-example/01helloproto/proto"
	"io/ioutil"
	"log"
	"testing"

	"google.golang.org/protobuf/proto"
)

/*
@author RandySun
@create 2022-05-04-17:44
*/

//
//  TestWritePerson
//  @Description: 写入文件
//  @param t
//
func TestWritePerson(t *testing.T) {
	book := &pb.AddressBook{
		People: []*pb.Person{
			{
				Id:    1234,
				Name:  "hello word !!",
				Email: "randy@example.com",
				Phones: []*pb.Person_PhoneNumber{
					{Number: "555-4321", Type: pb.Person_HOME},
				},
			},
		},
	}

	// Write the new address book back to disk.
	out, err := proto.Marshal(book)
	if err != nil {
		log.Fatalln("Failed to encode address book:", err)
	}
	// 将person写入grpc_file文件中
	if err := ioutil.WriteFile("grpc_file", out, 0644); err != nil {
		log.Fatalln("Failed to write address book:", err)
	}

}

image-20220504181349684

4.3阅读信息

要解析编码消息,请使用proto库的Unmarshal函数。调用它会将数据解析in为协议缓冲区并将结果放入book. list_people因此,要在命令中解析文件,我们使用:

package helloproto

import (
	pb "go-grpc-example/01helloproto/proto"
	"io/ioutil"
	"log"
	"testing"

	"google.golang.org/protobuf/proto"
)

/*
@author RandySun
@create 2022-05-04-17:44
*/


// person_test.go
//
//  TestReadPerson
//  @Description: 从grpc_file文件中读取person信息
//  @param t
//
func TestReadPerson(t *testing.T) {
	in, err := ioutil.ReadFile("grpc_file")
	if err != nil {
		log.Fatalln("Error reading file:", err)
	}
	book := &pb.AddressBook{}
	if err := proto.Unmarshal(in, book); err != nil {
		log.Fatalln("Failed to parse address book:", err)
	}
	t.Log(book.People[0])

}

image-20220504181729502

posted @ 2022-05-15 18:18  RandySun  阅读(188)  评论(0编辑  收藏  举报