C++ / Robotics · 系统工程 · LESSON 23

网络、IPC 与进程边界

比较 socket、共享内存、CLI 和消息中间件,选择合适的组件连接方式。

18 分钟networking · ipc · protocols

网络、IPC 与进程边界

JS/TS 项目常用 HTTP 或 WebSocket;机器人系统还会在节点间使用 ROS 2、Unix domain socket、共享内存或设备驱动 API。选择连接方式要先定义消息大小、频率、允许延迟、断连动作和安全边界。网络字节流没有消息边界,进程退出也不会自动通知业务状态,协议必须把这些故障写出来。

学习目标

你将学会区分 request/response、stream 和共享内存;设计版本、长度和序号字段;处理 partial read、timeout、断连和重连;把 Web 控制台、网关和 ROS 2 节点的权限、生命周期和安全动作写进协议。

请求、流和共享内存的取舍

TRANSLATION LENS 同一个意图,两种工程表达 窄屏可左右滑动查看完整代码
JS / TS
const ws = new WebSocket(url);
ws.onmessage = event => handle(JSON.parse(event.data));
ws.send(JSON.stringify(command));
C++
TcpClient client{endpoint};
client.send(encode_frame(command));
if (auto frame = client.receive(timeout)) handle(decode_frame(*frame));

HTTP 适合配置和查询,WebSocket 适合浏览器持续看到状态,ROS 2 topic 适合机器人内部的类型化流。高频图像可能需要共享内存减少复制,但同步、权限、版本和崩溃恢复的成本会上升。实时控制不应直接信任一个可重放的 Web 请求,要增加序列号、超时、授权和失联安全动作。

帧格式比 send 一次更重要

#include <cstdint>
#include <vector>

struct FrameHeader {
  std::uint16_t version{};
  std::uint32_t payload_size{};
  std::uint64_t sequence{};
};

bool valid_header(const FrameHeader& header, std::size_t max_payload) {
  return header.version == 1 && header.payload_size <= max_payload;
}

真实网络代码不能直接把 FrameHeader 的内存布局当作线协议:padding、字节序和对齐可能不同,应逐字段按约定编码。接收端要先读固定头,再校验版本、长度和序号,最后读取完整 payload;部分读取、超时和断连都应是正常分支。解析器要限制最大长度,避免恶意或损坏消息让内存无限增长。

IPC 的故障模型

共享内存进程崩溃后可能留下半写数据,需要双缓冲、序号或原子发布协议;socket 断开后需要重连、退避和状态重置;CLI 子进程失败要检查退出码和 stderr。ROS 2 会提供发现与序列化,但 QoS 不会替你决定权限、版本兼容或业务重试。跨语言边界优先使用可抓包、可回放和可单测的格式。

编译、链接和运行时排错

undefined reference 先确认网络库 target 和平台库链接;编译过却收不到消息,检查监听地址、端口、协议版本和 framing。连接成功后偶发解析错误,打印 sequence、payload_size 和实际读取字节数。运行时超时要区分对端未发送、读取被信号打断和本地线程饥饿;不要无限重试,否则断连会拖死控制循环。

解析器先校验长度

bool valid_header(const FrameHeader& header, std::size_t max_payload) {
  return header.version == 1 &&
         header.payload_size <= max_payload &&
         header.payload_size > sizeof(std::uint64_t);
}

这段函数只验证 header,真正的 recv 仍需循环读取完整头和 payload,因为一次读取不保证得到一帧。测试版本错误、长度为 0、超上限、序号倒退和合法边界,确认不会提前分配超大内存。

partial read 和超时

bool read_exact(Socket& socket, std::span<std::byte> buffer,
                std::chrono::milliseconds timeout) {
  std::size_t offset = 0;
  while (offset < buffer.size()) {
    auto count = socket.receive(buffer.subspan(offset), timeout);
    if (!count) return false;
    offset += *count;
  }
  return true;
}

让 fake socket 依次返回 2、3 和剩余字节,验证解码器只收到完整帧;中途 timeout 要释放临时 payload 并记录 sequence。网络线程不能无限重试拖住实时控制线程,控制命令还要带有效期和权限。

JS/TS 迁移的协议反例

JSON.stringify 不会自动定义版本、单位和上限;WebSocket 的 onmessage 也不代表输入可信。不要把 TCP send 一次当成对端一次 recv,不要盲目重试运动命令,也不要把共享内存的可变对象当作天然安全。跨语言边界要能抓包、回放和单测。

进程边界的可观测性

记录 connection_idsequence、payload bytes、send/receive timestamp、parse_error、retry_count 和 heartbeat。网关将浏览器请求转成受限命令,控制节点验证序号与有效期,失联进入安全状态。集成测试用 fake peer 注入半包、坏版本、断连和延迟,确认故障不会阻塞控制循环。

迁移练习

为“浏览器调整机器人参数”“显示持续状态”“发送安全速度命令”分别选择 HTTP、WebSocket、ROS 2 或其他 IPC,并写出 framing、超时、重连和权限策略。实现一个固定头解析函数,拒绝错误版本和超过上限的 payload。

01
TRY IT YOURSELF

给机器人控制边界选协议

画出 Web 控制台、网关进程和控制节点的边界;为每条连接列出消息格式、断连动作、重试规则和可观测指标。

给我一点提示

持续状态和请求响应的生命周期不同;实时控制要有序列号、有效期和失联安全状态。

查看参考答案
配置查询可用 HTTP;持续状态可用 WebSocket 或 ROS 2 topic;速度命令经过网关并带序列号、有效期和权限,控制节点超时则进入安全输出。每条连接记录连接状态、读写超时、重连次数、解析失败和最后序号。
本节结论

网络和 IPC 的核心学习成果是故障边界可见、协议可验证。下一节会把 CMake 依赖和这些通信组件组织进 ROS 2 工作区。

延伸实践:让控制命令有生命周期

机器人控制消息不能只携带一个速度数值,还要说明它何时产生、何时失效、由谁发出以及是否已经被处理。可以把协议对象写成显式的数据结构,让网关和控制节点共享同一组校验规则。这样浏览器刷新、网络重连或旧消息重放时,控制节点仍能拒绝过期命令,而不是把旧速度继续执行。

struct Command {
  std::uint64_t sequence{};
  std::int64_t expires_at_ns{};
  float linear_x{};
};

bool acceptable(const Command& command, std::int64_t now_ns) {
  return command.sequence > 0 &&
         command.expires_at_ns >= now_ns &&
         std::isfinite(command.linear_x) &&
         std::abs(command.linear_x) <= 1.0F;
}

用固定的 now_ns 测试刚好未过期、刚好过期、重复序号、NaN 和超范围速度,预期只有合法命令进入控制队列。编译时打开 -Wall -Wextra -Wconversion,把浮点到整数或有符号/无符号比较的警告当成协议审查线索。运行集成测试时模拟半包、延迟和重连,验证故障会回到安全状态;不要用 sleep 代替明确的时钟和超时输入。

用解析状态区分半包与非法帧

流式读取一次可能只拿到头部的一部分,也可能拿到多帧拼接后的字节。解析器应返回三种明确结果:还需要更多字节、得到一帧、输入非法;不能把“不完整”误报为“连接坏了”,也不能把非法长度当作等待更多数据。下面展示一个小端 32 位长度头的检查顺序,生产协议还应固定字节序、版本和校验策略。

#include <cstddef>
#include <cstdint>
#include <vector>

enum class FrameState { NeedMore, Ready, Invalid };
struct FrameView { const std::byte* data{}; std::size_t size{}; };

FrameState inspectFrame(const std::vector<std::byte>& bytes,
                        std::size_t maxPayload, FrameView& view) {
  if (bytes.size() < 4) return FrameState::NeedMore;
  const auto b0 = std::to_integer<std::uint8_t>(bytes[0]);
  const auto b1 = std::to_integer<std::uint8_t>(bytes[1]);
  const auto b2 = std::to_integer<std::uint8_t>(bytes[2]);
  const auto b3 = std::to_integer<std::uint8_t>(bytes[3]);
  const std::uint32_t length = static_cast<std::uint32_t>(b0)
      | (static_cast<std::uint32_t>(b1) << 8)
      | (static_cast<std::uint32_t>(b2) << 16)
      | (static_cast<std::uint32_t>(b3) << 24);
  if (length > maxPayload) return FrameState::Invalid;
  if (bytes.size() - 4 < length) return FrameState::NeedMore;
  view = {bytes.data() + 4, length};
  return FrameState::Ready;
}

解析顺序先确保有完整头,再检查上限,最后才比较载荷字节数;由于先检查 bytes.size() >= 4,减去头长不会下溢。成功返回的 FrameView 借用 vector 内存,调用方不能在消费完 view 前清空或扩容底层缓冲区。

运行验证:固定字节样例

准备少于 4 字节的输入、完整头但不完整载荷、长度超过上限和完整合法帧。预期分别为 NeedMoreNeedMoreInvalidReady;合法 payload 长度为 0 时也要明确协议是否允许空帧。测试每一步解析后保留未消费尾部,确认下一个 frame 的字节不会被丢弃。

若出现间歇性错帧,记录连接 id、已读字节数、解析状态和缓冲区上限,再用相同字节序列回放。不要直接把网络分块边界当作消息边界;TCP 只提供有序字节流,发送端一次 write 与接收端一次 read 并不一一对应。

写侧也需要处理 partial write

发送端的 write/send 也可能只接受部分字节。调用方要从已写偏移继续发送剩余部分,并处理 EINTREAGAIN、对端关闭和 deadline;不能每次都从 buffer 起点重发,否则接收端会看到重复前缀。非阻塞 socket 遇到暂时不可写,应等待可写事件并检查取消/超时,而不是忙循环占满 CPU。

把读写状态机拆成“准备帧、发送偏移、等待可写、接收累积、解析完整帧、消费后回收”几个阶段,每个阶段都有一个超时和关闭出口。对大 payload 再定义最大并发帧数与背压策略;只限制单帧大小仍挡不住许多合法帧同时排队耗尽内存。

端到端故障演练

用 fake peer 分别模拟延迟头部、断在 payload 中间、重复 sequence、超限 payload 和写入被打断。验证客户端不会交付半帧、不会无限重试,并在断连后将连接状态置为不可用;机器人控制面还应验证旧命令过期后转入预定安全状态。保留字节 fixture 和固定时钟,让以后调整协议或缓冲区时可以离线回放。

小结补充

从 JS/TS 的 fetch、WebSocket 和子进程迁移到 C++ 网络边界,真正需要迁移的是“输入不可信、读取可能不完整、资源必须释放、失败必须可恢复”的工程模型。下一节进入 ROS 2 workspace 时,Topic 和 Service 仍然要用同样的序号、时间、版本和失败证据来设计。

再做一次离线演练:让 fake peer 先发送一个合法 header 的一半,再等待超时,随后发送错误版本和超长 payload。记录每一步返回的错误类型、已读取字节数和连接状态,确认解析器没有把半包当成有效命令,也没有在拒绝后继续消费旧 buffer。这个练习会把网络协议从“能连上”提升为“在异常输入下仍能安全停止”。最后把这组日志保存为测试 fixture,确保以后修改 framing 或重连策略时仍能重放。

FURTHER READING

延伸阅读

先完成本节练习,再用这些资料查阅完整 API 和真实项目组织方式。

当前学习阶段系统工程
0/7

本节是阶段检查点。完成练习后,再进入下一阶段。