440
社区成员




重点:
1.掌握通信协议设计原理
2.理解protobuf为什么快
3.掌握protobuf怎么在工程使用
协议(Protocol) 是一套规则或约定,用于规范不同实体之间通信的格式、内容、顺序以及如何处理错误等。
在计算机系统中:
比如:
客户端与服务端之间的通信,就是一种进程间通信(跨主机或本地)。进程间通信(IPC)包括:
不管哪种,只要两个独立进程之间要交换数据,就必须达成“怎么解释这些数据”的共识 —— 即需要 协议
原因 | 描述 |
---|---|
效率需求 | 可能只需要几个字节传输,不需要完整 HTTP 报文(太臃肿) |
场景定制 | 系统消息结构是固定/可预测的,用现成协议反而复杂 |
资源受限 | 在嵌入式或无人机/无人船等平台,资源宝贵,自己设计协议可以压缩带宽和内存使用 |
安全性 | 有些自定义协议对攻击者来说是“黑盒”,可降低被攻击面 |
功能扩展方便 | 可以灵活设计ACK机制、心跳包、数据校验等机制 |
举例:简单的自定义协议
[HEAD][TYPE][LEN][DATA][CRC]
这种协议既精简又高效,适用于 USV/UAV 协同这类低时延、低资源的跨平台通信。
优点:
缺点:
<SOF>数据内容<EOF>
举例:
$GPRMC,.....*校验码\r\n
<STX>...<ETX>
优点:
缺点:
| HEAD | TYPE | LENGTH | BODY | CRC |
举例:
优点:
缺点:
原理:
[4字节长度][序列化后的内容]
举例:
优点:
缺点:
头有十六个字节
8. body
头有十八个字节
8. body
Nginx 自身协议不多,但它支持例如:
实践中经常会在 Header 中加自定义字段来传递用户/设备上下文
两个换行符之后就是body,复杂
缺点:
*
、$
等表示参数数量/长度;优点:
序列化(Serialization)
将内存中的对象(数据结构)转换为可存储或传输的格式。
常用于把:
比如把结构体 Person{name="Tom", age=25}
转换成:
"{"name":"Tom","age":25}"
(JSON)0A 03 54 6F 6D 10 19
(protobuf 编码)反序列化:
把序列化后的格式重新转换回内存中的对象。
它是序列化的逆过程,接收到字节流或字符串后重新还原成程序对象
场景一:网络通信
场景二:数据存储
场景三:跨语言/平台通信
序列化格式 | 可读性 | 压缩效率 | 跨语言 | 编码速度 | 使用场景举例 |
---|---|---|---|---|---|
JSON | ✅ 高 | ❌ 一般 | ✅ 强 | ✅ 快 | Web API、配置文件 |
XML | ✅ 高 | ❌ 差 | ✅ 强 | ❌ 慢 | 配置、SOAP协议 |
protobuf | ❌ 差 | ✅ 高 | ✅ 强 | ✅ 非常快 | 微服务、IoT传输 |
JSON(JavaScript Object Notation)
{"name":"Alice","age":23}
XML(eXtensible Markup Language)
<person><name>Alice</name><age>23</age></person>
Protocol Buffers(Google 出品)
message Person {
string name = 1;
int32 age = 2;
}
由 Google 提出的一种语言无关、平台无关、可扩展的结构化数据序列化方法
IDL:接口描述语言‘
(1)编写 .proto
文件(IDL,接口描述语言)
syntax = "proto3";
message Person {
int32 id = 1;
string name = 2;
string email = 3;
}
(2)用 protoc
编译器生成语言对应的代码
protoc --cpp_out=. person.proto
生成文件包括:
person.pb.h
:头文件,包含类声明person.pb.cc
:源文件,包含实现(3)在代码中使用
#include "person.pb.h"
Person p;
p.set_id(123);
p.set_name("Alice");
p.set_email("alice@example.com");
string data;
p.SerializeToString(&data); // 序列化成二进制字符串
Person p2;
p2.ParseFromString(data); // 反序列化回来
(4)编译时记得加上 .pb.cc
g++ main.cpp person.pb.cc -lprotobuf -o myprog
1)只传编号,不传字段名
name = 2
)"name":"Alice"
要节省非常多2)可变长度编码(Varint)
int32 id = 1;
→ 值为 5
时只用 1 个字节int32
总是占 4 字节3)不传默认值
string name = ""
不会写入输出数据
import "auth/user.proto";
message LoginRequest {
auth.User user = 1;
}
例如:
message Person {
int32 id = 1;
string name = 2;
}
会被编码为:
[Tag=8][Varint=id] [Tag=18][Length][String=name]
2.为什么要用变长编码
原理(Base128 编码):
数值 | 二进制 | Base128 表示(字节流) |
---|---|---|
1 | 00000001 | 0x01 |
300 | 100101100 | 0xAC 0x02 |
优点:
缺点:
和固定长度编码区别
类型 | 描述 | 优点 | 缺点 | 场景 |
---|---|---|---|---|
可变长度(Varint) | int32、int64、bool 等 | 小数字更节省 | 编解码复杂,性能略差 | id、flag、状态位、枚举等 |
固定长度 | fixed32、fixed64 | 编解码快,CPU 缓存友好 | 小数字浪费空间 | 经纬度、时间戳、浮点数、hash值等 |
message User {
uint32 id = 1;
string name = 2;
string email = 3; // 新增字段
}
email
email
message User {
uint32 id = 1;
// 删除 name = 2 (错误❌)
}
正确方式是:
message User {
uint32 id = 1;
reserved 2;
reserved "name";
}
// 老版本
string nickname = 2;
// 新版本(错误)
string nickname = 4; // ❌ 改了 tag 编号
老版本程序会错误地把别的字段解析成 nickname
,数据混乱
向前兼容 vs 向后兼容
兼容方向 | 定义 | 例子 |
---|---|---|
向前兼容 | 新代码能读取老数据 | 老版本写的 protobuf,新代码能读 |
向后兼容 | 老代码能读取新数据(忽略新增字段) | 老版本代码能跑新版消息 |
Protobuf 默认支持 向后兼容,即新增字段自动被老版本忽略
一种节省空间并简化协议字段表示的方法
message Action {
oneof cmd {
string login = 1;
string logout = 2;
string heartbeat = 3; // ✅ 安全新增
}
}
兼容性说明:
heartbeat
login/logout
// 错误示范
message Action {
oneof cmd {
string login = 1;
// 删除 logout = 2 ❌
}
}
即使客户端不再使用某个字段,也不要删除,应该使用 reserved
:
reserved 2;
reserved "logout";
message.proto
syntax = "proto3";
package demo;
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
main.cpp
#include <iostream>
#include "message.pb.h"
int main() {
demo::HelloRequest req;
req.set_name("HIT");
demo::HelloReply rep;
rep.set_message("Hello " + req.name());
std::cout << rep.message() << std::endl;
return 0;
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(protobuf_demo)
#寻找系统中安装的 Protobuf 库
find_package(Protobuf REQUIRED)
# 编译 .proto 文件为 .pb.h 和 .pb.cc
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS message.proto)
#创建一个可执行程序 main,它会编译 main.cpp 和刚才生成的 .pb.cc 文件
add_executable(main main.cpp ${PROTO_SRCS} ${PROTO_HDRS})
#把 protobuf 的库链接进可执行程序中,比如 libprotobuf.a 或 libprotobuf.so
target_link_libraries(main PRIVATE ${Protobuf_LIBRARIES})
指定 .pb.h 的头文件路径,告诉编译器去哪里找
target_include_directories(main PRIVATE ${Protobuf_INCLUDE_DIRS})
mkdir build
cd build
cmake ..
make
./main
# 输出:Hello HIT