目录
1、背景
最近在项目中用遇到用go和c++通过自定义TLV协议传输数据时,V里面放的是json格式的字节数组,在数据量较大的时候性能就特别的差,这时就考虑用protobuf代替json传输数据。传输数据的协议有很多:tcp、udp、unix套接字等,下文用go给出以tcp协议传输protobuf格式数据的例子。
2、命令安装
【1】protoc安装
下载地址:https://github.com/protocolbuffers/protobuf/releases,
去此地址页面直接找到Assets部分根据操作系统和架构下载对应的zip包即可,解压zip文件后将对应bin目录里的protoc命令放的自己的环境变量里面,方便全局使用。
protoc是protobuf的编译器,用于将.proto文件编译生成不同编程语言的代码。可以通过如下方式检测protoc是否安装好:
$ protoc --version
libprotoc 3.19.4
【2】protoc-gen-go安装
直接安装到GOPATH下的bin目录:go install google.golang.org/protobuf/cmd/protoc-gen-go@latest,
注意GOPATH下的bin目录要放在环境变量下。
protoc-gen-go是protoc的一个插件,用于生成Go语言代码。可以通过如下方式检测protoc-gen-go是否安装好:
$ protoc-gen-go --version
protoc-gen-go.exe v1.32.0
3、代码示例
【1】定义.proto文件
一个简单的test.proto文件,内容如下:
syntax = "proto3"; //protobuf版本
option go_package = "./test_pb"; //包名
message Msg {
string A = 1;
int32 B = 2;
}
【2】生成.go文件
可以在.proto文件所在目录定义一个makefile或脚本,当然也可以直接使用命令生成,这里定义makefile如下:
proto:
protoc --go_out=. --go_opt=paths=source_relative ./*.proto
clear:
rm -f ./*.go
执行make命令生成.go文件:
$ make proto
protoc --go_out=. --go_opt=paths=source_relative ./*.proto
命令解释:
–go_out:指定生成的Go文件输出到当前目录。
–go_opt=paths=source_relative:表示生成的Go文件会与.proto文件保持相对路径一致。
./*.proto:指定当前目录下的所有.proto文件。
执行make命令后会在当前目录产生一个名为test.pb.go的文件,内容如下:
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
// protoc v3.19.4
// source: test.proto
package test_pb
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Msg struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
A string `protobuf:"bytes,1,opt,name=A,proto3" json:"A,omitempty"`
B int32 `protobuf:"varint,2,opt,name=B,proto3" json:"B,omitempty"`
}
func (x *Msg) Reset() {
*x = Msg{}
if protoimpl.UnsafeEnabled {
mi := &file_test_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Msg) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Msg) ProtoMessage() {}
func (x *Msg) ProtoReflect() protoreflect.Message {
mi := &file_test_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Msg.ProtoReflect.Descriptor instead.
func (*Msg) Descriptor() ([]byte, []int) {
return file_test_proto_rawDescGZIP(), []int{0}
}
func (x *Msg) GetA() string {
if x != nil {
return x.A
}
return ""
}
func (x *Msg) GetB() int32 {
if x != nil {
return x.B
}
return 0
}
var File_test_proto protoreflect.FileDescriptor
var file_test_proto_rawDesc = []byte{
0x0a, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x21, 0x0a, 0x03,
0x4d, 0x73, 0x67, 0x12, 0x0c, 0x0a, 0x01, 0x41, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x01,
0x41, 0x12, 0x0c, 0x0a, 0x01, 0x42, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x42, 0x42,
0x0b, 0x5a, 0x09, 0x2e, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x33,
}
var (
file_test_proto_rawDescOnce sync.Once
file_test_proto_rawDescData = file_test_proto_rawDesc
)
func file_test_proto_rawDescGZIP() []byte {
file_test_proto_rawDescOnce.Do(func() {
file_test_proto_rawDescData = protoimpl.X.CompressGZIP(file_test_proto_rawDescData)
})
return file_test_proto_rawDescData
}
var file_test_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_test_proto_goTypes = []interface{}{
(*Msg)(nil), // 0: Msg
}
var file_test_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_test_proto_init() }
func file_test_proto_init() {
if File_test_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_test_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Msg); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_test_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_test_proto_goTypes,
DependencyIndexes: file_test_proto_depIdxs,
MessageInfos: file_test_proto_msgTypes,
}.Build()
File_test_proto = out.File
file_test_proto_rawDesc = nil
file_test_proto_goTypes = nil
file_test_proto_depIdxs = nil
}
【3】protobuf库下载
go get -u google.golang.org/protobuf/proto
【4】服务端代码
l, err := net.Listen("tcp", ":8888")
if err != nil {
logger.Error("listen error", zap.Error(err))
return
}
defer l.Close()
logger.Info("listen tcp port 8888 success")
for {
conn, err := l.Accept()
if err != nil {
logger.Error("accept error", zap.Error(err))
continue
}
go func() {
for {
buf := make([]byte, 65*1024)
n, err := conn.Read(buf)
if err != nil {
conn.Close()
logger.Error("conn read error", zap.Error(err))
break
}
//解析读到的数据
msg := test_pb.Msg{}
if err = proto.Unmarshal(buf[:n], &msg); err != nil {
logger.Error("proto unmarshal error", zap.Error(err))
continue
}
logger.Info("receive client msg", zap.String("msg", msg.String()))
}
}()
}
运行服务端代码后控制台输出如下:
[2024-10-11 11:23:14.506] | INFO | Goroutine:1 | [protobuf_demo/main.go:100] | listen tcp port 8888 success
服务端以tcp协议监听8888端口。
【5】客户端代码
conn, err := net.Dial("tcp", "127.0.0.1:8888")
if err != nil {
logger.Error("dial error", zap.Error(err))
return
}
defer conn.Close()
msg := &test_pb.Msg{}
msg.A = "aaa"
msg.B = 111
bytes, err := proto.Marshal(msg)
if err != nil {
logger.Error("proto marshal error", zap.Error(err))
return
}
if _, err = conn.Write(bytes); err != nil {
logger.Error("conn write error", zap.Error(err))
return
}
logger.Info("conn write success", zap.String("msg", msg.String()))
运行客户端代码后控制台输出如下:
[2024-10-11 11:34:01.258] | INFO | Goroutine:1 | [protobuf_demo/main.go:156] | conn write success | {"msg": "A:\"aaa\" B:111"}
此时服务端收到客户端连接,并且解析接收到的数据并打印:
[2024-10-11 11:23:14.506] | INFO | Goroutine:1 | [protobuf_demo/main.go:100] | listen tcp port 8888 success
[2024-10-11 11:34:01.258] | INFO | Goroutine:18 | [protobuf_demo/main.go:127] | receive client msg | {"msg": "A:\"aaa\" B:111"} //打印解析出来的数据
[2024-10-11 11:34:01.299] | ERROR | Goroutine:18 | [protobuf_demo/main.go:116] | conn read error | {"error": "EOF"} //客户端发送完数据程序退出就断开连接了,从已关闭的连接读取就会报错。
4、总结
上文go中操作protobuf的步骤为:定义.proto文件、编译生成.go文件、进行序列化和反序列化,以及通过网络传输数据。protobuf还用于RPC协议中,使用protobuf可以定义RPC方法和消息结构,通过protoc编译后自动生成客户端和服务端代码,大大简化PRC系统的实现。谷歌就提供了高效的RPC框架:gRPC。