在c++中使用Protobuf序列化结构化数据

protobuf的编码方式(write-type)

  • 可变长编码方式Varint(对应write-type值为0)
    Varint编码序列 = Tag信息 + Data信息
    Tag信息:使用高5位表示序号,低3位表示编码方式。
    Data信息:对数据进行切割,最高位0/1表示下一个字节是否仍然是该字段数据,每个字节仅使用7位来表示数据,实现了数据压缩
  • ZigZag编码:负数的补码是对除符号位外部分取反加1,为了节省内存,针对负数进行压缩处理
  • Length-delimited编码:使用一个 Varint 类型表示 length,后面接 length 个字节的内容
  • fixed不编码:由于Varint的每个字节只有低7位表示数据,在恰好需要8位来表示数据时,使用fix32比int32更节省空间

常见数据采用的编码方式

  • int32(也就是c++中的uint32):采用Varint编码
  • sint32:先ZigZap再Varint
  • string、map:Length-delimited编码

宁愿增加新字段,也不要对复杂类型使用repeated:repeated字段采用Length-delimited编码,当该字段添加多个数据时,会保存为Tag+data+Tag+data,也就是重复保存了Tag。对于基础数据类型会做压缩优化,但复杂类型就不会。

protobuf的动态反射

  • 在 描述符池(DescriptorPool)中检索名为 type_name 的消息类型的描述符元数据

    const google::protobuf::Descriptor* descriptor 
    = google::protobuf::DescriptorPool::generated_pool()
    ->FindMessageTypeByName(type_name);
    

    在程序启动时,会执行AddDescriptors()构建DescriptorPool索引。具体而言,会遍历描述符表(DescriptorTable)中的描述符,将它们依赖的描述符通通注册到描述符池和消息工厂中。

    • 描述符池的索引构建:
      std::vector<EncodedEntry> all_values_;//数据表
      //文件元数据
      std::set<FileEntry, FileCompare> by_name{FileCompare{*this}}; 
      std::vector<FileEntry> by_name_flat;
      //标签元数据
      std::set<SymbolEntry, SymbolCompare> by_symbol{SymbolCompare{*this}};
      std::vector<SymbolEntry> by_symbol_flat;	
      //扩展元数据
      std::set<ExtensionEntry, ExtensionCompare> by_extension{ExtensionCompare{*this}};
      std::vector<ExtensionEntry> by_extension_flat;	
      
      数据表 all_values_ 中存储着 文件元数据+标签元数据+扩展元数据,它们都包含所指 数据项EncodedEntry 在 数据表all_values_ 中的偏移量,数据项包含指向data数据的指针。此外,它们还另外包含如下所述信息。
      文件元数据FileEntry:还包含名字
      标签元数据SymbolEntry:包含符号类型(消息、枚举、扩展、服务)
      扩展元数据ExtensionEntry:包含扩展名
    • 描述符池索引的查询

      首先尝试按name来查找,找不到就构建symbol标签索引,并为每一种符号类型构建索引(例如按name查找)
      然后按照符号类型查找

  • 在 MessageFactory 中检索对应类型的工厂 prototype,用于创建该类型的实例

    const google::protobuf::Message* prototype
    = google::protobuf::MessageFactory::generated_factory()
    ->GetPrototype(descriptor);
    google::protobuf::Message* req_msg = prototype->New();
    
    • MessageFactory的索引构建

      与描述符池一样,都是在程序启动时构建一部分索引(type_map_ ),在查询时触发构建其余索引(file_map_ )。

    • MessageFactory的索引查询

      首先根据消息类型描述符 查type_map_ 表 得到原型对象用于创建消息实例。找不到就构建file_map_ ,使用反射得到消息类型的默认实例,完成对所有消息类型的遍历注册,然后就能从type_map_ 表得到原型对象了。

  • 创建反射实例 req_msg_ref 用于操作 req_msg 的字段

    const google::protobuf::Reflection* req_msg_ref
    = req_msg->GetReflection();
    

    Message 基类指针无法调用 子类的set_payload函数 来读写字段,因此使用Reflection代为操作

  • 使用描述符获取 payload 字段描述符,用于读写该具体字段

    const google::protobuf::FieldDescriptor *req_msg_ref_field_payload
    = descriptor->FindFieldByName("payload");
    

    这里的字段索引构建和上述索引构建是同时的

  • 使用反射实例和字段描述符,写 payload 字段值

    req_msg_ref->SetString(req_msg, req_msg_ref_field_payload, "my payload");
    

protobuf的使用

  • 安装
    sudo apt install libprotobuf-dev protobuf-compiler

  • 先写两个proto文件如下
    xx.proto

    syntax="proto3";
    import "yy.proto";
    package yx;
    message Test_2{
        yxother.Test_1 t1 = 1;
        enum Color{
            RED = 0;
            BLACK = 1;
        }
        map<string, string> c = 2;
    }
    

    yy.proto

    syntax="proto3";
    //package是命名空间
    package yxother;
    message Test_1{
    	int32 a = 1;
        repeated string b = 2;
    }
    

    字段唯⼀编号的范围:1 ~ 2^29-1,其中 19000 ~ 19999不可⽤

  • 编译proto,⽣成C++⽂件:对于每个 .proto ⽂件⽣成 .h 和 .cc ⽂件,分别⽤来存放类的声明与类的实现
    protoc --cpp_out=. xx.proto protoc --cpp_out=. yy.proto

  • 使用:编译时要链接protobuf库

    #include <iostream>
    #include <fstream>
    #include "../xx.pb.h"
    
    int main()
    {
    	  yx::Test_2 p1;
    	  yxother::Test_1 p2 = p1.t1();
    	  //设置
    	  p2.add_b("nh");//b是repeated字段,使用时不是直接set,而是add添加
    	  p2.add_b("world");
    	  p2.set_a(123);
    	  p1.mutable_c()->insert({"key1","value1"});
    	  //获取
    	  std::cout<<p2.a()<<std::endl;//123
    	  std::cout<<p2.b(1)<<std::endl;//world
    	  std::cout<< p1.c().at("key1") <<std::endl;//value1
    	  
    	  //将yxother::Test_1序列化为字符串
    	  std::string res;
    	  p2.SerializePartialToString(&res);
    	
    	  //反序列化
    	  yxother::Test_1 p2_back;
    	  p2_back.ParseFromString(res);
    	  std::cout<<p2_back.a()<<std::endl;//123
    	  std::cout<<p2_back.b(1)<<std::endl;//world
    	  return 0;
    }
    
  • 注意:结构体的编号是从1开始,而enum枚举的第一个编号必须是0

protobuf使用服务

proto文件中定义好服务,例如SearchService,服务中有rpc方法Search,生成的文件里会包含2个类,一个是SearchService,一个是SearchService_Stub。
SearchService_Stub对象stub是客户端用来发送请求的,需要一个RpcChannel作为参数,当stub调用CallMethod()时,要传入MethodDescriptor,MethodDescriptor有个index()可以用来指明执行不同的rpc方法,本文只写了一个rpc方法Search,因此调用的method->index()就是0,会执行Search()方法。
SearchService_Stub.Search()调用的就是channel的CallMethod(),在CallMethod的实现中会根据method->index的不同来确保客户端发送不同请求,因为客户端调用任何一个RPC接口,最终都是调用到CallMethod

服务端:实现rpc方法
客户端:通过Stub类发送请求,即调用RpcChannel的CallMethod()方法

g++ test.cpp ssr.pb.cc -I/usr/local/include -L/usr/local/lib -lprotobuf -o test

easy_pb_rpc
proto文件里定义了一个服务EchoService,只有一个rpc方法Foo()
服务端:

  • 实现Foo方法

    class EchoServiceImpl : public EchoService{
    	virtual void Foo(...)
    }
    
  • 提供注册rpc服务的方法

    • google::protobuf::Service提供了GetDescriptor,可以获取到服务描述符service_descriptor。
      const google::protobuf::ServiceDescriptor *sd = service->GetDescriptor();
      使用service_descriptor的method(i)可以得到方法描述符,而不是自己创建MethodDescriptor类型的对象。
      将方法描述符传入Service对象的GetRequestPrototype、GetResponsePrototype
      获取到请求和响应消息的Message对象,将它们保存起来(需要保存:方法描述符、请求与响应Message对象、Service、请求号),至此服务注册成功。
  • 启动:套接字通信的流程,将可读事件放入eventfd监听

  • 有新连接:连接后注册可读事件函数

  • 可读事件发生(客户端发送信息来了):读消息头、消息体,然后解析数据(反序列化)得到服务名和rpc方法名、重置控制器

    • 对于读到的消息体,其中需要包含服务号、方法号、请求号、数据。由服务号和方法号就能从数组里找到方法描述符、服务
  • 根据读到的消息体数据,解析为请求对象与响应对象:首先使用当前服务的New()得到一个请求Message对象request、一个响应Message,然后使用request的ParseFromString()进行解析。

    Message *request = the_method->_request_proto->New();
    Message *response = the_method->_response_proto->New();
    request->ParseFromString(rpc_data.content());
    
  • 至此,可以调用rpc_service->CallMethod(…)得到响应Message了(实际上是调用RpcChannel::CallMethod)

  • 将response序列化后发送给客户端

客户端:

  • 使用proto中定义的request类型构造好要发送的信息
  • RpcChannel::CallMethod中实现连接、发送request、读取服务端响应。客户端调用该函数时没有请求号,会去执行我们实现的connect流程,并启动一个线程用于监听管道读事件,一旦发生,就循环的从队列中取出信息,将信息序列化后发送给服务器
  • 使用服务对应的Stub对象,stub需要传入channel对象
  • 创建好stub对象后,就可以调用其中的rpc方法了,stub.Foo(…)也就是调用channel里的CallMethod。这个时候就会在CallMethod里执行“连接、request序列化发送、读响应”

总之:客户端只需要创建 指定服务的存根对象stub,调用想要调用的rpc方法。服务端需要实现rpc方法,提供服务注册的方法。客户端通过存根调用rpc方法时,实际上是调用的channel_->CallMethod(),服务端分发器处调用的CallMethod是pb.cc文件里的CallMethod

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值