Protobuf序列化工具

 

Protobuf在golang中的使用方法以及protobuf的优点说明

Protobuf在golang中的使用方法以及protobuf的优点说明

Protobuf

Protobuf(Protocol Buffer)是由Google开发的跨语言、跨平台的序列化库

  • 什么是序列化:序列化指的是将数据结构或对象转换成可以存储或传输的格式

常用的序列化方法有XML,JSON,YAML等

protobuf的安装

  • Linux版本

Github Release上下载zip格式的包,然后解压:

$ unzip -d proto protoc-3.17.3-linux-x86_64.zip

在解压后的proto/bin文件夹里面有一个可执行文件protoc,将他复制到/usr/local/bin

$ cp proto/bin/protoc /usr/local/bin/

测试一下:

$ protoc --version
libprotoc 3.17.3

如果提示找不到文件或没有权限,可以修改一下:

$ sudo chmod 755 /usr/local/bin/protoc

protobuf的用法

protobuf的使用通常是编写一个.proto文件,然后用插件生成对应语言的文件,protobuf会自动为用户自定义的类型创建注册和连接等方法,作为一个包被主程序调用

proto文件

protobuf中有两种主要的类型:message和service:

message String {
    string value = 1;
}

service HelloService {
    rpc Hello (String) returns (String);
}
  • message类似于go中的结构体,用于定义一种数据结构,它的每个成员有三个属性:type(成员类型),name(成员名)和number(二进制标识)

  • service类似于go中的接口,用于定义对外暴露的服务,每个服务都是一个函数

下面的seq.proto是一个简单的protobuf文件:

syntax = "proto3";
option go_package = "./;seq";

message String {
    string value = 1;
}

service HelloService {
    rpc Hello (String) returns (String);
}

最上面的两行中

  • syntax表示protobuf的版本

  • option用于定义一些选项,上面的option go_package = "./;seq";指定了如果生成.go文件,package名为seq

生成特定语言的文件

如果要生成特定语言的文件,需要安装插件,以go为例,需要安装protoc-gen-go:

$ go get github.com/golang/protobuf/protoc-gen-go

然后生成文件:

$ protoc --go_out=plugins=grpc:. seq.proto

--go_out制定了插件,.表示生成的文件放在当前目录,seq.proto是源文件

运行后,会在当前目录下生成一个seq.pb.go文件,为我们自定义的String和HelloService类型自动创建了一系列的序列化方法

测试

有了序列化方法,我们就可以将HelloService这个服务远程调用了

远程调用使用grpc,需要安装:

$ go get google.golang.org/grpc
  • 首先自定义一个实现了HelloService的类:
type HelloServiceImpl struct{}

// Hello是在proto文件中自定义的方法,contex参数是默认要添加的
func (p *HelloServiceImpl) Hello(ctx context.Context, args *String) (*String, error) {
	reply := &String{Value: "hello:" + args.GetValue()}
	return reply, nil
}
  • 服务端:

服务端通过在seq.pb.go中自动生成的RegisterHelloServiceServer来注册服务

func main() {
	grpcServer := grpc.NewServer()                                        // 构造一个rpc服务对象
	seq.RegisterHelloServiceServer(grpcServer, new(seq.HelloServiceImpl)) // 注册自定义的服务

	lis, err := net.Listen("tcp", ":1234")
	if err != nil {
		log.Fatal(err)
	}
	grpcServer.Serve(lis) // 在1234端口提供rpc服务
}
  • 客户端

protobuf为客户端创建了一个helloServiceClient类型(注意是小写),并具体化了Hello方法,客户端的程序将生成一个helloServiceClient作为连接的主体,并访问它的Hello方法来获得结果

func main() {
	conn, err := grpc.Dial("localhost:1234", grpc.WithInsecure()) // WithInsecure表示跳过对服务器证书的验证
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	client := seq.NewHelloServiceClient(conn)
	// 返回一个上面提到的helloServiceClient,
	// helloServiceClient的Hello方法会调用所注册的服务(HelloServiceImpl)的Hello方法,并最终返回HelloServiceImpl.Hello的结果
	reply, err := client.Hello(context.Background(), &seq.String{Value: "hello"})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(reply.GetValue())
}

这时通过客户端就可以调用服务端提供的服务

最好自己观察一下seq.pb.go的代码,就能知道protobuf都做了什么事情

protobuf的优势

RPC远程调用本质上也是要在两个节点之间交换数据的,我们当然希望传递的数据长度越小越好,这就需要尽可能地压缩数据长度,而不丢失信息

而protobuf在压缩数据方面做了很大的改进

比如在json中,数据是以键值对存储的:

{"value": "hello"}

其中的{}""等字符都是要占用空间的,而且每次传输不同的值,它的键是不变的,这也带来了很大的开销

protobuf中取消了key,直接将value拼接在一起,服务端与客户端事先约定哪些字段,以及字段的顺序,这也就是声明字段时string value = 1;这个1的作用

我们将上面例子中的String字段修改一下,注意number的顺序

message String {
    string value = 2;
    string name = 1;
}

使用protobuf和json两种序列化方法,测试序列化的结果:

func main() {
	str := &seq.String{Value: "test", Name: "chunar"}
	strP, _ := proto.Marshal(str)
	strJ, _ := json.Marshal(str)
	fmt.Printf("protobuf:\n%v\t%v\t%s\n", len(strP), strP, strP)
	fmt.Printf("json:\n%v\t%v\t%s\n", len(strJ), strJ, strJ)
}

运行一下,输出结果为:

protobuf:
14      [10 6 99 104 117 110 97 114 18 4 116 101 115 116]
chunartest
json:
32      [123 34 118 97 108 117 101 34 58 34 116 101 115 116 34 44 34 110 97 109 101 34 58 34 99 104 117 110 97 114 34 125]      {"value":"test","name":"chunar"}

可以看到,在protobuf中,第一个字符是换行符\n,然后是number=1那个字段的长度(本例中是chunar,长度为6),然后是该字段值,后面依次拼接,大大减少了不必要的字符,比json序列化的结果长度大幅减少

不仅长度减小,在运行时间方面protobuf也有很大优势,将上面的测试各运行100000次,看一下时间:

func main() {
	str := &seq.String{Value: "test", Name: "chunar"}
	start := time.Now()
	for i := 0; i < 100000; i++ {
		proto.Marshal(str)
	}
	fmt.Println("protobuf:", time.Since(start))
	start = time.Now()
	for i := 0; i < 100000; i++ {
		json.Marshal(str)
	}
	fmt.Println("json:", time.Since(start))
}

结果:

protobuf: 21.004ms
json: 55.0136ms