Go是如何實現protobuf的編解碼的(1):原理

原文鏈接: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系列文章

參考文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章