zoukankan      html  css  js  c++  java
  • 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"
    
    目录结构

  • 相关阅读:
    我开发的Quartz Cron表达式生成器
    web前端css定位position和浮动float
    ecmall模板编辑中的标题如何自定义读取
    记录一个项目的需求探讨过程
    这些记录
    今日工作总结:jquery轮转效果的集成与前台页面banner的设计思路总结
    小问题总结:鼠标点击到输入框(input)里的时候,输入框的提示消失,鼠标再移开,输入框提示出现
    web app与app的区别,即html5与app的区别
    3月初的日记:网站工作记录
    [接口]支付宝接口开发集成支付环境开发总结
  • 原文地址:https://www.cnblogs.com/remixnameless/p/15658849.html
Copyright © 2011-2022 走看看