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)中的描述符,将它们依赖的描述符通通注册到描述符池和消息工厂中。
- 描述符池的索引构建:
数据表 all_values_ 中存储着 文件元数据+标签元数据+扩展元数据,它们都包含所指 数据项EncodedEntry 在 数据表all_values_ 中的偏移量,数据项包含指向data数据的指针。此外,它们还另外包含如下所述信息。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;
文件元数据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_ 表得到原型对象了。
- MessageFactory的索引构建
-
创建反射实例 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.protosyntax="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、请求号),至此服务注册成功。
- google::protobuf::Service提供了GetDescriptor,可以获取到服务描述符service_descriptor。
-
启动:套接字通信的流程,将可读事件放入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