日常小知识点之序列化结构(protobuf使用及简单原理)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 日常小知识点之序列化结构(protobuf使用及简单原理)

很早的时候用过protobuf,但是近年项目中用的少,但是面试的时候,突然被问到protobuf的底层原理,一直以为自己会,却也难免语塞,就对这个问题记在心头。

这里的目标是通过简单实例,了解一下protobuff的底层逻辑(序列化方式)。

0:相关类型

1:概述

protobuf本质上说是定义好(序列化/反序列化)的一种协议,设计协议需要考虑:

==》1:序列化和反序列化(TLV,文本流,固定格式(tcp/ip))

==》2:判断包的完整性(固定大小,特定符号分界,固定包头+包体结构(tcp/udp),先解析包头(包头完整性)再接收包体(http,redis))

==》3:协议的可升级(增加字段)

==》4:协议安全(xxtea,aes,openssl,signal protocol)

==》5:数据压缩(deflate,gzip,lzw)

protobuf语言无关,平台无关,相对于文本类协议(json,xml),体量更小,解析速度快。

2:protobuf的安装

linux环境上,通过简单的通用安装命令进行安装:

tar zxvf protobuf-cpp-3.8.0.tar.gz 
cd protobuf-3.8.0/
./configure CXXFLAGS="-O2" CFLAGS="-O2"
make
sudo make install
sudo ldconfig
protoc --version

3:protobuf的测试用例

3.1:protobuf的使用方式:

编写对应的设定的.proto文件,使用如下命令生成对应的文件进行使用

protoc -I=./ --cpp_out=./ ./*.proto  #这里都指定了当前目录,可以自己设定 -I是proto的路径 --cpp_out 是cpp生成目标路径 以及proto文件

3.2:protobuf的使用

3.2.1:proto文件的定义

IM.BaseDefine.proto

syntax = "proto3";
package IM.BaseDefine;             //服务前缀,包名,防止冲突
option optimize_for = LITE_RUNTIME;
enum PhoneType{
  PHONE_DEFAULT     = 0x0;    
    PHONE_HOME              = 0x0001;   // 家庭电话
    PHONE_WORK              = 0x0002;   // 工作电话     
}

IM.Login.proto

syntax = "proto3";
package IM.Login;           //服务前缀,包名,防止冲突
import "IM.BaseDefine.proto";     //这里包含了别的proto文件
option optimize_for = LITE_RUNTIME;   //optimize_for是文件级别的选项,Protocol Buffer定义三种优化级别SPEED/CODE_SIZE/LITE_RUNTIME
//缺省情况下是SPEED。 
//SPEED: 表示生成的代码运行效率高,但是由此生成的代码编译后会占用更多的空间。
//CODE_SIZE: 和SPEED恰恰相反,代码运行效率较低,但是由此生成的代码编译后会占用更少的空间,通常用于资源有限的平台,如Mobile。
//LITE_RUNTIME: 生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。     这是以牺牲Protocol Buffer提供的反射功能为代价的
message Phone{
    string number = 1;
    IM.BaseDefine.PhoneType phone_type = 2;
}
message Book{
    string name = 1;
    float price = 2;
}
message Person{
    string  name    = 1;
    int32   age     = 2;
    repeated string languages = 3;
    Phone   phone   = 4;
    repeated Book   books   = 5;
    bool            vip     = 6;
    string          address = 7;
}
//使用T开头测试
message TInt32{
    int32   int1     = 1;
}
message TString{
    string   str1     = 1;
}

3.2.2:生成对应的头文件

protoc -I=./ --cpp_out=../test_src ./*.proto #-I指定proto文件路径 --cpp_out指定cpp生成文件路径 然后是要使用的proto文件

即可看到在**–cpp_out**目录下生成对应.h和.cpp文件

3.2.3:通过调用生成的头文件对proto进行使用

g++ -o test test.cpp IM.BaseDefine.pb.cc IM.Login.pb.cc -lprotobuf -lpthread -std=c++11

demo源码:

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/wait.h>
//依赖proto生成的文件进行使用
#include "IM.BaseDefine.pb.h"
#include "IM.Login.pb.h"
//按结构进行数据构造,并获得序列化后的数据 并输出
bool create_encode_data(std::string &strProto); //参数传出结果
//根据获取到的序列化数据,进行反序列化获取原特定结构的数据
bool decode_data_get_data(std::string &strProto); //传入参数
int main()
{
  std::string strProto;
  //构造原始数据 获取序列化的数据
  create_encode_data(strProto);
  //根据序列化的数据 反序列化后打印原始结构数据
  decode_data_get_data(strProto);
  return 0;
}
//根据proto文件中的结构定义,构造数据
//最终的数据即是序列化后的数据 进行分析
//传出序列化后的数据,这里仅是测试
bool create_encode_data(std::string &strProto)
{
/************************************
message Person{
    string  name    = 1;
    int32   age     = 2;
    repeated string languages = 3;
    Phone   phone   = 4;
    repeated Book   books   = 5;
    bool            vip     = 6;
    string          address = 7;
}
************************************/
  IM::Login::Person person;
  person.set_name("my name test");      // 设置以set_为前缀
    person.set_age(21);
    //repeated字段可以有多个value值
    person.add_languages("C++");    // 数组add
    person.add_languages("Java");
    //取其中的子元素进行数据构造
    //mutable_ 嵌套对象时使用,并且是单个对象时使用
    IM::Login::Phone *phone = person.mutable_phone();
    if(!phone)
    {
        std::cout << "mutable_phone failed." << std::endl;
        return false;
    }
    phone->set_number("137 7777 9899"); //字符串
    phone->set_phone_type(IM::BaseDefine::PHONE_HOME);
    //add_针对 repeated多个对象使用,每次增加一个,可以增加多个
    //添加第一个对象
    IM::Login::Book *book = person.add_books();
    book->set_name("c++ plus");
    book->set_price(6.7);
    //添加第二个对象
    book = person.add_books();
    book->set_name("Advanced Programming in the UNIX Environment");
    book->set_price(16.7);
    person.set_vip(true);
    person.set_address("xxxx xxx xx");
    //这里就已经构成了一个person的对象
    //可以对该数据进行序列化,用于网络传输等业务处理
    uint32_t buff_size = person.ByteSize();
    //这里使用std::string 直接存储
    strProto.clear();
    strProto.resize(buff_size);
    //拷贝序列化后的内容进存储空间 实际就是写入strProto 中
    uint8_t * c_protobuf = (uint8_t*)strProto.c_str();
    if(!person.SerializeToArray(c_protobuf, buff_size))
    {
      std::cout<<"proto buff to array error"<<std::endl;
      return false;
    }
    //输出序列化后的数据
    printf("序列化后的数据:\n");
    for(int i=0;i <buff_size; i++)
    {
      printf("%02x", c_protobuf[i]);
    }
    printf("\n");
    // for(int i=0;i <buff_size; i++)
    // {
    //  printf("%c", c_protobuf[i]);
    // }
    // printf("\n");
    return true;
}
bool decode_data_get_data(std::string &strProto)
{
  //对数据进行反序列化(解析),获取原结构数据(使用)
  //调用接口,进行解析,获取到对象结构
  IM::Login::Person person;
  //strProto 这里使用可能有点不可靠,最好长度传进来
  person.ParseFromArray(strProto.c_str(), strProto.size());
  //根据IM::Login::Person 结构对内存进行输出
  printf("struct data is: \n");
  std::cout << "  name:\t" << person.name() << std::endl;
    std::cout << "  age:\t" << person.age() << std::endl;
    std::string languages;
    for (int i = 0; i < person.languages_size(); i++)
    {
        if (i != 0)
        {
            languages += ", ";
        }
        languages += person.languages(i);
    }
    std::cout << " languages:\t" << languages << std::endl;
    // 自定义message的嵌套并且不是设置为repeated则有has_
    if (person.has_phone()) 
    {
        const IM::Login::Phone &phone = person.phone();
        std::cout << " phone number:\t" << phone.number() << ", type:\t" << phone.phone_type() << std::endl;
    }
    else
    {
        std::cout << " no phone" << std::endl;
    }
    //多个元素  依次取数据
    for (int i = 0; i < person.books_size(); i++)
    {
        const IM::Login::Book &book = person.books(i);
        std::cout << " book name:\t" << book.name() << ", price:\t" << book.price() << std::endl;
    }
    std::cout << " vip:\t" << person.vip() << std::endl;
    std::cout << " address:\t" << person.address() << std::endl;
    return false;
}
/****************************************
//编译命令
g++ -o test test.cpp IM.BaseDefine.pb.cc IM.Login.pb.cc -lprotobuf -lpthread -std=c++11
//如果编译有报错,请注意环境是否有以前装过的protobuf
序列化后的数据:
0a0c6d79206e616d65207465737410151a03432b2b1a044a61766122110a0d3133372037373737203938393910012a0f0a08632b2b20706c7573156666d6402a330a2c416476616e6365642050726f6772616d6d696e6720696e2074686520554e495820456e7669726f6e6d656e74159a99854130013a0b7878787820787878207878
struct data is: 
  name: my name test
  age:  21
 languages: C++, Java
 phone number:  137 7777 9899, type:  1
 book name: c++ plus, price:  6.7
 book name: Advanced Programming in the UNIX Environment, price:  16.7
 vip: 1
 address: xxxx xxx xx
  0a 0c 6d7920 6e616d 652074 657374   //"my name test" 0000 0 010
 10 15                  //21       0001 0 000
 1a 03 432b2b             //"C++"      0001 1 010
 1a 04 4a617661             //"Java"
 22 11                           0011 0 010
  0a 0d 31333720 37373737 20 39383939 //"137 7777 9899"
  10 01               //1(enum)
 2a 0f                           0011 1 010
  0a 08 632b2b20 706c7573       //"c++ plus"
  15 6666d6402a33           //6.7
  0a 2c 41647661 6e636564 2050726f 6772616d 6d696e67 20696e20 74686520 554e4958 20456e76 69726f6e 6d656e74  //"Advanced Programming in the UNIX Environment"
  15 9a998541             //16.7
 30 01                  //true       0011 0 000
 3a 0b 78787878 20787878 207878     //"xxxx xxx xx"  0011 1 010
******************************************/

3.2.4:关注到的细节知识点。

3.2.4.1:一些细节

repeated修饰的proto字段,对应的值可以是多个。 通过add_xxx(“xxx”)添加参数,如果是自定义结构对象,可以通过add_xxx()取对象后赋值

嵌套对象,用mutable_XXX取值并赋值。

/*如果proto结构体的变量是基础变量,比如int、string等等,那么set的时候直接调用set_xxx即可。
如果变量是自定义类型(也就是message嵌套),那么C++的生成代码中,就没有set_xxx函数名,取而代之的是三个函数名:
set_allocated_xxx()
release_xxx()
mutable_xxx()
使用set_allocated_xxx()来设置变量的时候,变量不能是普通栈内存数据,
必须是手动new出来的指针,至于何时delete,就不需要调用者关心了,
protobuf内部会自动delete掉通过set_allocated_设置的内存;
release_xxx()是用来取消之前由set_allocated_xxx设置的数据,
调用release_xxx以后,protobuf内部就不会自动去delete这个数据;
mutable_xxx()返回分配内存后的对象,如果已经分配过则直接返回,如果没有分配则在内部分配,建议使用mutable_xxx*/
3.2.4.1:一些关键词

message (结构化数据的标识)

required(必须有值,proto3删除了),optional(可选是否有该成员,标记是否有值,可以采用默认值),repeated(可重复)

reserved(保留字段)

rpc (protobuf中可以使用grpc)

4:protobuf序列化规则梳理

4.1:基础规则表

3 和 4 已经被废弃了,所以 wire_type 取值目前只有 0、1、2、5

编码方式可选: 0,1,2,5,对应的编码方式就如图。

wire_type = 0 ===》 varints方案,要考虑无符号的数。

wire_type = 1 ===》 需要一个64位数据块大小即可

wire_type = 5 ===》 需要一个32位数据块大小即可

wire_type = 2 ===》 使用key + length + content方式(key 的编码方式是统一的,length 采用 varints 编码方式,content 就是由 length 指定长度的 Bytes)

4.2:序列化方式

序列化方式就是根据上面的定义规则进行的。

4.2.1:需要关注的一些特征:

wire_type ===》标识该结构对应的值,如图,有对应的序列化方式, 决定了后面字段的解析方式

字段号===》同一个结构中,字号段依次增长。

一般,第一个字节是标识编码和字段号的方式,最后三个bit对应的上图中的编码方式。(可选值位0 1 2 5)

如测试代码中解析:

//可以看到第一个字节对应得后3个bit对应得值都是0,1,2,5之间。
//在这个结构中得,前面得字节标识得是字段号,字段号是从0依次增加得。
//其他字段,都是根据第一个字段得3bit对应得解析方式进行得解析
 0a 0c 6d7920 6e616d 652074 657374    //"my name test" 0000 0 010
 10 15                  //21       0001 0 000
 1a 03 432b2b             //"C++"      0001 1 010
 1a 04 4a617661             //"Java"
 22 11                           0011 0 010
  0a 0d 31333720 37373737 20 39383939 //"137 7777 9899"
  10 01               //1(enum)
 2a 0f                           0011 1 010
  0a 08 632b2b20 706c7573       //"c++ plus"
  15 6666d6402a33           //6.7
  0a 2c 41647661 6e636564 2050726f 6772616d 6d696e67 20696e20 74686520 554e4958 20456e76 69726f6e 6d656e74  //"Advanced Programming in the UNIX Environment"
  15 9a998541             //16.7
 30 01                  //true       0011 0 000
 3a 0b 78787878 20787878 207878     //"xxxx xxx xx"  0011 1 010

4.2.2:特征(Base 128 Varints 编码:最高位标识 7位存储)

可以看到,如果第一个字节最后是010,则对应得是字符串得序列化,用的是tag+length+data得方式,紧跟着后面一个字节存长度,及对应string字符串。

除此之外,有关数字得存储,有一个细节(Base 128 Varints 编码):

proto有关数字得存储,以一个字节为准,最高一位作为标识位,剩余得7位做存值。

===》例如:如果7个字节能存储到得数字,即一个字节存储,最高位标0,如果存储不上,第一位标1,存七位,下个字节继续,直到最后一个字节能存储上,根据首位0判断终结。

注意:定义了专门得类型,把负数转为正数进行存储: sint32 类型,采用 zigzag 编码,再用Varints编码。

5:总结

要被问到protobuf得序列化原理:

===》序列化方式1:存储数字得细节,使用varints得方式,

===》序列化方式2:string得存储方式是tag+length_data得方式

===》自定义得内部嵌套对象,也是按照string得序列化方式进行存储得,只是对应字段按结构进行解析(参考上位4.2.1)。

===》负数通过专门得类型,专门进行先映射(zigzag 编码),再按varints编码。

===》序列化方式3:留专门得字节,4个字节或者8字节(write_type决定)

参考课程:C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂 (qq.com)

目录
相关文章
|
4月前
|
Java
JDK序列化原理问题之Hessian框架不支持writeObject/readObject方法如何解决
JDK序列化原理问题之Hessian框架不支持writeObject/readObject方法如何解决
|
4月前
|
自然语言处理 JavaScript 前端开发
JDK序列化原理问题之FuryJDK序列化性能问题的如何解决
JDK序列化原理问题之FuryJDK序列化性能问题的如何解决
|
4月前
|
XML 存储 JSON
(十二)探索高性能通信与RPC框架基石:Json、ProtoBuf、Hessian序列化详解
如今这个分布式风靡的时代,网络通信技术,是每位技术人员必须掌握的技能,因为无论是哪种分布式技术,都离不开心跳、选举、节点感知、数据同步……等机制,而究其根本,这些技术的本质都是网络间的数据交互。正因如此,想要构建一个高性能的分布式组件/系统,不得不思考一个问题:怎么才能让数据传输的速度更快?
118 1
|
4月前
|
缓存 Java
JDK序列化原理问题之Fury如何实现与JDK序列化100%兼容的如何解决
JDK序列化原理问题之Fury如何实现与JDK序列化100%兼容的如何解决
|
4月前
|
Java
JDK序列化原理问题之在JDK序列化中不同JDK版本字段不一致的情况如何解决
JDK序列化原理问题之在JDK序列化中不同JDK版本字段不一致的情况如何解决
|
7月前
|
缓存 自然语言处理 JavaScript
万字长文深度解析JDK序列化原理及Fury高度兼容的极致性能实现
Fury是一个基于JIT动态编译的高性能多语言原生序列化框架,支持Java/Python/Golang/C++/JavaScript等语言,提供全自动的对象多语言/跨语言序列化能力,以及相比于别的框架最高20~200倍的性能。
168746 12
|
7月前
|
C++
[序列化协议] --- protobuf
[序列化协议] --- protobuf
66 0
|
7月前
protobuf 序列化和反序列化
protobuf 序列化和反序列化
43 0
|
7月前
|
存储 Java 开发工具
[Android]序列化原理Parcelable
[Android]序列化原理Parcelable
134 0
|
7月前
|
存储 Java Android开发
[Android]序列化原理Serializable
[Android]序列化原理Serializable
95 0