Go是如何实现protobuf的编解码的: 原理

Posted Go语言充电站

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go是如何实现protobuf的编解码的: 原理相关的知识,希望对你有一定的参考价值。

这是Go语言充电站的第 30 期分享。

各位朋友咱们又见面了,我是大彬,今天聊一聊Go是如何实现protobuf编解码的。

这是一篇姊妹篇文章,本编是第一篇:

1.Go是如何实现protobuf的编解码的(1): 原理2.Go是如何实现protobuf的编解码的(2): 源码

Protocol Buffers介绍

Protocol buffers缩写为protobuf,是由Google创造的一种用于序列化的标记语言,项目Github仓库:https://github.com/protocolbuffers/protobuf。

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.

Protocol buffers的多语言支持

protobuf是支持多种编程语言的,即多种编程语言的类型数据可以转换成protobuf定义的类型数据,各种语言的类型对应可以看此介绍:https://developers.google.com/protocol-buffers/docs/proto3#scalar 。

我们介绍一下protobuf对多语言的支持原理。protobuf有个程序叫protoc,它是一个编译程序,负责把proto文件编译成对应语言的文件,它已经支持了C++、C#、Java、Python,而对于Go和Dart需要安装插件才能配合生成对于语言的文件。

对于C++,protoc可以把a.proto,编译成a.pb.ha.pb.cc

对于Go,protoc需要使用插件protoc-gen-go,把a.proto,编译成a.pb.go,其中包含了定义的数据类型,它的序列化和反序列化函数等。

Go是如何实现protobuf的编解码的(1): 原理

敲黑板,对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

首先去下载页:https://github.com/protocolbuffers/protobuf/releases/tag/v3.9.1 ,下载符合你系统的protoc,本文示例版本如下:

➜ protoc-3.9.0-osx-x86_64 tree ..├── bin│ └── protoc├── include│ └── google│ └── 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 to copy the contents of the 'include' directory somewhere as well, for example into '/usr/local/include/'.

如果使用已经定义好的类型,即上面include目录*.proto文件中的类型,把include目录下文件,拷贝到/usr/local/include/

安装protoc-gen-go:

go get –u github.com/golang/protobuf/protoc-gen-go

检查安装,应该能查到这2个程序的位置:

➜ fabric git:(release-1.4) which protoc/usr/local/bin/protoc➜ fabric git:(release-1.4) which protoc-gen-go/Users/shitaibin/go/bin/protoc-gen-go

Hello world

它的目录结构如下:

➜ protobuf git:(master) tree helloworld1helloworld1├── main.go├── request.proto└── types └── request.pb.go

定义proto文件

使用proto3,定义一个Request,request.proto内容如下:

// file: request.protosyntax = "proto3";package helloworld;option go_package="./types";
message Request { string data = 1;}

syntax:protobuf版本,现在是proto3package:不完全等价于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_UnmarshalXXX_Marshal,文件内容如下:

// 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.Marshalvar _ = fmt.Errorfvar _ = 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.gopackage 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 ", req.String()) fmt.Printf("unmarshaledReq: %v ", unmarshaledReq.String())}

运行结果:

➜ helloworld1 git:(master) go run main.goreq: data:"Hello LIB"unmarshaledReq: data:"Hello LIB"

以上都是铺垫,下一节的proto包怎么实现编解码才是重点,protobuf用法可以去翻:

1.参考资料中的官方介绍2.煎鱼的gRPC系列文章:https://eddycjy.gitbook.io/golang/di-4-ke-grpc/install

点击阅读原文,直达博客原文(http://lessisbetter.site/2019/08/26/protobuf-in-go/),获得更好代码阅读体验。

如有收获请划到底部,点击“在看”,谢谢!

参考文章

https://tech.meituan.com/2015/02/26/serialization-vs-deserialization.html 《序列化和反序列化》出自美团技术团队,值得一读。https://github.com/golang/protobuf Go支持protocol buffer的仓库,Readme,值得详读。https://developers.google.com/protocol-buffers/docs/gotutorial Google Protocol Buffers的Go语言tutorial,值得详细阅读和实操。https://developers.google.com/protocol-buffers/docs/overview Google Protocol Buffers的Overview,介绍了什么是Protocol Buffers,它的原理、历史(起源),以及和XML的对比,必读。https://developers.google.com/protocol-buffers/docs/proto3 《Language Guide (proto3)》这篇文章介绍了proto3的定义,也可以理解为.proto文件的语法,就如同Go语言的语法,不懂语法怎么编写.proto文件?读这篇文章会了解很多原理,以及可以少踩坑,必读。https://developers.google.com/protocol-buffers/docs/reference/go-generated 《Go Generated Code》这篇文章详细介绍了protoc是怎么用.protoc生成.pb.go的,可选。https://developers.google.com/protocol-buffers/docs/encoding# 《Protocol Buffers Encoding》这篇介绍编码原理,可选。https://godoc.org/github.com/golang/protobuf/proto 《package proto文档》可以把proto包当做Go语言操作protobuf数据的SDK,它实现了结构体和protobuf数据的转换,它和.pb.go文件配合使用。


以上是关于Go是如何实现protobuf的编解码的: 原理的主要内容,如果未能解决你的问题,请参考以下文章

Netty使用Google Protobuf实现编解码

go protobuf 编码与解码

Protobuf协议实现原理

Protobuf如何提升编码效率

Netty-整合Protobuf高性能数据传输

Go 每日一库之 jsonrpc:来自标准库