解读 110 · sim.rs:仿真 IO 后端
解读对象:
duck-control/src/sim.rs(441 行)
需要的前置:解读 07(RobotIo 接缝)、解读 09(谁在调用 read/write)
RemoteIo 是设计文档点名的第三个后端(sim.rs:3-7):身体在 MuJoCo 里,通过 RobotIo 接进来——"接缝之上的一切原样运行,分不出差别"。模块注释交代了三个形状决定:用 TCP,因为 unix 路径上限约 108 字节、仿真器要能跨容器与虚拟机被够到(:9-14);用 JSON,一拍约 1 KB,"能用 nc 读一帧、用二十行 Python 写回另一半"胜过两仓库共享内存布局(:16-22);仿真器经常不在(改鸭子数量就得重启),断线只报错、下拍重连,"控制循环就是重试定时器"(:24-30)。
1. 线上的词表:Request、SensorFrame、Ack
Request(sim.rs:59-72)六个变体对应 trait 的动作,#[serde(tag = "op")] 让每帧自带 "op":"read" 标签——"日志里不用解码器就能读一帧"(:56)。PROTOCOL = 1(:47)在握手时核对,不匹配时错误同时报出两个数字(:183-189):两端住在两个仓库,"仿真器太旧"和"守护进程太旧"是同一个症状。SensorFrame(:86-92)的字段就是 Sensors 要的单位;原则是(:82-84)"换算由仿真器做——这里再放一层翻译,就是第二处会漂移的知识"。Ack(:108-112)带 Option<String> 的 error,让"被拒的写"不冒充成功。
2. connect:懒连接与三道设置
RemoteIo 只有三个字段(sim.rs:115-120):地址、Option<Link>、只抱怨一次的开关。at(:130-136)只记名字不连接——"守护进程必须能先于仿真器启动"(:128-129),--sim 缺仿真器时只是报不健康,和真总线没上电一个待遇(见解读 111)。connect(:138-196)检查 link.is_none(),要连时:解析地址取第一个(:140-146);connect_timeout 用 200 ms 的 TIMEOUT(:54)——没人监听的端口立刻拒绝,静默丢包的主机不能把循环挂死(:152-158);然后三道设置(:160-165),第一道最要命:
// **Nagle 在这里是灾难性的,而且无声。** 它会把小写入攒起来等更多数据,
// 最多 ~40 ms——两拍——把每笔事务变成一次错过 deadline。
let _ = stream.set_nodelay(true);
握手发 Request::Hello{protocol, joints}(:176-182),版本不符即失败(:183-189);成功后记日志、清 complained、存下 Link(:191-194),读半边是 try_clone 出的 BufReader。
3. call/exchange:任何错误都拆线
call(:203-216)是所有请求的通道:connect().and_then(exchange),关键在错误处理(:205-214)——任何错误都把 link 置 None,不只是断连:"一帧解析不出来,意味着两端对消息从哪开始有了分歧,行协议除了重来无法重新同步"(sim.rs:199-202);complained 保证警告只发一次。exchange(:219-243)是行协议本体:一行 JSON 加换行(:220-224),read_line 返回 0 视为挂断(:231-236),解析失败报"答了读不懂的东西"(:237-242),错误都带 tag(request)(:245-254)指明步骤;acked(:256-261)把 Ack 的 error 变成 IoError::Bus。
4. trait 实现与"罐头仿真器"测试
RobotIo 实现里,read(:264-276)把 SensorFrame 逐字段搬进 Sensors;write/set_gain/set_torque(:278-293)发请求并检查 ack;reboot(:299-302)是全文最短的方法——仿真舵机没有锁存的硬件错误,直接 Ok(()),"刻意不占用线上操作,仿真器反正只能回一个 ack";slow_sensors(:304-310)取 SlowFrame。测试(:313-441)起真的 TcpListener 播放罐头台词(:324-346):"值得在这里测的东西就是 socket——挂断的对端、解析不了的帧、死后的第二条连接"。五个测试各钉一类故障:目标按关节顺序过线、版本不匹配报双方版本、拒绝是错误、重启后下一拍自动重连并重新握手(:410-427)、坏帧导致拆线重建(:430-440)。
你带走的收获
- 换算放在数据的源头:仿真器自己出"机器人的单位",翻译层只此一家。
- Nagle 算法对 20 ms 控制环是隐形杀手:
set_nodelay(true)是低延迟 TCP 第一课。 - 行协议一旦错位只能拆线重来——"解析失败即断连"比"带病续读"安全。
- 测网络代码要用真 socket 放故障剧本:挂断、坏帧、重启,trait 替身恰好挡在门外。