网络通信工程:UDP → TCP → CAN/SocketCAN → ZMQ → Protobuf
1、UDP
1、窗口重排是什么?
假设收到:
100
101
103
102
104
不能收到 103 就马上输出。
因为:
102
可能只是晚了一点。
所以程序可以建立:
重排窗口
例如:
期待:102
当前收到:
103
104
等待102
等:
102
来了:
102
103
104
再依次输出。但是不能无限等
这是实时系统里的关键。
假设:
期待102
但是:
102永远丢了
如果一直等:
系统卡死
所以必须有:
超时
于是:
收到103
↓
等待102
↓
超过窗口/超时
↓
判断102缺失
↓
继续处理103
这就是:
实时性和完整性的权衡。
UDP
↓
收到数据
↓
解析帧号
↓
进入有界队列
↓
重排窗口
↓
判断缺帧
↓
去重
↓
合并
↓
输出
2、CRC32在这里干什么?
Payload
↓
CRC32
↓
一个校验值
例如:
Payload A
↓
CRC = 123456
如果另一份数据:
Payload B
↓
CRC = 789012
说明内容不同。
因此可以快速发现:
同一个Frame ID
+
不同Payload
3、队列水位又是什么?
例如:
队列容量 = 256
当前:
队列里有180个
那么:
watermark = 180
如果长期:
200
220
240
250
255
说明:
生产速度明显快于消费速度。
这时候即使网络没问题:
系统也快撑不住了。
所以:
网络监控
+
队列监控
需要一起看。
2、 TCP:从"能通信"到"工程上可靠"
1. 应用层定义"消息边界"
这是 TCP 工程最核心的知识之一。
常见方法:
方法 A:固定长度
例如:
每条消息固定 64 bytes
那么:
每收到 64 bytes
↓
一条完整消息
适合固定结构的数据。
方法 B:特殊结束符
例如:
HELLO\n
WORLD\n
看到:
\n
就知道一条消息结束。
适合文本协议。
方法 C:长度 + Payload
机器人项目里非常常见:
┌────────────┬──────────────────┐
│ length │ payload │
│ 4 bytes │ N bytes │
└────────────┴──────────────────┘
例如:
[100][Protobuf数据100 bytes]
接收端:
先收 4 bytes
↓
知道 payload = 100 bytes
↓
继续接收 100 bytes
↓
得到完整消息
↓
Protobuf deserialize
这就和你之前学的:
TCP
↓
Protobuf
↓
反序列化
真正连起来了。
2. TCP 心跳
进一步可以设计:
Client ───── heartbeat ─────→ Server
Client ←──── heartbeat ack ── Server
例如:
PING
↓
PONG
如果连续很多次:
PING
↓
没有 PONG
↓
timeout
↓
认为连接异常
所以:
TCP连接状态
+
应用层心跳
可以共同判断通信状态。
3. 自动重连
真正的机器人程序通常不会只写:
connect();
而是:
connect
↓
成功 ─────→ communication
│
失败
↓
retry
↓
retry
↓
retry
断线:
communication
↓
disconnect
↓
cleanup
↓
reconnect
↓
connected
这里又出现了一个你之前 C++ 学过的东西:
生命周期管理
TCP connection 本身就可以看成一个生命周期:
Disconnected
↓
Connecting
↓
Connected
↓
Error
↓
Disconnected
这和你后面 EtherCAT、ROS2 lifecycle、设备驱动状态管理会产生非常强的联系。
3、CAN:从 CAN 总线到 Linux SocketCAN
你先建立一个整体认识:
CAN 总线
↓
CAN Frame
↓
CAN ID + Data
↓
设备之间通过 ID 区分消息
↓
Linux SocketCAN
↓
candump / cansend
↓
C/C++ 程序
↓
电机 / 驱动器 / 传感器
1. CAN 到底是什么?
CAN 可以简单理解成:
多个设备共用一条通信总线,大家都可以发送消息。
例如:
┌── 电机1
│
CAN Bus ├── 电机2
│
├── 编码器
│
└── 控制器
不像 TCP:
电脑 ─────────→ 设备
CAN 更像:
┌── Device A
│
CAN ────┼── Device B
│
└── Device C
大家挂在同一条总线上。
CAN有一个非常重要的特点:
多个设备可以同时尝试发送。
比如:
设备A → ID 0x100
设备B → ID 0x200
设备C → ID 0x300
如果同时发送怎么办?
CAN通过 ID仲裁解决。
核心规律先记:
ID越小,优先级越高。
所以:
0x100
0x200
0x300
竞争时:
0x100
↓
优先发送
2. CAN ID 是干什么的?
这里非常容易和 TCP/IP 搞混。
TCP:
IP + Port
CAN:
CAN ID
CAN ID 可以帮助设备判断:
这是什么消息?
例如:
0x201 → 电机1状态
0x202 → 电机2状态
0x301 → 电机1控制
具体 ID 含义由具体协议决定。
所以:
CAN本身
↓
只提供CAN Frame
上层协议
↓
规定0x201代表什么
规定Data每个Byte代表什么
这就是:
CAN ≠ 电机协议。
3. DLC 是什么?
你简历里如果看到:
DLC
它表示:
Data Length Code
简单理解就是:
这条 CAN Frame 里面有多少数据。
例如:
ID: 0x201
DLC: 8
Data:
11 22 33 44 55 66 77 88
表示:
8 bytes
注意:
DLC 是 CAN 帧层面的数据长度描述,不等于某个具体应用协议的数据含义。
4. Linux 为什么叫 SocketCAN?
Linux 不需要你自己重新设计一套:
CAN驱动
CAN接收
CAN发送
Linux 内核提供了:
SocketCAN
它把 CAN 接口统一成 Linux 网络编程风格的接口。
所以你可以把它理解成:
CAN硬件
↓
Linux CAN Driver
↓
SocketCAN
↓
Linux Socket API
↓
你的 C/C++ 程序
你在 Linux 里看到:
can0
这时候不要认为:
"can0就是CAN协议。"
不是。
可以理解成:
CAN硬件
↓
Linux CAN驱动
↓
SocketCAN
↓
Linux Socket API
↓
你的C/C++程序
所以程序可以通过类似 Socket 的方式操作 CAN。
例如你以前看到:
candump can0
就是:
从 Linux 的
can0接口监听 CAN 数据。
而:
cansend can0 123#11223344
就是:
往 CAN 总线上发送一帧数据。
5. candump 是干什么的?
在 Linux 上,调试 CAN 时非常常用。
例如:
candump can0
意思可以简单理解成:
监听 can0 上收到的 CAN Frame。
如果总线上有数据,你可能看到类似:
can0 201 [8] 11 22 33 44 55 66 77 88
你可以把它理解成:
接口:can0
ID:201
DLC:8
Data:
11 22 33 44 55 66 77 88
6. cansend 是干什么的?
和 candump 对应:
cansend can0 201#1122334455667788
意思就是:
通过 can0
发送 ID = 0x201
Data = 11 22 33 44 55 66 77 88
所以最基本的 CAN 调试习惯:
candump
↓
看设备有没有发数据
cansend
↓
主动给设备发送数据
7. CAN 和 EtherCAT 不要混淆
这非常容易混。
你可以先粗略理解:
CAN
CAN
↓
共享总线
↓
一帧一帧发送
EtherCAT
EtherCAT
↓
工业实时以太网
↓
Master
↓
多个Slave
EtherCAT更强调:
实时
同步
周期通信
大量设备
高效率
所以机器人多关节控制里,经常看到:
EtherCAT
你现在可以这样建立关系:
CAN
│
├── CAN Frame
├── ID
├── DLC
└── Data
而:
EtherCAT
│
├── Master
├── Slave
├── PDO
├── SDO
├── Distributed Clocks
└── Process Data
它们都是工业通信技术,但体系结构不同。
再往上:
CAN
↓
CANopen
↓
CiA402
↓
电机控制
而你的另一条技术链:
EtherCAT
↓
IgH Master
↓
PDO / SDO
↓
CiA402
↓
CSP
↓
电机控制
所以 CiA402 可以出现在不同底层通信技术之上,不要把"CAN"和"CiA402"当成同一个层级的东西。
4、 ZMQ:从 Socket 到消息通信
先建立一张图:
TCP / UDP
↓
原始通信能力
↓
ZMQ
↓
消息模式 + 队列 + 连接管理
↓
应用程序
所以:
ZMQ 不是替代 TCP/UDP 的另一种物理网络协议。
它更像是:
把底层网络通信封装成更好用的消息通信模型。
你可以先记住一句:
ZMQ 的核心不是"怎么发 TCP 数据",而是"程序之间采用什么消息通信关系"。
例如:
发布者
↓
多个订阅者
或者:
任务生产者
↓
任务队列
↓
多个消费者
或者:
Client
↓
Server
↓
Response
这些就是 ZMQ 的通信模式。
1. PUB / SUB
这是机器人项目里非常容易遇到的一种。
┌── Subscriber A
│
Publisher ───┼── Subscriber B
│
└── Subscriber C
例如你的动捕系统:
MotionInput
↓
Publisher
↓
┌───┼────┐
↓ ↓ ↓
GMR MuJoCo Logger
MotionInput 发布:
HumanPose
多个模块都可以订阅。
Publisher
↓
┌──┼──┐
↓ ↓ ↓
S1 S2 S3
一个发布者:
PUB
多个订阅者:
SUB
典型用途:
机器人状态广播
传感器数据广播
日志
监控
PUB 做什么?
PUB:
发布消息。
例如:
HumanPose
Frame 10001
Frame 10002
Frame 10003
SUB 做什么?
SUB:
订阅自己感兴趣的消息。
例如:
SUB
↓
HumanPose
或者按照 topic/filter:
mocap/
robot/
diagnostics/
发布者通常不知道具体有多少订阅者。
所以它很适合:
一个数据源
↓
多个数据消费者
例如:
Motion Capture
↓
ZMQ PUB
↙ ↓ ↘
GMR MuJoCo Logger
这就是非常典型的解耦。
MotionInput 不需要知道:
到底谁在使用我的数据?
它只负责:
发布数据
2. PUSH / PULL
这个模式和 PUB/SUB 不一样。
它更像:
任务流水线。
Producer
↓
PUSH
↓
Queue
↓
PULL
↓
Worker
例如:
Frame Processing
↓
PUSH
↓
┌────┼────┐
↓ ↓ ↓
Worker1 Worker2 Worker3
适合:
任务处理
数据处理
并行计算
PUSH
↓
┌────────┐
↓ ↓
PULL PULL
更像:
任务分发。
例如:
任务生产者
↓
PUSH
↓
任务队列
↓
┌──┴──┐
↓ ↓
Worker1 Worker2
适合:
任务处理
计算任务
工作线程
3. PUB/SUB 和 PUSH/PULL 不要混
简单记:
PUB/SUB
=
广播
PUSH/PULL
=
分发任务
例如:
一个人体姿态
↓
GMR + MuJoCo + Logger
更像:
PUB/SUB
而:
1000个待处理Frame
↓
多个Worker分担
更像:
PUSH/PULL
4. REQ / REP
这是比较接近传统:
Client → Server
Server → Client
例如:
Client
│
│ request
↓
Server
│
│ response
↓
Client
可以用于:
查询
控制命令
配置
RPC风格接口
例如:
Client:
"读取当前机器人状态"
↓
Server:
"当前状态 = ENABLED"
Client
REQ
↓
REP
Server
典型:
请求
↓
处理
↓
响应
例如:
获取机器人状态
↓
返回状态
生产者
Xsens / Noitom
↓
UDP
↓
MotionInput
↓
Queue
↓
消费者
GMR
↓
MocapFrame
↓
ZMQ
↓
MuJoCo
5. ZMQ 的几个重要对象
Context
可以理解成:
ZMQ运行环境
一个程序通常先创建 Context。
Socket
不是简单等同于 TCP socket。
ZMQ Socket 是:
一种特定通信模式的消息端点。
例如:
PUB socket
SUB socket
PUSH socket
PULL socket
REQ socket
REP socket
Message
ZMQ 面向的是:
Message
而不是让你直接处理 TCP 字节流。
6. HWM 是什么?
HWM:
High Water Mark
简单理解:
给队列设置一个上限。
例如:
Queue
│
├── msg
├── msg
├── msg
├── ...
└── HWM
达到限制以后,ZMQ 会根据具体 socket 类型和配置采取相应行为。
你真正应该记住的是:
无限队列
↓
可能无限积压
↓
延迟越来越大
而实时机器人系统通常更关心:
当前最新数据
而不是:
几秒钟以前还没处理完的数据
这和你之前学习的:
queue_capacity = 256
queue watermark
是同一个工程思想。
7. ZMQ 和 Protobuf
这两个东西解决的是不同问题。
ZMQ
↓
负责"怎么把消息送过去"
Protobuf
↓
负责"消息里面的数据长什么样"
例如:
MotionInput
↓
Protobuf Serialize
↓
HumanPose binary message
↓
ZMQ
↓
网络/进程间传输
↓
ZMQ Receive
↓
Protobuf Deserialize
↓
HumanPose
所以:
ZMQ ≠ 数据格式。
Protobuf ≠ 网络传输框架。
两者组合起来非常自然。
Protobuf
┌────────────────┐
│ 数据怎么表示? │
└───────┬────────┘
↓
GMR ───────→ MocapFrame
↓
Serialize
↓
ZMQ
↓
Network
↓
ZMQ
↓
Parse
↓
MocapFrame
↓
MuJoCo
5、 Protobuf:定义"消息里面装什么"
先记住一句话:
Protobuf 主要解决"数据怎么定义、怎么序列化和反序列化",而不是负责网络传输。
所以:
UDP / TCP / ZMQ
↓
负责"把数据送过去"
↓
Protobuf
↓
负责"数据长什么样"
1. .proto 是什么?
你可以把 .proto 理解成:
通信数据结构的"接口定义文件"。
例如:
message HumanPose {
uint64 timestamp = 1;
uint64 frame_id = 2;
repeated float joint_positions = 3;
}
这句话是在定义:
HumanPose
│
├── timestamp
├── frame_id
└── joint_positions
注意:
= 1
= 2
= 3
不是数组下标。
它们是 Protobuf 的 field number(字段编号)。
2. message 是什么?
message 就相当于:
定义一个数据结构。
比如:
message MotorState {
int32 motor_id = 1;
float position = 2;
float velocity = 3;
}
逻辑上就是:
MotorState
│
├── motor_id
├── position
└── velocity
在 C++ 中,protoc 会根据这个定义生成对应代码。
于是你的业务代码不需要自己手工处理:
第1个字节是什么
第2个字节是什么
......
3. protoc 是什么?
你写:
id="g3oz3k"
human_pose.proto
机器并不能直接拿这个 .proto 文件通信。
需要:
.proto
↓
protoc
↓
生成代码
例如 C++:
human_pose.proto
↓
protoc
↓
human_pose.pb.h
human_pose.pb.cc
你的 C++ 程序再使用这些生成代码。
所以:
.proto
↓
协议定义
protoc
↓
代码生成
.pb.h / .pb.cc
↓
C++使用
4. Serialize 是什么?
假设你的程序里面已经有:
HumanPose
对象:
timestamp = 1000
frame_id = 123
joint_positions = [...]
现在准备通过网络发送。
需要:
对象
↓
Serialize
↓
二进制 bytes
这就是:
序列化。
5. Deserialize 是什么?
接收端:
网络 bytes
↓
Deserialize
↓
HumanPose
这就是:
反序列化。
所以完整流程:
发送端:
HumanPose
↓
Serialize
↓
bytes
↓
ZMQ / TCP / UDP
↓
网络
接收端:
网络
↓
bytes
↓
Deserialize
↓
HumanPose
6. 现在把四个技术彻底串起来
这是你这一部分最重要的一张图:
应用数据
│
↓
Protobuf
│
Serialize
│
↓
bytes
│
┌─────────┼─────────┐
↓ ↓ ↓
UDP TCP ZMQ
│ │ │
↓ ↓ ↓
网络传输 字节流 消息通信
│ │ │
└─────────┼─────────┘
↓
接收 bytes
↓
Protobuf
↓
Deserialize
↓
应用数据
7. Protobuf 和 JSON 的区别
你还需要知道一个实际工程区别。
JSON:
{
"frame_id": 123,
"position": 1.2
}
优点:
人容易看懂
调试方便
缺点:
数据体积通常更大
解析开销通常更高
Protobuf:
结构化二进制数据
更适合:
高频
大量数据
跨进程
跨语言
机器人实时数据链路
所以:
Web API
→ JSON 很常见
机器人高频数据
→ Protobuf 很常见
不是说 JSON 不能用于机器人,而是根据场景选择。