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限定修饰符

  • 5
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Linux C/C++后台架构开发是一门非常具有前景的技能,在互联网和各种电子设备中都得到广泛应用。因此,这门课程的成长体系很重要,能够立足于现有技术发展趋势,不断完善内容,使学员能够跟随市场需求进行技术升级。 首先,课程应该注重基础知识的讲解,包括Linux操作系统的基础知识、C/C++编程语言的基础知识等,这是后续学习的基础。其次,应该注重实战训练,通过项目的实践来加深对知识的理解,并促进学员的技能提升。同时,要合理结合课程的理论知识和实践操作,培养学员的动手能力和实际应用技能。 除此之外,课程还应该关注行业技术变化的趋势,不断更新课程内容,讲解新技术的应用,使学员能够跟随技术的发展趋势提升自己。同时,要注重培养学员的团队合作能力和创新能力,帮助他们更好地适应团队工作和市场需求。 总之,Linux C/C++后台架构开发成长体系课程需要注重基础知识的讲解、实战训练、行业技术变化的跟进和团队合作与创新能力的培养。通过这些方面的努力,才能使学员掌握实用的技术,具备市场竞争力,并有能力适应未来技术的发展趋势。 ### 回答2: 作为一种开源的操作系统,Linux的应用广泛,尤其在服务器端,被广泛应用于Web服务器、数据库服务器等。因此,当今各大企业都需要专业的Linux后台架构开发人员来维护服务器的安全、稳定和高效运行。 而C/C++作为一种高效、可靠的编程语言,被广泛应用于Linux系统编程,尤其是在高性能、实时应用和底层驱动方面。因此,具备C/C++编程能力的Linux后台架构开发人员具有较高的市场竞争力。 在成长体系课程方面,专业的培训机构可以提供基础和高级的C/C++编程语言学习,以及Linux系统编程相关知识扎实的培训。而在课程设置和教学方式上,应该采取理论与实践相结合的方式,让学员在编程实践中逐渐掌握并理解相关知识。同时,在应用开发的过程中,特别是在搭建后台架构时,需要学会合理设计系统架构,选择合适的开发工具和技术,并能有效管理和维护系统。 此外,针对行业发展趋势和技术更新,培训机构应当不断更新课程内容,结合最新的技术趋势,为学员提供更具有竞争力的技术挑战,并开展多种实战项目实践。让学员在实践中提升自己的技能和实践经验,不断提升自身的职业竞争力。 ### 回答3: Linux C/C++后台架构开发是一个广受欢迎的领域,无论是大型互联网企业还是中小型企业都需要有相应的开发团队和技术人才。开发人员需要具备扎实的C/C++编程基础,熟悉Linux操作系统的运行机制和性能优化,了解分布式系统架构和网络通信协议等知识,以及掌握一定的数据库开发和管理经验。 针对此领域的开发人员,成长体系课程可以提供以下培训内容: 一、Linux操作系统原理:Linux系统的运行机制,常用命令和工具的使用方法,文件系统和进程管理等。 二、C/C++编程:C/C++基础语法和编程规范,数据结构和算法,内存管理和锁机制等。 三、分布式系统架构:分布式系统的概念和架构,通信协议和数据传输方式,分布式存储和计算等。 四、网络通信:TCP/IP协议栈和网络编程,HTTP、Websocket等常用协议的使用和封装。 五、数据库开发和管理:常见数据库的概念和使用方法,SQL语言编写,数据库的设计和优化等。 通过以上培训内容的学习和实践,开发人员可以逐渐掌握Linux C/C++后台架构开发的技能和经验,不断提升能力和水平。同时,课程还将涉及团队协作和项目管理等方面的知识,培养学员的软技能和团队意识。最终,学员能够独立完成复杂的后台开发任务,为企业创造更大的价值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值