通信协议理论
- protobuf
- 简述
- 使用简介
- proto 文件
- 为了nanopb 编译.proto文件
- 修改生成器行为
- streams
- output streams
- input streams
- Data types(数据类型)
- Field callbacks(字段回调)
- Encoding callbacks(编码回调)
- Message descriptor(信息描述)
- 三个关键字required、optional、repeated 的理解(这个protobuf的关键字,nanopb不支持)
- Oneof(多中呈一,可理解是一种联合体)
- Extension fields(扩展字段)
- Default values(默认值)
- Message framing
- Return values and error handling
- 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文件。使用生成器选项文件,可以设置字段的最大大小,进而静态申请其内存。
# Foo.proto
message Foo {required string name = 1;
}# Foo.options
Foo.name max_size:16
streams
回调的通用准则:
- IO有错误,编码和解码进程立即终止
- 用 ”state“ 存储自己的数据,例如文件描述符
- 通过pb_write 和 pb_read 更新 bytes_written 和 bytes_left
- 回调也可用于次数据流,,结构内值和初始流近似
- 总是读取或写入所请求的完整长度的数据
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限定修饰符》