一種更緊湊的數據格式: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 第 27 期:Nvidia 收購 Groq

從 Windsurf 的案件後,我一直在想,新創成立時,支持創辦人的早期員工與投資人,是不是能得到合理的報酬?Groq 收購案給出很多細節,告訴我們即使是 Acquhire,也還是能盡可能公平。 改變世界很重要,但使用的方式也很重要。 🗞️ 熱門新聞 Nvidia deal a big win for Groq employees and investors Groq 收購案的消息出來後,我關心的事情是:這跟 Windsurf 的案子有什麼不同?早期投資人跟團隊有得到回報嗎? 先講結論:如果消息正確,該得到的報酬都有得到,皆大歡喜。但我覺得 Acquhire 存在太多灰色地帶,應該要納入監管,不是每家收購發起者跟創辦人都有同樣的素質。 按照 Axios 的說法,輝達付出的 200 億將被視為估值分配給股票持有人,90% 員工會加入輝達,到期股票會變現,

By Ken Chen

Weekly Issue 第 26 期:AI 批評指南

最近在讀《高效槓桿力》,書中提出一套變革管理框架:「尋找關鍵支點,重新配置資源。」當然,書裡給出很多案例,說明如何找到支點,只是我同時在想,如何將他們帶到我面對的情境呢? ✨ 科技觀點 Pluralistic: The Reverse-Centaur’s Guide to Criticizing AI 看到有人非常認真討論事情,即使是批評 AI,都會讓我有興趣。 附上一些我的觀點: 1) 成長型公司聽起來很美好,每個人都會想待在那,但當它變成前提時就是另一回事了。很多決策都會以成長為基礎,最後就是投資人跟企業都沒辦法接受不成長的代價。 2) 常常在爭論 AI 是否會取代工作,看的是 AI 的兩個面向,賦能與自動化,哪個會更符合當前情境。贊同賦能的人會認為 AI 帶來生產力的解放,並創造價值,可是實際上呢? 3) 很多人提過 AI 的解壓縮 / 壓縮特性,特別是在履歷或信件應用。

By Ken Chen

Weekly Issue 第 25 期:Slack 基礎設施爭議

因為地緣政治議題,我們會關心資料存放的地點是否足夠安全,即使當使用者被盯上,他仍然可以放心資料足夠隱密。這也是為什麼當網路上傳出 Slack 台灣的資料轉移到阿里雲時,會引起爭議的原因。 Slack 已經出面澄清並無此事,這也讓我們反思,當軟體業面臨這類公關危機時,應該要揭露到什麼程度。 🗞️ 熱門新聞 Slack 在臺服務將移轉至中國? Salesforce:臺灣用戶使用全球基礎設施,與阿里巴巴無關 前幾天 Salesforce 傳出要將 Slack 台灣資料轉移到阿里雲,立刻引起一陣討論,有 Salesforce 的人出來澄清,說沒有這回事。 「台灣市場一直以來都是採用 Global Infrastructure 全球基礎設施。簡單說,台灣用戶的資料是儲存在美洲或亞太區(如日本),跟中國的阿里雲在物理和邏輯上都是完全切開的。 」 讓我有興趣的是,Salesforce 沒有說他們是用哪個雲平台。我們以前有次遇到類似情況,也討論到是否揭露使用平台。當時我持反對意見,認為只需要揭露「使用全球基礎設施」已經夠了,頂多說非中國廠商的服務就好,不需要也不應該說明具體是哪個。

By Ken Chen

Weekly Issue 第 24 期:網路的精神高地

前陣子去了雪梨一趟,跟布里斯本或台北都形成有趣的對比,旅行中也不斷在想,一座城市如何發展出自己的文化?這有點像是網路平台如何形成聚落,而又如何消亡。 很喜歡本期談知乎的一篇文章,理想主義的光輝是最吸引人的,我常在想,有沒有辦法將那座「看不見的城市」帶到真實世界中。 🗞️ 熱門新聞 A ChatGPT prompt equals about 5.1 seconds of Netflix 看到 Simon Willison 提到,如果 Sam Altman 的資訊是對的,每個 LLM 提問相當於 5.1s 的 Netflix 影片耗能。 計算的需求讓輝達跟台積電挖到金礦,那電力需求又會讓誰挖到金礦呢? ✨ 科技觀點 我们失去的不只是知乎,而是中文互联网的精神高地 「那时的知乎,更像“思想沙龙”,而非“内容平台”。」 昨天跟朋友聊天,

By Ken Chen