
现场一条永远发不出去的广播几年前做一个分布式采集系统主控通过广播通道向所有节点下发同步命令和参数。协议栈的单帧净荷上限是 31 字节这是链路层硬约束改不了。前期功能很简单同步命令就几个字节跑得好好的。后来需求加了一条广播下发一组通道配置参数。我把参数序列化后一算48 字节。当时没多想直接往发送接口里塞。发送函数返回成功我就以为完事了。测试的时候发现问题了。节点端有一部分能收到配置有一部分收不到能收到的里面数据还是截断的。用监听工具抓包只看到一帧长度顶格 31 字节后面 17 字节凭空消失。更麻烦的是这 31 字节里前几个字节被协议头占掉了真正到达应用层的有效数据更少。节点端拿着半截配置去解析有的直接校验失败有的解析出乱七八糟的参数把执行机构设到了错误状态。这个坑让我明白一件事广播发送接口返回成功不等于数据发出去了。长度超出承载上限时底层要么截断、要么直接丢而且往往不报错。先搞清楚“上限”到底卡在哪一层很多人一说分包上来就按应用层数据长度切。这是错的。实际能装多少有效数据要一层一层扣应用层有效载荷 链路层单帧净荷上限 - 链路层帧头 - 网络层/传输层头如果有 - 应用层协议头 - 可能的校验字段我那个项目里链路层 31 字节协议头占了 5 字节应用层协议头又占 3 字节实际每帧只能装 23 字节有效数据。48 字节要拆成 3 帧不是 2 帧。当初按 31 算怎么算都错。这里有个隐形陷阱不同协议栈对“净荷”的定义不一样。有的把帧头算进去有的不算。调试时不要凭记忆抓一帧实际数据从字节偏移上数确认应用层第一个有效字节到底在第几位。这个数错一位后面全错。分包不只是“切开”接收端要能拼回去分包发送本身不难难的是接收端怎么知道这些帧属于同一条消息、怎么知道拼完了、拼的顺序对不对。公开资料里讲分包通常只讲发送端切片接收端重组一笔带过。但工程上出问题十有八九出在重组环节。我见过几种常见的错误做法第一种靠“帧间隔”判断。发送端连续发几帧接收端认为间隔小于某个阈值的就是同一组。这在广播场景下极不可靠多个发送源、重传、信道拥塞都会打乱间隔。现象是偶尔把两条消息拼成一条数据错乱而且极难复现。第二种只带一个总长度字段不带序号。接收端收到第一帧知道总长但如果中间丢了一帧它不知道缺的是哪一段只能干等或者拼出错误结果。广播没有确认重传丢帧是常态。第三种用固定分片大小但发送端和接收端对“每片有效数据长度”理解不一致。发送端按 23 切接收端按 31 拼拼出来全是错位的。一个能用的分包结构我后来固定用一套结构发送端和接收端都按同一套规则来。核心是三个字段消息标识、总分片数、当前分片序号。可选加一个总长度用于校验。/* 应用层分片头放在每帧有效数据的开头 */typedef半导体厂商ruct{uint16_tmsg_id;/* 同一消息的所有分片共用 */uint8_ttotal_frag;/* 总分片数 */uint8_tfrag_index;/* 从 0 开始 */uint8_tpayload_len;/* 本片有效数据长度 */}frag_header_t;发送端逻辑#defineMAX_FRAG_PAYLOAD23/* 实测得出不是猜的 */intbroadca半导体厂商_send(uint16_tmsg_id,con半导体厂商uint8_t*data,uint16_tlen){uint8_ttotal(lenMAX_FRAG_PAYLOAD-1)/MAX_FRAG_PAYLOAD;uint8_tframe[MAX_FRAG_PAYLOADsizeof(frag_header_t)];frag_header_t*hdr(frag_header_t*)frame;for(uint8_ti0;itotal;i){uint16_toffseti*MAX_FRAG_PAYLOAD;uint16_tremainlen-offset;uint8_tchunk(remainMAX_FRAG_PAYLOAD)?MAX_FRAG_PAYLOAD:remain;hdr-msg_idmsg_id;hdr-total_fragtotal;hdr-frag_indexi;hdr-payload_lenchunk;memcpy(framesizeof(frag_header_t),dataoffset,chunk);if(raw_send(frame,sizeof(frag_header_t)chunk)!0){return-1;/* 中途失败整条消息作废 */}}return0;}接收端维护一个重组缓冲按 msg_id 索引。每收到一片按 frag_index 放到对应位置记录已收到的片数。收齐 total_frag 片后校验 payload_len 累加是否等于预期总长然后交给上层。typedef半导体厂商ruct{uint16_tmsg_id;uint8_ttotal_frag;uint8_treceived;uint8_tgot_flag[FRAG_MAX];/* 位图标记哪些片已到 */uint8_tbuf[FRAG_BUF_SIZE];uint32_tla半导体厂商_国外半导体厂商ck;/* 用于超时清理 */}reassembly_ctx_t;广播场景下必须处理的几个异常广播没有 ACK发送端不知道接收端收没收到。所以接收端的重组逻辑必须能处理不完整。丢片。中间某片没收到received 永远到不了 total_frag。必须有超时机制比如 500ms 内没收齐就丢弃整个缓冲释放给新消息用。否则缓冲被占满后续消息全丢。现象是节点一开始能收配置跑一段时间后彻底收不到任何广播重启才好。这就是缓冲泄漏。乱序。虽然同一发送源连续发送通常有序但多路径、中继转发可能造成乱序。用位图按 frag_index 落位不要假设按顺序到达。重复。重传或信道反射可能造成同一片收到两次。位图能天然去重已置位的片直接丢弃。新消息覆盖旧消息。如果同一个 msg_id 在旧消息未收齐时又来了新消息要么用更大的 msg_id 空间避免回绕要么在收到 frag_index 0 时重置该 msg_id 的缓冲。我倾向于后者简单可靠。判断和排查现场怎么观察分包出问题现象往往不是“完全收不到”而是“时好时坏”“部分节点异常”。这类问题最难查。第一步抓原始帧。不要看应用层日志直接看链路层实际发出的每一帧。确认帧数对不对、每帧长度对不对、分片头字段有没有填错。我遇到过发送端 total_frag 算成 2 但实际发了 3 帧的情况接收端收齐 2 片就交给上层第三片永远被当成新消息的开头。第二步在接收端打印重组状态。每收到一片打印 msg_id、frag_index、received/total_frag。一眼就能看出是丢片、乱序还是 msg_id 冲突。第三步构造边界用例。数据长度刚好等于 MAX_FRAG_PAYLOAD 时total 应该是 1不要算成 2。长度等于 MAX_FRAG_PAYLOAD 1 时total 是 2第二片只有 1 字节。这两个边界最容易算错。第四步检查超时清理。人为让发送端只发前两片看接收端缓冲是否在超时后被释放。如果不释放反复触发观察缓冲是否耗尽。权衡分包粒度越细越好吗不是。分片越小单帧有效载荷越少同样数据需要的帧数越多。广播场景下帧数多意味着占用信道时间更长、冲突概率更高、接收端重组缓冲压力更大、丢任意一片的概率累积更高。分片越大单帧越接近上限帧数少但每帧出错影响的数据量更大而且留给协议头的空间更紧。我的经验值是每片有效载荷控制在承载上限的 70% 到 85% 之间。既留出余量应对协议头变动又不至于帧数膨胀。具体到 31 字节上限、8 字节头的情况23 字节左右是合适的。另一个权衡是重组缓冲大小。缓冲开小了并发消息一多就丢开大了占内存。广播场景下同时到达多条长消息的概率不高一般 2 到 4 个重组槽位够用。但必须确认最坏情况如果上层可能连续下发多条配置槽位不够时新消息会顶掉旧消息要确保这个行为是可接受的。一个容易忽略的点发送端的中途失败分包发送时如果第 2 片发送失败底层返回错误第 1 片已经出去了接收端会一直等后续片直到超时。这期间接收端的重组槽位被占用。发送端能做的要么整条消息重发msg_id 不变接收端收到 frag_index 0 时重置要么放弃并确保接收端超时足够短。我倾向于整条重发并且 msg_id 保持不变。接收端在收到重复的 frag_index 0 时清空该 msg_id 的旧状态重新开始。这样即使上一次发了一半这一次也能正确重组。但要注意如果两次发送的 msg_id 相同但内容不同接收端可能把两次的片混在一起。所以 msg_id 的分配要保证一条消息的生命周期内唯一。用递增计数器回绕周期足够长即可。最后一点提醒分包处理写完之后不要只在“正常长度”下测试。专门构造这些用例长度刚好等于上限、上限加一、上限的整数倍、上限整数倍加一、远大于缓冲容量的长度。每一个都跑一遍观察发送帧数、接收重组结果、超时行为。广播分包的问题从来不是“能不能切开”而是“接收端在丢片、乱序、重复、并发的情况下还能不能拼出正确结果”。把异常路径走通了这个功能才算真正做完。