《Protobuf 与 Go 开发实战》

《Protobuf 与 Go 开发实战》

1 简介

Protocol Buffers(protobuf)是 Google 的高效二进制序列化协议。它先用 .proto 文件定义消息结构,再用 protoc 编译出各语言的代码,比 JSON 更省空间、解析更快。

Go 生态有两套实现:

  • google.golang.org/protobuf(protobuf-go,新一代,推荐);
  • github.com/golang/protobuf(老版本)。

2 安装 protoc 编译器

以 Linux x86_64 为例:

wget https://github.com/protocolbuffers/protobuf/releases/download/v3.12.3/protoc-3.12.3-linux-x86_64.zip unzip protoc-3.12.3-linux-x86_64.zip # ./bin/protoc 就是编译器,可加入 PATH export PATH=$PATH:$PWD/bin

3 安装 Go 代码生成插件

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

4 定义消息格式(addressbook.proto)

syntax = "proto3"; package pb; import "google/protobuf/timestamp.proto"; // go_package 是最终生成的 addressbook.pb.go 所在目录, // 也是项目 import 该包时使用的包路径 option go_package = "example.com/workspace/websocket/pb"; message Person { string name = 1; int32 id = 2; // Unique ID number for this person. string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { string number = 1; PhoneType type = 2; } repeated PhoneNumber phones = 4; google.protobuf.Timestamp last_updated = 5; } // Our address book file is just one of these. message AddressBook { repeated Person people = 1; }

5 生成 Go 代码

SRC_DIR="./workspace/websocket" DST_DIR=$SRC_DIR protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/addressbook.proto

生成后的目录结构:

workspace/websocket |-- addressbook.proto |-- compile.sh `-- workspace `-- websocket `-- pb `-- addressbook.pb.go

SRC_DIR 是 .proto 文件所在目录,DST_DIR 是生成 .pb.go 的目录,通常与 SRC_DIR 相同。

6 使用 protobuf SDK 读写消息

客户端用 proto.Marshal 序列化消息、proto.Unmarshal 反序列化;示例为基于 websocket 的 echo 场景。

package main import ( "fmt" "log" "net/url" "time" "github.com/golang/protobuf/proto" "github.com/gorilla/websocket" "example.com/workspace/websocket/pb" ) const normalClose = "websocket: close 1000 (normal)" func main() { u := url.URL{Scheme: "ws", Host: "localhost:12345", Path: "/echo"} c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) if err != nil { log.Fatal("failed to dial:", err) } defer c.Close() done := make(chan struct{}) p := &pb.Person{ Id: 1234, Name: "John Doe", Email: "jdoe@example.com", Phones: []*pb.Person_PhoneNumber{ {Number: "555-4321", Type: pb.Person_HOME}, }, } // 接收消息的 goroutine var messageChan = make(chan []byte) go func() { defer close(done) for { _, m, err := c.ReadMessage() if err != nil { if err.Error() == normalClose { return } log.Println("failed to read message:", err) } messageChan <- m } }() // 发送 protobuf 序列化后的消息 out, _ := proto.Marshal(p) _ = c.WriteMessage(websocket.TextMessage, out) // 等待服务端回显 var message []byte select { case message = <-messageChan: } // 反序列化并读取字段 p = &pb.Person{} if err := proto.Unmarshal(message, p); err != nil { log.Fatalln("failed to parse address book:", err) } fmt.Printf("Name: %s\n", p.GetName()) fmt.Printf("Id: %v\n", p.GetId()) fmt.Printf("Email: %s\n", p.GetEmail()) // 通知服务端正常关闭 _ = c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) select { case <-done: log.Println("server has closed ws, so I can quit.") case <-time.After(time.Second): } }

7 常见问题

7.1 websocket close 1006 / 1000

websocket 是双向通道,任一方要结束会话时需先发送关闭帧通知对方:

  • 未发送关闭消息直接断开 -> close 1006 (abnormal closure): unexpected EOF;
  • 正确发送关闭消息 -> 服务端收到 close 1000 (normal)。

7.2 生成的 .pb.go 版本不匹配

protoc 与 protocolbuffers/protobuf-go 存在版本兼容要求,报 EnforceVersion 错误时升级/对齐两者版本。

8 参考文档

阅读 — · 全站 —
🎸 我的歌单 0 首