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"
目录结构

posted @ 2021-12-07 21:09  自己有自己的调调、  阅读(709)  评论(0编辑  收藏  举报