• C/C++编程-理论学习-通信协议理论


    protobuf

    简述

    作用:
    1. 将结构化数据 序列化 进行信息通信、存储。意为,数据结构化管理;意为,对结构化的数据进行序列化,便于发送、存储。可类比XML、JSON。

    弊端:
    1. buffer占用额外空间,传输比透传降低很多。(这里有一个故事,我们领导极力让单片机和上位机通信,采用protobuf传输数据,端口为串口。我问有何好处,不采用透传自定义协议的原因?他回答protobuf传输效率更高,比如连续8个字节都是0x00,它会智能简化、压缩传输,比如传输0x00,还附带额外信息表明共有8个连续此字节信息。后来事实证明串口透传效率更高,如果透传花费2ms的话,protobuf装填message,序列化,然后将绑定buf传输出去,总共约需要将近20ms,大约差10倍的效率),其实道理也很简单,自定义的透传协议本质不需要序列化,已经是二进制的数据序列了,只要根据格式直接传输、解析即可,效率自然极高。

    使用简介

    《Nanopb:Basic concenpts》
    在这里插入图片描述

    contents :

    • Proto 文件
      • 编译文件
      • 修改生成行为
      • 输出流
      • 输入流
    • 数据类型
      proto 描述 和 生成的数据结构
    • Field callbacks
      • 编码回调
      • 解码回调
      • 功能名字绑定回调
    • 信息描述
    • Oneof
    • Extension fields
    • Default values
    • Message framing
    • Return values and error handling
    • Static assertions

    proto 文件

    为了nanopb 编译.proto文件
    修改生成器行为

    写一个和.proto同名的 .options文件。使用生成器选项文件,可以设置字段的最大大小,进而静态申请其内存。

    # Foo.proto
    message Foo {
       required string name = 1;
    }
    
    # Foo.options
    Foo.name max_size:16
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    streams

    回调的通用准则:

    1. IO有错误,编码和解码进程立即终止
    2. 用 ”state“ 存储自己的数据,例如文件描述符
    3. 通过pb_write 和 pb_read 更新 bytes_written 和 bytes_left
    4. 回调也可用于次数据流,,结构内值和初始流近似
    5. 总是读取或写入所请求的完整长度的数据
    output streams
    struct _pb_ostream_t
    {
       bool (*callback)(pb_ostream_t *stream, const uint8_t *buf, size_t count);
       void *state;
       size_t max_size;
       size_t bytes_written;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    如果callback是空,则只简单的数bytes_written进行发送,并且max_size会被忽略。
    否则,bytes_written(要写的数据)+ 已经被写的数据 比 max_size 大, pb_write 会在做任何事之前,返回错误。 如果你不想限制流的大小,注销掉SIZE_MAX即可。

    /* example1 */
    Person myperson = ...;
    pb_ostream_t sizestream = {0};
    pb_encode(&sizestream, Person_fields, &myperson);
    printf("Encoded size is %d\n", sizestream.bytes_written);
    
    /* example2 */
    bool callback(pb_ostream_t `stream, const uint8_t `buf, size_t count)
    {
       FILE *file = (FILE*) stream->state;
       return fwrite(buf, 1, count, file) == count;
    }
    
    pb_ostream_t stdoutstream = {&callback, stdout, SIZE_MAX, 0};
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    input streams

    不需要知道消息长度。读取时获得EOF错误,将bytes_left设置为0,并返回false。pb_decode会检测到,并且如果EOF出现在恰当位置,会返回ture。

    struct _pb_istream_t
    {
       bool (*callback)(pb_istream_t *stream, uint8_t *buf, size_t count);
       void *state;
       size_t bytes_left;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    callback 必须赋值函数指针。bytes_left是将要读取的字节数的上限。如果回调函数像上面描述的那样处理EOF,则可以使用SIZE_MAX。

    bool callback(pb_istream_t *stream, uint8_t *buf, size_t count)
    {
       FILE *file = (FILE*)stream->state;
       bool status;
    
       if (buf == NULL)
       {
           while (count-- && fgetc(file) != EOF);
           return count == 0;
       }
    
       status = (fread(buf, 1, count, file) == count);
    
       if (feof(file))
           stream->bytes_left = 0;
    
       return status;
    }
    
    pb_istream_t stdinstream = {&callback, stdin, SIZE_MAX};
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    Data types(数据类型)

    Field callbacks(字段回调)

    Encoding callbacks(编码回调)

    Message descriptor(信息描述)

    要使用pb_encode()和pb_decode()函数,你需要对消息中包含的所有字段进行描述。该描述通常从.proto文件自动生成。

    三个关键字required、optional、repeated 的理解(这个protobuf的关键字,nanopb不支持)
    • required关键字
      顾名思义,就是必须的意思,数据发送方和接收方都必须处理这个字段,不然还怎么通讯呢
    • optional关键字
      字面意思是可选的意思,具体protobuf里面怎么处理这个字段呢,就是protobuf处理的时候另外加了一个bool的变量,用来标记这个optional字段是否有值,发送方在发送的时候,如果这个字段有值,那么就给bool变量标记为true,否则就标记为false,接收方在收到这个字段的同时,也会收到发送方同时发送的bool变量,拿着bool变量就知道这个字段是否有值了,这就是option的意思。

    这也就是他们说的所谓平滑升级,无非就是个兼容的意思。

    • repeated关键字
      字面意思大概是重复的意思,其实protobuf处理这个字段的时候,也是optional字段一样,另外加了一个count计数变量,用于标明这个字段有多少个,这样发送方发送的时候,同时发送了count计数变量和这个字段的起始地址,接收方在接受到数据之后,按照count来解析对应的数据即可。

    【注】上面关键字说明是基于proto2版本的,在proto3上,关键字做了很多调整,比如去掉了required,默认什么都不写就是required。如果使用optional,可以使用。但是protobuf-c的实现(即c语言版本的protobuf)没有支持该关键字,所以最好改成oneof关键字替代,效果是一样的,repeated保持和proto2版本一样,整体说proto3的语法简洁很多。

    举一个二级信息的例子,在Person.proto 文件中:

    message Person {
        message PhoneNumber {
            required string number = 1 [(nanopb).max_size = 40];
            optional PhoneType type = 2 [default = HOME];
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这会在.pb.h文件中转换生生一个宏

    #define Person_PhoneNumber_FIELDLIST(X, a) \
    X(a, STATIC,   REQUIRED, STRING,   number,            1) \
    X(a, STATIC,   OPTIONAL, UENUM,    type,              2)
    
    • 1
    • 2
    • 3

    然后在.pb.c文件中有一个宏”PB_BIND"会被调用:

    PB_BIND(Person_PhoneNumber, Person_PhoneNumber, AUTO)
    
    • 1

    这个宏会组合生成 pb_msgdesc_t 结构 和 相关列表:

    const uint32_t Person_PhoneNumber_field_info[] = { ... };
    const pb_msgdesc_t * const Person_PhoneNumber_submsg_info[] = { ... };
    const pb_msgdesc_t Person_PhoneNumber_msg = {
      2,
      Person_PhoneNumber_field_info,
      Person_PhoneNumber_submsg_info,
      Person_PhoneNumber_DEFAULT,
      NULL,
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    编码和解码函数接受一个指向该结构的指针,并使用它来处理消息中的每个字段。
    【注1】:原来这就是我找不到生成的消息描述结构的原因,只找到了在.pb.h中的声明(如下图),原来是在.pb.c中通过宏生成的。

    /* .pb.h */
    extern const pb_msgdesc_t Serial_ack_repeat_msg;
    extern const pb_msgdesc_t Serial_fault_msg;
    extern const pb_msgdesc_t Serial_heart_beat_msg;
    extern const pb_msgdesc_t Serial_ready_msg;
    extern const pb_msgdesc_t Serial_system_info_msg;
    extern const pb_msgdesc_t Serial_ir_msg;
    extern const pb_msgdesc_t Serial_fan_msg;
    extern const pb_msgdesc_t Serial_imu_msg;
    extern const pb_msgdesc_t Serial_imu_speed_msg;
    extern const pb_msgdesc_t Serial_motor_msg;
    extern const pb_msgdesc_t Serial_power_msg;
    extern const pb_msgdesc_t Serial_power_batt_msg;
    extern const pb_msgdesc_t Serial_power_chg_msg;
    extern const pb_msgdesc_t Serial_nfc_msg;
    extern const pb_msgdesc_t Serial_button_msg;
    extern const pb_msgdesc_t Serial_tx_rx_msg;
    extern const pb_msgdesc_t Serial_tof_msg;
    extern const pb_msgdesc_t Serial_touch_msg;
    
    /* .pb.c */
    PB_BIND(Serial_ack_repeat, Serial_ack_repeat, AUTO)
    PB_BIND(Serial_fault, Serial_fault, AUTO)
    PB_BIND(Serial_heart_beat, Serial_heart_beat, AUTO)
    PB_BIND(Serial_ready, Serial_ready, AUTO)
    PB_BIND(Serial_system_info, Serial_system_info, AUTO)
    PB_BIND(Serial_ir, Serial_ir, AUTO)
    PB_BIND(Serial_fan, Serial_fan, AUTO)
    PB_BIND(Serial_imu, Serial_imu, AUTO)
    PB_BIND(Serial_imu_speed, Serial_imu_speed, AUTO)
    PB_BIND(Serial_motor, Serial_motor, AUTO)
    PB_BIND(Serial_power, Serial_power, AUTO)
    PB_BIND(Serial_power_batt, Serial_power_batt, AUTO)
    PB_BIND(Serial_power_chg, Serial_power_chg, AUTO)
    PB_BIND(Serial_nfc, Serial_nfc, AUTO)
    PB_BIND(Serial_button, Serial_button, AUTO)
    PB_BIND(Serial_tx_rx, Serial_tx_rx, AUTO)
    PB_BIND(Serial_tof, Serial_tof, AUTO)
    PB_BIND(Serial_touch, Serial_touch, AUTO)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    【注2】:看到这里,我突然明白原来nanopb采用了两个结构体,一个用于数据结构,一个用于数据结构的描述。举例如下:

    /* 数据结构 */
    typedef struct _Serial_fan {
        Serial_fan_value_t value;
        Serial_fan_id_t id;
        Serial_fan_fg_t fg;
    } Serial_fan;
    
    /* 数据的描述结构 - 通过.pb.c的宏生成 */
    extern const pb_msgdesc_t Serial_fan_msg;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    Oneof(多中呈一,可理解是一种联合体)

    举例子:

    /* .proto */
    message MsgType1 {
        required int32 value = 1;
    }
    
    message MsgType2 {
        required bool value = 1;
    }
    
    message MsgType3 {
        required int32 value1 = 1;
        required int32 value2 = 2;
    } 
    
    message MyMessage {
        required uint32 uid = 1;
        required uint32 pid = 2;
        required uint32 utime = 3;
    
        oneof payload {
            MsgType1 msg1 = 4;
            MsgType2 msg2 = 5;
            MsgType3 msg3 = 6;
        }
    }
    
    Nanopb 以C联合体的形式生成 payload,并增加了额外字段“which_payload”:
    typedef struct _MyMessage {
      uint32_t uid;
      uint32_t pid;
      uint32_t utime;
      pb_size_t which_payload;
      union {
          MsgType1 msg1;
          MsgType2 msg2;
          MsgType3 msg3;
      } payload;
    } MyMessage;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    "which_payload"表示哪个字段被实际设置。用户需要使用正确的字段标签手动设置字段:

    MyMessage msg = MyMessage_init_zero;
    msg.payload.msg2.value = true;
    msg.which_payload = MyMessage_msg2_tag;
    
    • 1
    • 2
    • 3

    不论是 “which_payload”字段 还是在“payload” 中未使用的字段都不会在生成编码信息(encoded message)中消耗任何空间。

    当在oneof 中包含一个pb_callback_t字段是,回调值不能在编码前被设置。 这是因为 在C 联合体中 不同的字段分享共同的存储空间。相反,可以使用函数名称绑定回调或单独的消息级别回调。

    Extension fields(扩展字段)

    Default values(默认值)

    Protobuf 有两种语法变体, proto2 和 proto3。在proto2有用户可定义的默认值,可以在.proto文件中给出:

    message MyMessage {
        optional bytes foo = 1 [default = "ABC\x01\x02\x03"];
        optional string bar = 2 [default = "åäö"];
    }
    
    • 1
    • 2
    • 3
    • 4

    Nanopb将为默认值生成静态初始化和运行时初始化。在myprotob .pb.h中有一个#define MyMessage_init_default{…}可以用来将整个消息初始化为默认值:

    MyMessage msg = MyMessage_init_default;
    
    • 1

    除此之外,pb_decode()将在运行时将消息字段初始化为默认值。如果不希望这样做,可以使用pb_decode_ex()。

    Message framing

    Return values and error handling

    static assertions

    参考文献

    protobuf的Required,Optional,Repeated限定修饰符

  • 相关阅读:
    Zookeeper和Nacos的区别
    高等代数_证明_不同特征值的特征向量线性无关
    【Linux练习生】线程安全
    Python Prim 算法 生成迷宫
    python使用pip命令安装出错:Could not fetch URL https://pypi.org/simple/selenium/
    分布式唯一ID几种生成方案
    MySql 各种 join
    ChatGPT及GIS、生物、地球、农业、气象、生态、环境科学领域案例
    指针深入了解——函数指针,函数指针数组,指向函数指针数组的指针。
    机械键盘怎么选?5年老码农倾情推荐!
  • 原文地址:https://blog.csdn.net/qq_18661919/article/details/136509115