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

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;
};

如果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};
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;
};

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};

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];
    }
}

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

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

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

PB_BIND(Person_PhoneNumber, Person_PhoneNumber, AUTO)

这个宏会组合生成 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】:原来这就是我找不到生成的消息描述结构的原因,只找到了在.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)

【注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;

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;

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

MyMessage msg = MyMessage_init_zero;
msg.payload.msg2.value = true;
msg.which_payload = MyMessage_msg2_tag;

不论是 “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 = "åäö"];
}

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

MyMessage msg = MyMessage_init_default;

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

Message framing

Return values and error handling

static assertions

参考文献

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值