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

56次阅读

共计 6666 个字符,预计需要花费 17 分钟才能阅读完成。

原文链接:https://mp.weixin.qq.com/s/O8…

这是一篇姊妹篇文章,浅析一下 Go 是如何实现 protobuf 编解码的:

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

本编是第一篇。

Protocol Buffers 介绍

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

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 定义的类型数据,各种语言的类型对应可以看此介绍。

我们介绍一下 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 语言,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

首先去下载页下载符合你系统的 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

创建了一个使用 protoc 的小玩具,项目地址 Github: golang_step_by_step。

它的目录结构如下:

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

定义 proto 文件

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

// file: request.proto
syntax = "proto3";
package helloworld;
option go_package="./types";

message Request {string data = 1;}
  • syntax:protobuf 版本,现在是 proto3
  • package:不完全等价于 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.Marshal
var _ = fmt.Errorf
var _ = 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.go
package 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\n", req.String())
    fmt.Printf("unmarshaledReq: %v\n", unmarshaledReq.String())
}

运行结果:

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

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

  1. 官方介绍:protoc3 介绍,编码介绍,Go 教程
  2. 煎鱼 grpc 系列文章

参考文章

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

正文完
 0