《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 参考文档
阅读 —
·
全站 —