protobuf入门学习

目录

1 protobuf是什么

2 protobuf安装

3 protobuf的优点

4 如何创建proto

5 编译成c++类

6 序列化和反序列化

对于简单数据的序列化

对于复杂数据的序列化

反序列化

7  含有protobuf的文件链接方式

8 附加:Protobuf更快的秘密


1 protobuf是什么

protobuf是谷歌开发的,那么先看看谷歌对protobuf的定义:

google:
Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.

也就是说:protobuf是一种类似于XML的序列化协议,可以跨平台、跨语言使用,但是使用起来比XML更小、更快、更简单。

注:如果这段话感觉没有收获,请移步到3 protobuf的优点

2 protobuf安装

protobuf安装教程

3 protobuf的优点

常见序列化和反序列化协议有XML、JSON、protobuf,相比于其他,protobuf更有优势:

  1. protobuf是二进制存储的,xml和json都是文本存储的,故protobuf占用带宽较低。

  2. protobuf不需要存储额外的信息。 json如何存储数据?键值对。例:Name:”zhang san”, pwd: “12345”。 protobuf存储数据的方式:“zhang san” “123456”(省略了Name,pwd这样的额外信息)

  3. protobuf跨平台语言支持。 可以直接在同构和异构系统中进行调用。异构系统指的是:有的RPC进程是C++写的服务,有的RPC进程是Golang或者Java写的服务,但因为都是基于统一的protobuf协议进行通信的,所以直接可以进行远程通信。

  4. protobuf序列化和反序列化效率高、速度快且序列化后体积比XML和JSON都小很多,适合网络传输。

    image-20240627205409728

4 如何创建proto

vscode编辑需要先安装插件:vscode-proto3。

先看一个最简单的.proto文件,了解下语法:

//test.proto文件
syntax = "proto2";
package tutorial;
message PersonInfor
{
    optional string name;   
    optional int32 age;
    required string sex;
    repeated string friends;
}
  • syntax:表示当前使用的版本是proto2,现在工程常用的是proto3。
  • package:java中的语法,可以理解为C++中的namespace,后面的tutorial是自定义的,想怎么取就怎么取。
  • message:这个可以类比与C++中的struct关键字,里面只能放成员变量,不能放成员方法。后面的PersonInfor也是自定义的。
  • optional:属性修饰词,表示该属性可以设置其值,也可以不设置其值
  • required:属性修饰词,表示该属性必须有值。
  • repeated:属性修饰词,表示该属性是一个动态数组,可以理解为vector。

当然这是在proto2语法下,在proto3语法下所有的optional和required可以省略不写,缺省条件下默认是optional。

以protobuf3为例写一下.protobuf文件:

  1. 创建一个xxx.proto文件,我这里使用vscode远程连接ubuntu,创建了一个:test.proto

  2. 定义版本和声明,第三个为生成service所需要的声明(如果不加这行,service服务类和rpc方法描述默认不生成,我这边是rpc项目使用的,如果其他项目根据需求决定是否需要添加。)

    syntax = "proto3";      //声明protobuf的版本
    package fixbug;         //声明代码所在的包(对于c++来说是namespace)
    option cc_generic_services = true;  //表示生成service服务类和rpc方法服务,不加这行语句生成不了service
  3. 定义消息类型

    //错误信息
    message ResultCode
    {
        int32 errcode=1;    //这里的=1,=2是必须添加的,表示第一个成员变量,第二个成员变量,.cpp文件会根据这个声明顺序来进行处理
        bytes errmsg=2;
    }
    //登录请求消息
    message LoginRequest
    {
        bytes name=1;
        bytes pwd=2;
    }
    //登录响应消息
    message LoginResponse
    {
        ResultCode result=1;
        bool success=2;
    }

    注意,其实这里的btyes类型和string类型都表示字符串,建议使用bytes类型,因为bytes直接存二进制文件,效率更高一点,如果用string,最后还要将其转换为字节数据,而bytes则不需要。

  4. 生成rpc方法的类型

    在protobuf里面定义描述rpc方法的类型 – service

    service UserServiceRpc
    {
        rpc Login(LoginRequest) returns(LoginResponse);
        rpc Register(RegisterRequest) returns(RegisterResponse);
    }

最终的test.proto文件就是将上面所有代码写在一起:

//test.proto
syntax = "proto3";      //声明protobuf的版本
package fixbug;         //声明代码所在的包(对于c++来说是namespace)
option cc_generic_services = true;  //表示生成service服务类和rpc方法服务,不加这行语句生成不了service
​
//错误信息
message ResultCode
{
    int32 errcode=1;
    bytes errmsg=2;
}
//登录请求消息
message LoginRequest
{
    bytes name=1;
    bytes pwd=2;
}
//登录响应消息
message LoginResponse
{
    ResultCode result=1;
    bool success=2;
}
​
service UserServiceRpc
{
    rpc Login(LoginRequest) returns(LoginResponse);
    rpc Register(RegisterRequest) returns(RegisterResponse);
}

5 编译成c++类

现在已经写完了一个test.proto文件,那么怎么把它编译成一个可以使用的类?

编译生成.cpp和.h文件,终端输入: protoc test.proto --cpp_out=./,这个命令会在当前路径下生成test.pb.cctest.pb.h文件。

这两个文件里面,protoc会给各代码个字段自动相应的方法,比如:

void set_name(const string& input);
string& get_name() const;

这都是protoc编译器自己实现的,感兴趣的朋友可以去看看这两个文件。

除了这些针对属性的函数,protoc还实现了以下四个有关序列化和反序列化的函数:

bool SerializeToString(string* output) const;   //序列化消息,将字节以string方式输出
bool ParseFromString(const string& data);       //解析给定的string 
bool SerializeToOstream(ostream* output) const; //写消息给c++ ostream中
bool ParseFromIstreamstream(istream* input);    //从给定的c++ istream中解析出消息

举例说明编译.proto文件,生成的文件:

  1. message方法生成的c++类

    //以test.proto文件里的
    message LoginRequest
    {
        bytes name=1;
        bytes pwd=2;
    }
    //为例

    比如上面的message LoginRequest,在执行完上面语句之后,会变成一个LoginRequset类,而LoginRequest里面的name和pwd都变成了类的成员变量,并且protobuf提供了读取和设置成员变量的函数:

image-20240627210744575

message里面的成员变量,其实就是自己定义的消息体,可以被序列化和反序列化。

  1. service方法生成的类,以service UserServiceRpc为例:

    //这是一个注册语句的rpc方法,其中LoginRequest是Login函数参数类型,LoginResponse是Login函数的返回值类型
    service UserServiceRpc
    {
        rpc Login(LoginRequest) returns(LoginResponse);
    }
    //这段代码在执行完protoc test.proto --cpp_out=./后,会产生两个类:
    class service UserServiceRPC: public google::protobuf::Service
    和 
    class UserServiceRpc_Stub::public UserServiceRpc

    image-20240627211201653

    • UserServiceRpc

      callee–>rpc服务的提供者(Server)使用。继承自goole::protobuf::Service

    • UserServiceRpc_Stub

      caller–>rpc服务的调用者(User)使用。继承自UserServiceRpc

    • RpcChannel类的成员函数很干净,一切的源头只需要一个RpcChannel类,RpcChannel类中只需要重写一个CallMethod方法,如下:

      image-20240627212134721

service定义的类型,是用来提供使用框架的,其中User使用的时候调用的是xxx_stub下面这个channel_->CallMethod方法,所以只需要重写这个方法就行。

channel_->CallMethod(descriptor()->method(1),
                       controller, request, response, done);

6 序列化和反序列化

序列化:序列化就是把对象转换成二进制数据发送给服务端 反序列化:反序列化就是将收到的二进制数据转换成对应的对象

protobuf跨平台语言支持,序列化和反序列化效率高速度快,且序列化后体积比XML和JSON都小很多,适合网络传输。

注意:序列化和反序列化可能对系统的消耗较大,因此原则是:远程调用函数传入参数和返回值对象要尽量简单,具体来说应:

  1. 避免远程调用函数传入参数和返回值对象体积较大,如传入参数是List或Map,序列化后字节长度较长,对网络负担较大

  2. 避免远程调用函数传入参数和返回值对象有复杂关系,传入参数和返回值对象有复杂的嵌套、包含、聚合关系等,性能开销大

  3. 避免远程调用函数传入参数和返回值对象继承关系复杂,性能开销大。

对于简单数据的序列化

序列化和反序列化仍然以下面这个.proto文件生成的cpp文件为例:

syntax = "proto3";      //声明protobuf的版本
package fixbug;         //声明代码所在的包(对于c++来说是namespace)
option cc_generic_services = true;
//错误信息
message ResultCode
{
    int32 errcode=1;
    bytes errmsg=2;
}
//登录请求消息
message LoginRequest
{
    bytes name=1;
    bytes pwd=2;
}
//登录响应消息
message LoginResponse
{
    ResultCode result=1;
    bool success=2;
}
  1. 添加头文件:#include "test.pb.h"

  2. 定义对象并设置成员变量

    //封装对象的数据
    fixbug::LoginRequest req;//定义对象,这里加fixbug::是因为前面说过package相当于c++的namespace,前面设置的是:package fixbug,所以使用时要把命名空间加上。
    req.set_name("zhang san");//给对象里面成员变量设置内容使用:set_xxx(),生成的test.pb.cpp文件里面会自动生成这些函数
    req.set_pwd("123456");
  3. 将打包好的LoginRequest reqA数据交给protobuf进行序列化

    std::string send_str;
    // 进行序列化,框架干的事情
    if (req.SerializeToString(&send_str))//SerializeToString方法:把对象req转为string类型,然后放到send_str里面,如果成功返回true
    {
        // 序列化成功后再发送,下面cout为模拟发送过程
        std::cout << send_str.c_str() << std::endl;
    }

整体代码就是:

#include "test.pb.h"
int main()
{
    fixbug::LoginRequest req;
    req.set_name("zhang san");
    req.set_pwd("123456");  
    std::string send_str;
    if (req.SerializeToString(&send_str))
    {
        std::cout << send_str.c_str() << std::endl;
    }
}

对于复杂数据的序列化

复杂数据是指:数据里面有别的数据类型,数据里面有列表等...

例如:.proto文件是这样书写的:

//错误信息
message ResultCode
{
    int32 errcode=1;
    bytes errmsg=2;
}
//定义好友列表请求
message GetFriendListRequest
{
    uint32 userid=1;
}
//定义用户信息
message User
{
    bytes name=1;
    uint32 age=2;
    enum Sex
    {
        MAN=0;
        WOMAN=1;
    }
    Sex sex=3;
}
//定义好友响应
message GetFriendListResponse
{   
    ResultCode result=1;
    repeated User friend_list=2;        //repeated User相当于vector<User>
}
//序列化方式如下:
fixbug::GetFriendListResponse rsp;
fixbug::ResultCode* rc=rsp.mutable_result();//如果一个message里面还包含别的非repeated类型的message,序列化时必须通过mutable_result()生成的指针来set数据,比如这里的ResultCode得通过rc来设置
rc->set_errcode(0); //此时对rc操作,相当于对rsp里面的ResultCode操作
rc->set_errmsg("error!");
​
fixbug::User* user1=rsp.add_friend_list();//friend_list是个列表,对于列表里面User的值,使用.add_xxx转化到user2里面,xxx是对应的名字,比如这里的repeated User friend_list(名字)
user1->set_age(20); //此时可以在列表里面添加了
user1->set_name("zhang san");
user1->set_sex(fixbug::User::MAN);
//添加第二个
fixbug::User* user2=rsp.add_friend_list();
user2->set_age(30);
user2->set_name("li si");
user2->set_sex(fixbug::User::WOMAN);
//如何获取列表内元素个数
std::cout<<rsp.friend_list_size()<<std::endl;//xxx_size()
//如何获取列表内具体某个User
fixbug::User user3=rsp.friend_list(0);

反序列化

假设此时数据被发送到对端,对端需要反序列化刚刚发送过来的send_str,才能够获取数据对象

LoginRequest reqB;
// 从send_str反序列化一个login请求对象
if (reqB.ParseFromString(send_str))  //通过.ParseFromString()方法把send_str字符串反序列化到reqB对象里面
{
    // 以下代码不属于框架内的代码
    std::cout << reqB.name() << std::endl;
    std::cout << reqB.pwd() << std::endl;
}

需要注意,所有不涉及抽象层,涉及具体的业务的代码,都不属于RPC分布式网络通信框架的代码。

7  含有protobuf的文件链接方式

  1. 直接g++

    g++ main.cc test.pb.cc -lprotobuf       //用main.cc和test.pb.cc生成可执行文件
  2. 使用cmake

8 附加:Protobuf更快的秘密

大家都说protobu比json和XML更快,但是为什么呢?

最常见的答案:protobuf是二进制存储的,xml和json都是文本存储的,故protobuf占用带宽较低。

这是面试时,大多数人背的八股文,那么protobuf到底是如何进行二进制存储的呢?

根本原因在于编码方式。

json是文本传输,而protobuf虽然也有文本传输,但是其更多的来说是按编码来传输,其编码的方式使它更小,在相同的带宽下,无疑是体积更小的文件传输的越快!

image-20240714141741703

这个连接是google官方给出的protobuf编码介绍:protobuf的编码方式

google:
message Test1 { optional int32 a = 1; }
In an application, you create a Test1 message and set a to 150. You then serialize the message to an output stream. If you were able to examine the encoded message, you’d see three bytes:
08 96 01

这段话的意思是说,如果我们定义了一个message,然后在创建对象时,把里面的a赋值为150,最终存储的的数据为08 96 01三个字节。换句话说:在protobuf的世界,08 96 01三个字节就可以表示:上图中protobuf的数据格式。确实,只用来三个字节,却能够表示这么多信息,极大降低了占用空间。那么这三个字节是怎么得到的呢?

google的官方解释是:

对于150,其二进制表示为0000 0000 0000 0000 0000 0000 1001 0110,可以看到除了最后一个字节,其他三个字节完全是用不上的,是一种对空间的浪费。所以就需要使用一种特殊的编码,它这里使用的是Varint(可变宽度整数)编码技术。

  • 08是如何得到的?

    先看一张关于protobuf类型的表:

image-20240714142249637

可以看到,int32类型的meaning为Varint,它的type为0。

message Test1 { optional int32 a = 1; }

这个定义可以看出,a的key为1,所以google的推导公式是:

(field_number << 3) | wire_type
这里的field_number就是key的值,wire_type就是对应的类型

当把1和0带入到公式中,得出的结果正好为0x08,表达的意思是:当前这个变量的类型是int32,是Test1里面的第一个元素。

  • 那么0x960x01又是怎么得到的呢?

    1. 为什么会有0x96?

      因为0x96对应的十进制是自己set的150,0x96的二进制是1001 0110,最高位是符号位,舍去,所以0x96变成了001 0110

    1. 而为什么会有01?

      是因为a的key为1,0x01的二进制0000 0001,把符号位去掉是000 0001,由于使用的是varint(可变宽度整数),所以对于000 0001来说,前面的7个0都可以省略,

    所以0x96->001 01100x01->1,接下来需要将他们组合起来,怎么组合?原文中提到:

    Convert to big-endian order, concatenate, and interpret as an unsigned 64-bit integer:这些 7 位有效负载采用小端顺序。转换为大端顺序,连接并解释为无符号 64 位整数

    所以需要把1写在前面,最终结果就是:10010110,它的十进制正好为150。

    10010110 00000001 // Original inputs.
    0010110 0000001 // Drop continuation bits.
    0000001 0010110 // Convert to big-endian.
    0000001 0010110 // Concatenate.
    128 + 16 + 4 + 2 = 150 // Interpret as an unsigned 64-bit integer.
  • 13
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Protobuf是一种用于序列化结构化数据的协议,它具有高效、跨平台和可扩展等特点。使用Protobuf,可以定义数据结构和消息类型,并根据这些定义生成对应的代码,用于在不同的应用之间进行数据的传输和存储。在Protobuf中,通过定义message来定义消息类型,类似于C/Java中的class关键字。使用protobuf编译器将.proto文件编译成相应的代码文件后,就可以在代码中使用生成的类进行数据的序列化和反序列化操作。 Protobuf具有广泛的应用场景,特别是在分布式系统和微服务架构中。它可以用于消息传递、持久化存储、RPC通信等各种场景。此外,Protobuf还支持版本演化和向后兼容性,可以满足系统的扩展和升级需求。 如果你想开始学习Protobuf,可以按照以下步骤进行: 1. 下载并安装Protobuf编译器:根据你的操作系统,从Protobuf官方网站下载合适的编译器,并按照官方指南进行安装。 2. 定义消息类型:使用.proto文件定义你的消息结构和消息类型。在文件中使用message关键字定义消息,指定字段名称和类型等信息。 3. 编译.proto文件:使用Protobuf编译器将.proto文件编译成对应的代码文件。根据你的编程语言选择合适的编译选项和目标语言。 4. 在代码中使用生成的类:根据生成的代码文件,在你的代码中使用生成的类进行数据的序列化和反序列化操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值