一種更緊湊的數據格式:Protobuf 入門

Protocol buffers,簡稱 Protobuf,是由 Google 設計的結構化序列化資料技術。對網路應用來講,常常需要在不同的伺服端、客戶端之間交換資料,這些資料格式有 XML、JSON 等等之類。XML 特點是完整,便於記載更多的 meta data,但格式複雜,需要更強的效能來解析,傳輸時也需要更多頻寬;而 JSON 是 JavaScript 用於表示物件的語法,相對 XML 來得簡潔,隨著 JavaScript 普及,也變成現在常用的輕量級資料交換格式。

儘管 XML 跟 JSON 在當前網路應用中已經相當常見,但用不同語言開發的伺服端程式,都需要分別實現自己語言的 XML 或 JSON 解析,同時,這些格式定義也是團隊協作的痛點。假設 A 團隊開發某支應用,因為開發時沒有定義文件,只用 Email 跟 B 團隊說明,如果說明得不夠清楚,就會導致 B 團隊在交換資料時格式錯誤,需要好幾個來回,才能釐清彼此應該實現的格式。

Protobuf 對這些問題有它的看法,本文會使用 Protobuf 來序列化資料,搭配 Golang 寫個簡單的讀檔寫檔程式,體會一下 Protobuf 的設計特點。

Prepare Environment

Prorobuf 使用前,需要依據格式定義文件 proto file 來編譯訊息,編譯用工具是 protoc,可以到官網下載,或使用

choco install protoc

來安裝,安裝後輸入

PS C:\Users\ken> protoc --version
libprotoc 3.12.3

來確認版本。版本號很重要,跟 Google 其他的工具一樣,不同版本間可能有相容問題,使用時盡量依照語意化版本的方式選用對應版本。

Protobuf 用於結構化訊息的方法,是先將格式定義好,再用定義好的格式來產生序列化訊息,可以參考官方說明

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.

因為包含 Code Generator,Protobuf 可以相容多種語言,常見的 C++、C#、Java 等等都在支援中。

參照 Golang 的專案目錄,建個 pb 來放置 proto file

project
├── cmd
├── pkg
├── scripts
|   └── build_win.bat
├── pb
|   └── person.proto
|── go.mod
└── README.md

檔案內容是

syntax = "proto3";

package person;

message Person {
    string name = 1;
    string address = 2;
    string gender = 3;
    int32 age = 4;
}

syntax 是版本,用於跟 proto2 區別;package 是這份 proto file 所屬的套件名稱;message 用於定義訊息結構。假設訊息結構是 Person,其中包含姓名、地址、性別、年齡等欄位,用於表示 Person 的資料。各欄位等號後數字是用於區別欄位,如果開發中需要新增欄位,可以直接在 field 中使用新數字,即可無痛向後相容。

Use Protobuf Command Line Tool

材料準備好後,來試著用 protoc 序列化訊息。前面已經定義好的格式了,這邊將需要傳輸的內容寫在 sample.txt

name: "Ken Chen"
address: "New Taipei City"
gender: "Male"
age: 18

引號後面是 proto 欄位的值,格式類似 JSON,可以參考官方說明。

然後用 protoc 來序列化訊息

protoc --encode person.Person ./pb/person.proto > encode.txt < sample.txt
type encode.txt

Ken ChenNew Taipei CityMale

最底下那串是序列化後的訊息,可以看到 string 還留著,但 field 跟 number 都被序列化成 byte 訊息了。

這些序列化完成的訊息,可以再用 protoc 反序列化回來,變成人眼可讀

protoc --decode person.Person ./pb/person.proto < encode.txt
name: "Ken Chen"
address: "New Taipei City"
gender: "Male"
age: 18

我們將傳輸內容序列化、寫進檔案、讀出、反序列化,這就是個簡單的傳輸過程。

Prepare Golang Environment

驗證 Protobuf 的使用流程後,接著要將 Protobuf 的機制放到自行開發的應用程式中。

先下載 Golang 的 Protobuf 套件

go get google.golang.org/protobuf/cmd/protoc-gen-go

修改 proto file,讓它帶有 Golang Package 的資訊

syntax = "proto3";

option go_package = "example/pkg/pb/person";

package person;

message Person {
    string name = 1;
    string address = 2;
    string gender = 3;
    int32 age = 4;
}

使用 protoc 中的 Code Generator 來產生 go 的程式碼

protoc --go_out=./ --go_opt=paths=source_relative ./pb/*.proto

go_out 是產出文件的路徑;我們將 pb 的所有 proto file 都產生一份對應的 go 程式碼

可以看看產出文件 person.pb.go 的開頭

// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
//  protoc-gen-go v1.25.0
//  protoc        v3.12.3
// source: pb/person.proto

package person

import (
    proto "github.com/golang/protobuf/proto"
    protoreflect "google.golang.org/protobuf/reflect/protoreflect"
    protoimpl "google.golang.org/protobuf/runtime/protoimpl"
    reflect "reflect"
    sync "sync"
)

如第一行寫的,這是產出文件,看看就好,不要修改它。如果有一些客製化應用,希望在既有的 Protobuf struct 加上自己的 method,建議可以用 go package 的機制,在同目錄底下放 patch.go 文件來新增。

把產出的文件放到專案路徑 pkg,變成

project
├── cmd
├── pkg
|   └── person
|      └── person.pb.go
├── scripts
|   └── build_win.bat
├── pb
|   └── person.proto
|── go.mod
└── README.md

Read/Write Data in Golang

可以來寫點應用了,在 cmd 下新增 go 程式碼

project
├── cmd
|   └── proto
|      └── main.go
├── pkg
|   └── person
|      └── person.pb.go
├── scripts
|   └── build_win.bat
├── pb
|   └── person.proto
|── go.mod
└── README.md

內容是

package main

import (
    "example/pkg/person"
    "fmt"
    "io/ioutil"

    "github.com/golang/protobuf/proto"
)

func main() {
    data, _ := ioutil.ReadFile("encode.txt")
    msg := person.Person{}
    err := proto.Unmarshal(data, &msg)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(msg.Name, msg.Gender, msg.Address, msg.Age)
}

由於前面已經用 protoc 產生序列化後的檔案 encode.txt,這裡可以讀取該檔來驗證程式。example/pkg/person 是引入剛剛產出的 go 套件。使用 ioutil 讀檔,再用 proto.Umarshal 反序列化成 struct,最後印出。

編譯並執行,看看結果

.\bin\proto.exe
Ken Chen Male New Taipei City 18

可以看到欄位值跟 sample.txt 相同。

接著來寫檔,修改 main.go

func main() {
    // write file
    msgWrite := person.Person{}
    msgWrite.Age = 22
    msgWrite.Gender = "Female"
    msgWrite.Name = "Cythia"
    msgWrite.Address = "Boston ,US"
    dataWrite, err := proto.Marshal(&msgWrite)
    if err != nil {
        fmt.Println(err)
    }
    err = ioutil.WriteFile("encode.txt", dataWrite, os.ModePerm)
    if err != nil {
        fmt.Println(err)
    }

    // read file
    data, _ := ioutil.ReadFile("encode.txt")
    msg := person.Person{}
    err = proto.Unmarshal(data, &msg)
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(msg.Name)
    fmt.Println(msg.Gender)
    fmt.Println(msg.Address)
    fmt.Println(msg.Age)
}

寫檔的順序跟讀檔相反,先建立 Protobuf struct,填入各欄位,用 proto.Marshal 序列化,再用 ioutil.WriteFile 將序列化後的資訊寫進檔案中。寫入檔案同樣是 encode.txt,底下再用讀檔將內容讀出。

編譯並執行,觀察結果

.\bin\proto.exe
Cythia
Female
Boston ,US
22

檔案內容由 Ken 改為 Cythia,寫入成功。

Compare with JSON

既然都有現成的資料了,可以再跟 JSON 比較,看看兩者序列化後的大小差多少。

由於 Protobuf 相容 JSON 的格式,這件事變得很容易,可以看 person.pb.go 的內容

type Person struct {
    state         protoimpl.MessageState
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Name    string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
    Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"`
    Gender  string `protobuf:"bytes,3,opt,name=gender,proto3" json:"gender,omitempty"`
    Age     int32  `protobuf:"varint,4,opt,name=age,proto3" json:"age,omitempty"`
}

在 Person 這個 struct 中,Name、Address、Gender、Age 等欄位有同時打上 protobuf 跟 json 兩種 tag,因此要轉換 struct 為 json,只需要直接使用 Golang 標準庫中的 json 庫即可。

修改 main.go

// write json
dataJSON, err := json.Marshal(&msgWrite)
if err != nil {
    fmt.Println(err)
}
err = ioutil.WriteFile("encode_json.txt", dataJSON, os.ModePerm)
if err != nil {
    fmt.Println(err)
}

執行後可以看到多出 encode_json.txt 這個檔案,內容是

{"name":"Cythia","address":"Boston ,US","gender":"Female","age":22}

來比較 Protobuf 跟 JSON 序列化後的差異,用

dir *.txt /s

得到

2020/06/26  下午 02:42                30 encode.txt
2020/06/26  下午 02:42                67 encode_json.txt

Protobuf 只要 30 bytes,而 JSON 需要 67 bytes,相差一倍多,難怪 Google 宣稱它又小又快。

小結

Protobuf 對網路應用來講,最主要的優點就是體積更小,傳輸更快,在高密度的資料交換場景,例如微服務組成的大型應用中,Protobuf 能有效提高傳輸速度。讓人訝異的是,根據這篇文章,Protobuf 在解析使用的資源居然還低於 JSON,真要說不方便的地方,大約是人眼不可讀這點。

除了傳輸與效能上的特點,Protobuf 將資料格式文件化,無意中也防止兩個不同的應用程序無法交換,對多人協作來講有其優勢。麻煩的地方可能是 proto file 需要額外版控,而 Git 目前的子版本版控技術用起來不是很方便,無論 submodule 或 subtree 都有限制,以 subtree 來講,會需要開發者先到 proto file 的 Repo 中修改檔案,加入並提交後,再到專案開發資料夾用 subtree 拉下來。可以預期在開發初期,proto file 會頻繁變更,衍生出許多隱形的開發成本。

Google 怎麼處理這問題呢?他們不用處理,因為 Google 的版控 Policy 是 Monorepo,所有的程式碼都放在一起,不需要做子版本版控。這好像是某種康威定律的佐證。

Reference

Read more

Weekly Issue 第 2 期:Linux 基金會啟動 FAIR 專案

有些產品看到會覺得行不通,有些產品則相反,只要聽到就覺得是個好主意。Sentry 的產品通常都是後者。我猜有部分,也是因為它們的產品都指向同一個使命:可除錯性。 🗞️ 熱門新聞 Linux Foundation Announces the FAIR Package Manager Project for Open Source Content Management System Stability Linux 基金會啟動 FAIR 專案,為 WordPress 外掛程式提供替代方案。 底下的 Supporting Quotes 可以看看,講話都很客氣,左一句「去中心化」右一句「透明的治理架構」,在講什麼大家都很清楚 😜 。 Uber 與 Airbnb 重塑 VC 玩法,一文看懂 a16z 創辦人

By Ken Chen

Weekly Issue 第 1 期:Stack Overflow 流量大跌

來自阮一峰老師的靈感與嘗試,我會在 Weekly Issue 中記錄每周值得分享的科技內容,周一發刊。多數內容都有刊在我的 X、Threads 或 Facebook 中,你可以追蹤上述社群媒體得到最新消息。這裡的性質更接近單周回顧與歷史歸檔。 🗞️ 熱門新聞 The Pulse #134: Stack overflow is almost dead StackOverflow 的情況比我想的還糟,退化到剛成立三個月的狀況?太要命了。 我自己好奇的是 2020 年的衰退如何引起?平台治理的問題嗎? Cloudflare service outage June 12, 2025 Cloudflare 近幾次中斷事故都有出報告,內容包括背景跟時間軸,還有改善方式,這是很正確也很重要的實踐。 我也曾經遇過幾次重要的服務停機事件,當時都會盡可能擠出時間即時更新 + 出報告。後來服務也的確越來越穩。這種問題很多都是文化層面的問題。 Ask HN: How

By Ken Chen
自訂網域很難嗎?DNS 的限制與實踐

自訂網域很難嗎?DNS 的限制與實踐

自訂網域(Custom Domain)是 SaaS 常見的服務,只是我通常都沒花錢買。某次跟朋友聊天,她想聽聽我對內容平台的觀點,嘰哩呱啦分析完一堆後,我最後建議她,最好還是買個網域: 「你想想看,妳現在投入這麼多心力在經營內容,建立自己的品牌形象。如果妳的網址永遠都掛在別人的平台底下,就像在別人家租房子,雖然方便,但終究不是自己的。」 「有了自訂網域,妳的品牌就是自己的,無論未來平台怎麼變,妳的讀者都能透過固定的網址找到妳,這對品牌來說很重要。就算未來妳想換平台,也不會流失妳辛苦建立起來的流量。」 後來我在 Ghost 官方頁面看到類似說法 If you would like to make your site memorable and easy to find with a branded custom domain, then you can

By Ken Chen