ProtoBuf编解码
简介:
Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,并于2008年对外开源。Protobuf刚开源时的定位类似于XML、JSON等数据描述语言,通过附带工具生成代码并实现将结构化数据序列化的功能。但更关注的是Protobuf作为接口规范的描述语言,可以作为设计安全的跨语言PRC接口的基础工具
为什么选择Protobuf
一般而言需要一种编解码工具会参考:
- 编解码效率
- 高压缩比
- 多语言支持
其中压缩与效率 最被关注的点:
安装编译器
protobuf的编译器叫: protoc(protobuf compiler), 需要到这里下载编译器: Github Protobuf
这个压缩包里面有:
- include, 头文件或者库文件
- bin, protoc编译器
- readme.txt, 一定要看,按照这个来进行安装
安装编译器二进制
linux/unix系统直接:
mv bin/protoc usr/bin
windows系统:
注意: Windows 上的 git-bash 上默认的 /usr/bin 目录在:C:\Program Files\Git\usr\bin\
因此首先将bin下的 protoc 编译器放到C:\Program Files\Git\usr\bin\
安装编译器库
include 库文件需要放到: /usr/local/include/,如果没有include,手动创建即可
linux/unix系统直接:
mv include/google /usr/local/include
windows系统:
C:\Program Files\Git\usr\local\include
验证安装
C:\Users\snow>protoc --version
libprotoc 3.19.1
使用流程
首先需要定义数据,通过编译器,来生成不同语言的代码
创建hello.proto文件,其中包装HelloService服务中用到的字符串类型
syntax = "proto3";
package hello;
option go_package = "github.com/ProtoDemo/pb";
message String {
string value = 1 ;
}
- syntax:表示采用proto3的语法。第三版的Protobuf对语言进行了提炼简化,所有成员均采用类似Go语言中的零值初始化(不再支持自定义默认值),因此消息成员不再需要支持required特性。
- package:指明当前是main包(这样可以和Go的包名保持一致,简化例子代码),当然用户也可以针对不同的语言定制对应的包路径和名称。
- option:protobuf的一些选项参数,这里指定的是要生成的Go语言package路径,其他语言参数各不相同。
- message:关键字定义一个新的String类型,在最终生成的Go语言代码中对应一个String结构体。String类型中只有一个字符串类型的value成员,该成员的编码时用1编号代替名字。
关于数据编码:
在xml或json等数据描述语言中,一般通过成员的名字来绑定对应的数据。但是Protobuf编码是通过成员的唯一编号来绑定对应的数据,因此Protobuf编码后数据的体积会较小,但也非常不便于人类查阅。目前并不关注protobuf的编码技术,最终生成的Go结构体可以自由采用JSON或gob等编码格式,因此可以暂时忽略protobuf的成员编码部分。
但如何把这个定义文件(IDL:接口描述语言),编译成不同语言的数据结构,需要安装protobuf的编码器
安装Go语言插件
Protobuf核心的工具集是C++语言开发的,在官方的protoc编译器中并不支持Go语言。要想基于上面的hello.proto文件生成相应的Go代码,需要安装相应的插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
接下来就可以使用protoc来生成对于Go语言的数据结构
Hello Protobuf
编译hello.proto文件
protoc -I=./ --go_out=./pb --go_opt=module="github.com/ProtoDemo/pb" pb/hello.proto
- -I:-IPATH, --proto_path=PATH, 指定proto文件搜索的路径, 如果有多个路径 可以多次使用-I 来指定, 如果不指定默认为当前目录
- --go_out: --go指插件的名称, 安装的插件为: protoc-gen-go, 而protoc-gen是插件命名规范, go是插件名称, 因此这里是--go, 而--go_out 表示的是 go插件的 out参数, 这里指编译产物的存放目录
- --go_opt: protoc-gen-go插件opt参数, 这里的module指定了go module, 生成的go pkg 会去除掉module路径,生成对应pkg
- pb/hello.proto:proto文件路径
这样就在当前目录下生成了Go语言对应的pkg, message String 被生成为了一个Go Struct
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.27.1
// protoc v3.19.1
// source: pb/hello.proto
package 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 String struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Value string `protobuf:"bytes,1,opt,name=value,proto3" json:"value,omitempty"`
}
然后就可以以Go语言的方式使用这个pkg
序列化与反序列化
基于上面生成的Go 数据结构, 就可以来进行 数据的交互了(序列化与反序列化)
使用google.golang.org/protobuf/proto工具提供的API来进行序列化与反序列化:
- Marshal: 序列化
- Unmarshal: 反序列化
模拟一个 客户端 ---> 服务端 基于protobuf的数据交互过程
package main
import (
"fmt"
"github.com/ProtoDemo/pb"
"google.golang.org/protobuf/proto"
)
func main() {
clientObj:= &pb.String{Value: "Hello Proto3"}
//序列化
out,err := proto.Marshal(clientObj)
if err != nil {
fmt.Println("Failed to encode obj:",err)
}
//二进制编码
fmt.Println("encode bytes :",out)
//反序列化
serverObj := &pb.String{}
err = proto.Unmarshal(out, serverObj)
if err != nil {
fmt.Println("Failed to decode",err)
}
fmt.Println("decode obj:",serverObj)
}
>>>>>>>>>output
encode bytes : [10 12 72 101 108 108 111 32 80 114 111 116 111 51]
decode obj: value:"Hello Proto3"
基于protobuf的RPC
接下来改造之前的rpc: Protobuf ON TCP
新建一个目录: pbrpc
定义交互数据结构
pbrpc/service/service.proto
syntax = "proto3";
package hello;
option go_package="gitee.com/infraboard/go-course/day21/pbrpc/service";
message Request {
string value = 1;
}
message Response {
string value = 1;
}
生成Go语言数据结构
## 当前目录pbrpc
$ protoc -I=./ --go_out=./service --go_opt=module="github.com/ProtoDemo/pbrpc/service" service/service.proto
定义接口
基于生成的数据结构,定义接口 pbrpc/service/interface.go
package service
const HelloServiceName = "HelloService"
type HelloService interface {
Hello(*Request,*Response) error
}
服务端
pbrpc/server/main.go
package main
import (
"fmt"
"net"
"net/rpc"
"net/rpc/jsonrpc"
"github.com/ProtoDemo/pbrpc/service"
)
// 通过接口约束HelloService服务
var _ service.HelloService = (*HelloService)(nil)
type HelloService struct{}
// Hello的逻辑 就是 将对方发送的消息前面添加一个Hello 然后返还给对方
// 由于是一个rpc服务, 因此参数上面还是有约束:
// 第一个参数是请求
// 第二个参数是响应
// 可以类比Http handler
func (p *HelloService) Hello(req *service.Request, resp *service.Response) error {
resp.Value = "hello:" + req.Value
return nil
}
func main() {
// 把对象注册成一个rpc的 receiver
// 其中rpc.Register函数调用会将对象类型中所有满足RPC规则的对象方法注册为RPC函数,
// 所有注册的方法会放在“HelloService”服务空间之下
rpc.RegisterName(service.HelloServiceName, new(HelloService))
// 然后建立一个唯一的TCP链接,
listener, err := net.Listen("tcp", ":1234")
if err != nil {
fmt.Println("ListenTCP error:", err)
}
// 通过rpc.ServeConn函数在该TCP链接上为对方提供RPC服务。
// 没Accept一个请求,就创建一个goroutie进行处理
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Accept error:", err)
}
// // 前面都是tcp的知识, 到这个RPC就接管了
// // 因此 你可以认为 rpc 帮封装消息到函数调用的这个逻辑,
// // 提升了工作效率, 逻辑比较简洁,可以看看他代码
// go rpc.ServeConn(conn)
// 代码中最大的变化是用rpc.ServeCodec函数替代了rpc.ServeConn函数,
// 传入的参数是针对服务端的json编解码器
go rpc.ServeCodec(jsonrpc.NewServerCodec(conn))
}
}
客户端
pbrpc/client/main.go
package main
import (
"fmt"
"net"
"net/rpc"
"net/rpc/jsonrpc"
"github.com/ProtoDemo/pbrpc/service"
)
// 约束客户端
var _ service.HelloService = (*HelloServiceClient)(nil)
type HelloServiceClient struct {
*rpc.Client
}
func DialHelloService(network, address string) (*HelloServiceClient, error) {
// 建立链接
conn, err := net.Dial(network, address)
if err != nil {
fmt.Println("net.Dial:", err)
}
// 采用Json编解码的客户端
c := rpc.NewClientWithCodec(jsonrpc.NewClientCodec(conn))
return &HelloServiceClient{Client: c}, nil
}
func (p *HelloServiceClient) Hello(req *service.Request, resp *service.Response) error {
return p.Client.Call(service.HelloServiceName+".Hello", req, resp)
}
func main() {
client, err := DialHelloService("tcp", "localhost:1234")
if err != nil {
fmt.Println("dialing err:", err)
}
resp := &service.Response{}
err = client.Hello(&service.Request{Value: "hello"}, resp)
if err != nil {
fmt.Println(err)
}
fmt.Println(resp)
}
测试RPC
# 启动服务端
$ go run server/main.go
# 客户端调用
$ go run client/main.go
value:"hello:hello"