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

通信协议理论

  • protobuf
    • 简述
    • 使用简介
      • [proto 文件](#proto 文件)
      • streams
        • [output streams](#output streams)
        • [input streams](#input streams)
      • [Data types(数据类型)](#Data types(数据类型))
      • [Field callbacks(字段回调)](#Field callbacks(字段回调))
      • [Encoding callbacks(编码回调)](#Encoding callbacks(编码回调))
      • [Message descriptor(信息描述)](#Message descriptor(信息描述))
        • [三个关键字required、optional、repeated 的理解(这个protobuf的关键字,nanopb不支持)](#三个关键字required、optional、repeated 的理解(这个protobuf的关键字,nanopb不支持))
      • Oneof(多中呈一,可理解是一种联合体)
      • [Extension fields(扩展字段)](#Extension fields(扩展字段))
      • [Default values(默认值)](#Default values(默认值))
      • [Message framing](#Message framing)
      • [Return values and error handling](#Return values and error handling)
      • [static assertions](#static assertions)
    • 参考文献

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文件。使用生成器选项文件,可以设置字段的最大大小,进而静态申请其内存。

cpp 复制代码
# 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
cpp 复制代码
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即可。

cpp 复制代码
/* 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。

cpp 复制代码
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。

cpp 复制代码
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 文件中:

cpp 复制代码
message Person {
    message PhoneNumber {
        required string number = 1 [(nanopb).max_size = 40];
        optional PhoneType type = 2 [default = HOME];
    }
}

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

cpp 复制代码
#define Person_PhoneNumber_FIELDLIST(X, a) \
X(a, STATIC,   REQUIRED, STRING,   number,            1) \
X(a, STATIC,   OPTIONAL, UENUM,    type,              2)

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

cpp 复制代码
PB_BIND(Person_PhoneNumber, Person_PhoneNumber, AUTO)

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

cpp 复制代码
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中通过宏生成的。

cpp 复制代码
/* .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采用了两个结构体,一个用于数据结构,一个用于数据结构的描述。举例如下:

cpp 复制代码
/* 数据结构 */
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(多中呈一,可理解是一种联合体)

举例子:

cpp 复制代码
/* .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"表示哪个字段被实际设置。用户需要使用正确的字段标签手动设置字段:

cpp 复制代码
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文件中给出:

cpp 复制代码
message MyMessage {
    optional bytes foo = 1 [default = "ABC\x01\x02\x03"];
    optional string bar = 2 [default = "åäö"];
}

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

cpp 复制代码
MyMessage msg = MyMessage_init_default;

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

Message framing

Return values and error handling

static assertions

参考文献

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

相关推荐
AORO_BEIDOU9 小时前
抢抓5G机遇,AORO A23防爆手机如何直击园区巡检挑战?
大数据·5g·智能手机·信息与通信
陌夏微秋1 天前
51单片机基础02 动态数码管显示-并串转换
arm开发·单片机·嵌入式硬件·51单片机·硬件工程·信息与通信·信号处理
陌夏微秋1 天前
51单片机基础01 单片机最小系统
单片机·嵌入式硬件·51单片机·硬件工程·信息与通信
啤酒泡泡_Lyla1 天前
现代无线通信接收机架构:超外差、零中频与低中频的比较分析
笔记·信息与通信
一个通信老学姐2 天前
专业140+总分410+东北大学841考研经验东大电子信息与通信工程通信专业基础真题,大纲,参考书
考研·信息与通信·信号处理·1024程序员节
GCKJ_08243 天前
观成科技:Vagent注入的内存马加密通信特征分析
科技·网络协议·信息与通信
一个通信老学姐4 天前
专业140+总分430+复旦大学875信号与系统考研经验原957电子信息通信考研,真题,大纲,参考书。
考研·信息与通信·信号处理·1024程序员节
一个通信老学姐4 天前
专业140+总分400+南京大学851信号与系统考研经验南大电子信息通信工程集成电路,真题,大纲,参考书。
考研·信息与通信·信号处理·1024程序员节
晓琴儿5 天前
C++使用开源ConcurrentQueue库处理自定义业务数据类
c++·rocketmq·信息与通信·concurrentqueue
浙江赛思电子科技有限公司5 天前
金融领域时间同步解决方案
大数据·人工智能·网络安全·阿里云·金融·信息与通信·信号处理