一種更緊湊的數據格式: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 第 18 期:OpenAI 發布 AI 瀏覽器 Atlas

OpenAI 最近發布 AI 瀏覽器,加上稍早的 Sora 2,在技術圈中引起一些討論。 我認為 OpenAI 嘗試將模型領域的優勢帶到應用面,但這也讓它顯得更像是一家營利公司,而非研究單位(雖然現在沒人會把 OpenAI 當成研究單位了)。 🗞️ 熱門新聞 Dane Stuckey (OpenAI CISO) on prompt injection risks for ChatGPT Atlas Simon Willison 聊了他對 OpenAI Altas 的看法,主要是資安方面。 幾個點:1) 提示詞注入問題依然存在,而且還沒有好解法;2) OpenAI 設計了登出模式與監視模式,讓使用者更容易意識到安全性。 在我看來第二點很重要,好設計應該要避免使用者犯錯,如果 AI 瀏覽器可以在登出狀態下執行,能避免掉很多麻煩的狀況,當然這意味著沒辦法自動購物。

By Ken Chen

Weekly Issue 第 17 期:n8n 在 C 輪募得 180m

現在新創企業已經離不開 AI 了。像 n8n 這樣的自動化工具,重新用 AI 話題包裝後,可以在自由市場上募得鉅款;Postman 也需要在它的口號中,強調對 AI 的重要性。 我相信 AI 讓生活變得更方便,我剛到新國家,對任何事情都不熟時,AI 給了我很多幫助。但市場的話題像一場無差別的風暴,每個公司都面對一支麥克風,麥克風傳出的經 AI 編輯過的聲音。 🗞️ 熱門新聞 n8n raises $180m to get AI closer to value with orchestration n8n C 輪募了 180M 美元,沒想到它可以這麼值錢。 基於 zapier 只有 5B 的估值,

By Ken Chen

Weekly Issue 第 16 期:Anduril 的 MVP

近期嘗試降低 AI 相關選文,主要是因為我在閱讀時,不容易判斷內容是正確還是錯誤。本次選的「AI Evals 大辯論」在這點上就做得很棒,正反意見並陳,讓讀者知道自己哪些論點也有人支持,哪些論點具有爭議。 🗞️ 熱門新聞 The Amusement Park for Engineers 原本看是 Anduril 嘀咕幾聲(我對國防工業沒興趣),但看到一半覺得太讚了,推薦所有做產品的人閱讀。 這句話開始點亮我的眼睛:「那座臨時搭建的塔,是我們自掏腰包、為了驗證可行性而做的,幫助攔截了近一千磅的大麻,並導致數十起毒品走私逮捕 」 業界都說要做 MVP,但到底什麼是 viable?沒有 viable 的 MVP 只能稱為 prototype 而已。合作的 PM 有次說的傳神:「別人要樣品屋,但我們只有沒屋頂的牆壁。」 這篇雖然沒有講到 agile,卻做到

By Ken Chen

Weekly Issue 第 15 期:Go 語言從 1.25 支援 Flight Recorder

最近安排旅遊計畫,會到 Brisbane 居住三個月,突然跟熟悉的環境分開,用陌生眼光看待周圍一切,真是個特別的體驗。 世界依然在轉動,只是用了不同速度,反映在每週週報上,是項目變少了,可是內容變長了。 🗞️ 熱門新聞 Flight Recorder in Go 1.25 Go 1.25 開始支援 Flight Recorder。 以前要抓 trace,都是要等到事發後才能抓,有沒有可能事發前抓呢?有,原理很簡單,配置一塊記憶體存放臨時的 trace,如果符合條件,輸出持久化,否則丟棄,這就是 Flight Recorder。 官方給的範例很讚,像 slow request 這類例子,常常是處理 request 時遇到問題,在沒有 Flight Recorder

By Ken Chen