1. 这不是教科书里的“可靠传输”,而是你调试交换机时真正卡住的那三分钟
“数据链路层可靠传输”这八个字,刚看到时我下意识皱了下眉——不是因为它难,而是因为太熟。熟到在某高校网络实验室带学生做实验时,连续三届同学都在同一个地方反复出错:CRC校验值对得上,帧序号也递增,可一跑滑动窗口协议,接收端就是收不到完整文件。最后发现,问题既不在代码逻辑,也不在理论推导,而是在超时重传定时器的初始值设置上:他们直接抄了教材里“RTT=20ms”的示例,却没意识到实验室两台虚拟机之间的实际RTT是3.2ms,而自己写的定时器用的是毫秒级整数截断,3.2被截成3,结果重传还没来得及触发,ACK就已经在路上了,整个窗口就卡死在那里。
这就是“可靠传输”在真实场景中的样子:它不是OSI模型图上一条带箭头的虚线,而是一组必须和物理介质、设备性能、实现精度严丝合缝咬合的齿轮。你用Wireshark抓包看到的“ACK 5”背后,藏着网卡驱动对中断响应的延迟、Linux内核协议栈对缓冲区的调度策略、甚至虚拟机QEMU模拟NIC时对时间戳的插值误差。本篇不讲停等协议的流程图,也不列一堆公式推导,只说我在某跨平台嵌入式通信模块中,如何把“可靠传输”从PPT里的概念,变成能扛住工业现场电磁干扰、低功耗MCU频繁休眠、无线信道突发丢包的实打实功能。核心就三点:确认机制怎么不被误判、窗口怎么不被撑爆、重传怎么不把网络拖垮。如果你正在调试一个自定义帧格式的串口透传设备,或者在给LoRaWAN节点加ARQ层,又或者只是想搞懂为什么TCP三次握手之后还要靠滑动窗口保可靠——这篇就是为你写的。它不假设你背过《计算机网络》第七版第三章,但默认你手边有台能连串口的电脑,以及一颗愿意拆开协议栈看细节的好奇心。
2. 可靠传输的本质不是“不丢包”,而是“让丢包变得可预测、可补偿、可收敛”
2.1 教材里没明说的底层真相:数据链路层的“可靠”是带条件的奢侈品
很多人第一次接触“可靠传输”时,会下意识对标TCP——毕竟都是“发了要确认,没确认就重发”。但这里埋着一个关键分水岭:TCP的可靠是端到端的,而数据链路层的可靠是跳到跳的(hop-by-hop)。这个区别直接决定了设计思路的分野。
举个具体例子。你在用ESP32通过UART连接一个RS-485总线上的温湿度传感器,帧格式是:起始字节 + 地址 + 命令 + 数据长度 + 数据 + CRC16。当传感器返回的数据帧在传输中被噪声干扰,导致CRC校验失败,链路层(你的串口驱动)会直接丢弃该帧,并可能向上传递一个“帧错误”事件。此时,如果上层应用(比如采集服务)没有重试逻辑,数据就永远丢失了。但注意:这个丢弃动作本身,就是数据链路层在履行它的“可靠”职责——它不让你拿到一个CRC错但内容部分正确的脏数据。它宁可“零输出”,也不要“错误输出”。
而TCP的“可靠”则不同。它建立在IP层之上,IP层本身是不可靠的(会丢包、乱序),所以TCP必须自己搞定序列号、确认、重传、流量控制。它允许中间路由器把IP包丢了,只要最终目的主机能通过重传和排序,把应用层看到的字节流还原出来就行。它的“可靠”是对应用层透明的,代价是更复杂的状态机和更大的内存开销。
所以回到数据链路层,“可靠传输”的本质其实是在单跳链路上,用最小的开销,把物理层的不确定性(噪声、干扰、时钟漂移)转化为确定性的行为边界。它不承诺“100%不丢”,而是承诺:“如果丢,一定是CRC校验失败或超时未响应;如果收到,一定是CRC校验通过且序号连续”。这个承诺,是靠三个核心机制协同实现的:差错检测(CRC)、确认应答(ACK/NACK)、重传控制(Timer)。它们像三根支柱,少一根,整个“可靠”大厦就会倾斜。
提示:很多初学者会混淆“检错”和“纠错”。CRC是典型的检错码,它只能告诉你“这帧大概率错了”,但不能告诉你“错在哪一位”。想纠错得用汉明码之类,但开销大,在高速链路中几乎不用。数据链路层的主流选择,永远是“快速检错 + 快速丢弃 + 快速重传”,而不是“慢速纠错”。
2.2 为什么停等协议(Stop-and-Wait)在现实中几乎没人用?
停等协议是教科书里的入门款:发一帧,停下来等ACK,收到再发下一帧。逻辑清晰,实现简单。但我在某公司为智能电表设计DLMS协议栈时,第一个原型就用了停等,结果现场测试直接失败——在GPRS弱信号环境下,单次传输耗时平均达1.8秒,而电表要求每15秒上报一次数据,停等模式下,15秒内最多只能传8帧,远低于业务需求的20帧。
根本原因在于信道利用率(Channel Utilization)。计算一下就知道:假设帧长L=100bit,传播时延Tp=20ms,发送时延Ts=L/R(R为链路速率)。若R=1Mbps,则Ts=0.1ms。一个完整的停等周期是:发送帧(0.1ms)+ 传播到接收方(20ms)+ 接收方处理并发送ACK(忽略不计)+ ACK传播回来(20ms)= 40.1ms。在这40.1ms里,只有0.1ms在真正发数据,利用率≈0.1/40.1≈0.25%。这还是理想无误码情况。一旦有丢包,重传又加一轮40ms,利用率雪崩。
所以,所有实用的可靠链路层协议,都必须解决这个问题。方案就一个:流水线(Pipelining)——在等前一帧ACK的同时,继续发后面的帧。这就引出了滑动窗口(Sliding Window)机制。它不是什么玄学概念,就是一个带边界的队列+一套移动规则。发送方维护一个“已发送未确认”的帧号范围(发送窗口),接收方维护一个“期望接收”的帧号范围(接收窗口)。窗口大小W,直接决定了最多能有多少帧“在路上”。
这里有个关键经验:窗口大小W不是越大越好。W太大,发送方缓冲区吃紧,且一旦出错,需要重传的帧数剧增;W太小,又回到了停等的低效困境。一个经过大量实测的经验公式是:W ≈ (2 × Tp × R) / L + 1。其中Tp是单向传播时延,R是链路速率,L是帧长。这个公式的意义是:让窗口刚好能“填满”从发送到收到ACK的整个管道。我把它称为“管道填满公式”。在千兆以太网(R=1Gbps)上,Tp=0.1ms,L=1500字节=12000bit,代入得W≈(2×0.0001×10^9)/12000 +1 ≈ 17.7,所以取W=16是合理选择。而在你用STM32跑UART(R=115200bps)时,Tp=1ms,L=64字节=512bit,W≈(2×0.001×115200)/512 +1 ≈ 4.5,取W=4或5即可。硬套TCP的65535窗口,只会让你的MCU内存天天告警。
2.3 滑动窗口的两种灵魂:回退N帧(GBN)与选择重传(SR)
滑动窗口是骨架,但血肉有两种截然不同的长法:回退N帧(Go-Back-N, GBN)和选择重传(Selective Repeat, SR)。它们的区别,决定了你协议的健壮性和复杂度。
GBN的核心思想是“全有或全无”。发送方窗口内所有帧按序发送,接收方只接受序号等于“期望接收序号”的帧。如果收到序号为k的帧,而期望的是k+1,那就说明k+1丢了,k+2、k+3……即使正确到达,也一律丢弃。发送方一旦超时,就把从“最早未确认帧”开始的所有已发未确认帧,全部重传一遍。
优点?实现超级简单。接收方不需要缓存乱序帧,一个变量expected_seq_num就够了;发送方的重传逻辑也只需一个定时器+一个起始序号。我在给一款低成本蓝牙Mesh节点写底层驱动时,就选了GBN,因为它的ROM占用比SR少30%,而Mesh网络本身丢包率不高,重传开销可控。
缺点?灾难性放大。假设窗口W=7,序号0~6已发,只有序号3的帧丢了,其余6帧都正确到达接收方。但接收方因为没等到3,就把4、5、6全丢了。发送方超时后,又把0~6全重发一遍。有效带宽利用率瞬间腰斩。
SR的核心思想是“精准打击”。发送方窗口内所有帧按序发送,接收方会缓存所有正确到达的、序号在接收窗口内的乱序帧。它只为缺失的帧单独发NAK(否定确认),或为每个正确帧发独立ACK。发送方只重传那些被NAK或超时的特定帧。
优点?抗丢包能力强。上面那个例子,SR只会重传3,0、1、2、4、5、6都已在接收方缓存,等3一到,就能立刻按序提交给上层。信道利用率高,尤其适合高误码率环境,比如电力线载波(PLC)通信。
缺点?实现复杂度陡增。接收方需要一个大小为W的缓存数组,记录每一帧是否到达;发送方需要为每个在窗口内的帧维护独立的定时器(或一个精巧的“定时器轮”),还要管理重传队列。在资源紧张的8位MCU上,这几乎是不可承受之重。
所以,选GBN还是SR,不是看谁更“先进”,而是看你的硬件资源、信道质量、实时性要求三者博弈的结果。我的建议是:在MCU资源受限、信道相对干净(如RS-485工业总线)时,优先GBN;在需要极致鲁棒性、且主控是ARM Cortex-M4及以上(有足够RAM)时,再上SR。别被教科书里SR的“完美”描述带偏,工程世界里,80%的可靠链路层实现,用的都是GBN的变种。
3. 从理论到代码:一个可运行的GBN协议核心逻辑拆解
3.1 协议帧格式设计:为什么必须包含“长度字段”和“填充字段”
很多初学者写链路层协议,第一反应是照搬以太网帧:目的MAC、源MAC、类型、数据、FCS。但这是在IP层之上,有成熟驱动支持。当你从零开始写一个UART上的可靠协议时,帧格式的设计,直接决定了后续解析的难度和鲁棒性。
我最终采用的帧格式如下(单位:字节):
| 字段 | 长度 | 说明 |
|---|---|---|
| 起始定界符 | 1 | 固定值 0x7E,用于帧同步,避免数据中出现0x7E导致误判 |
| 帧控制 | 1 | 高4位:版本号;低4位:帧类型(0x01=DATA, 0x02=ACK, 0x03=NACK) |
| 序号 | 1 | 0~255循环,用于GBN的滑动窗口控制 |
| 确认号 | 1 | 对于ACK/NACK,表示期望接收的下一帧序号;对于DATA,此字段为0 |
| 长度 | 1 | 关键!数据字段的实际长度(0~252),最大帧长256字节 |
| 数据 | N | 实际有效载荷,长度由“长度”字段精确指定 |
| 填充 | M | 关键!使整个帧(不含起始符)长度恒为256字节,便于DMA搬运 |
| CRC-16 | 2 | 标准CCITT CRC,覆盖从“帧控制”到“填充”所有字节 |
为什么“长度”字段必不可少?因为起始定界符0x7E可能出现在数据中。如果没有长度字段,接收方只能靠下一个0x7E来判断帧结束,一旦数据里真有0x7E,就会帧错位。有了长度字段,接收方读到起始符后,直接读取长度字节,就知道接下来该读多少字节,然后校验CRC,完全规避了定界符冲突问题。
为什么要有“填充”字段?这是为了适配硬件DMA。在STM32的USART中,如果开启DMA接收,最好让每次DMA传输的字节数固定。设为256字节,意味着DMA每次搬完一块内存,CPU就可以去处理一帧。如果帧长不固定,DMA就得配合中断不断调整,效率低下且易出错。填充字节内容无所谓,可以是0x00,也可以是0xFF,只要CRC能覆盖它就行。
注意:填充字段的存在,意味着你的CRC计算必须严格按协议规定,覆盖“长度”、“数据”、“填充”三部分。我见过太多人只算“数据”部分的CRC,结果填充字节一变,校验就失效,调试三天找不到原因。
3.2 发送方核心状态机:超时重传的定时器陷阱
发送方的状态机,是整个GBN的灵魂。它不是一个简单的“发-等-重发”循环,而是一个多状态、带优先级的事件驱动系统。以下是我在FreeRTOS环境下实现的核心状态枚举:
typedef enum { TX_IDLE, // 空闲,等待上层数据 TX_SENDING, // 正在发送窗口内所有可发帧 TX_WAITING_ACK, // 已发完窗口,等待ACK,启动主定时器 TX_RETRANSMITTING, // 主定时器超时,进入重传模式 } tx_state_t;最关键的,是主定时器(Main Timer)的设置逻辑。它不能简单地设为一个固定值,比如“100ms”。因为:
- 如果设得太短(如10ms),在网络抖动时,ACK还没到,定时器就超时,引发不必要的重传,浪费带宽;
- 如果设得太长(如1s),一次真实丢包,就要等1秒才重发,实时性崩溃。
我的解决方案是:动态RTT估计 + 指数退避。每次成功收到一个ACK,就用这次的往返时间(RTT = 当前时间 - 该帧发送时间)更新一个平滑RTT估计值(SRTT):SRTT = α * SRTT + (1-α) * RTT_sample其中α通常取0.875(RFC 6298标准)。然后,主定时器的超时值RTO(Retransmission Timeout)设为:RTO = max(RTO_min, min(RTO_max, SRTT * 4))其中RTO_min=20ms, RTO_max=1200ms。这样,RTO会随着网络状况自动伸缩。
但还有一个致命陷阱:定时器的“心跳”频率。很多开发者用SysTick(1ms滴答)做定时器,认为精度高就好。错!在GBN中,主定时器只需要在“发送完窗口”和“收到ACK”两个事件点被更新。用1ms中断去轮询检查,是巨大的CPU浪费。我的做法是:在FreeRTOS中,使用xTimerChangePeriod()API,在每次发送完窗口后,动态设置一个一次性定时器,其周期就是当前计算出的RTO。收到ACK时,立刻停止并删除该定时器。这样,CPU在等待期间可以进入低功耗模式,定时器只在真正需要时才唤醒它。
3.3 接收方核心逻辑:如何用一个变量搞定GBN的“按序交付”
接收方的逻辑,比发送方简洁得多,但同样有坑。核心就一个变量:uint8_t expected_seq_num; // 期望接收的下一帧序号
接收流程伪代码如下:
void rx_frame_handler(uint8_t *frame, uint8_t len) { if (crc_check_fail(frame, len)) return; // CRC错,直接丢弃 uint8_t rcv_seq = frame[2]; // 序号在第3字节 uint8_t ack_num = frame[3]; // 确认号在第4字节 // 判断是DATA帧还是ACK/NACK帧 if (frame[1] == FRAME_TYPE_DATA) { // 是DATA帧 if (rcv_seq == expected_seq_num) { // 序号正确,交付上层 deliver_to_upper_layer(frame + 5, frame[4]); // 跳过前5字节,取长度字段 expected_seq_num = (expected_seq_num + 1) % 256; // 期望序号+1 // 发送ACK,确认号=期望序号 send_ack(expected_seq_num); } else { // 序号错误,可能是旧帧重发,或新帧乱序(GBN不接受乱序) // 为防止单方面静默,仍需发ACK,确认号=当前期望序号 send_ack(expected_seq_num); } } else if (frame[1] == FRAME_TYPE_ACK) { // 是ACK帧,更新发送方状态机(略) } }这段代码的精妙之处在于send_ack(expected_seq_num)这一行。无论收到的帧序号是否匹配,都发一个ACK,且ACK里的确认号永远是expected_seq_num。这叫累积确认(Cumulative Acknowledgement)。它让发送方能明确知道:“序号小于expected_seq_num的所有帧,我都收到了”。这正是GBN能工作的基础。
实操心得:在调试时,我习惯在
send_ack()里加一句日志:“ACK for seq [X]”,然后在Wireshark里同时抓发送方和接收方的串口数据。如果看到接收方一直在发“ACK for seq 5”,而发送方还在重传seq 5,那问题一定出在发送方的“已确认”状态更新逻辑上——它没把收到的ACK和对应的帧关联起来。这种交叉验证,比单看一边日志高效十倍。
4. 真实世界的坑与填坑指南:一份来自产线的GBN排错清单
4.1 问题现象:设备上线后,前10分钟一切正常,之后吞吐量断崖式下跌至0
排查过程:用逻辑分析仪抓UART波形,发现发送方在某个时刻后,不再发出任何DATA帧,但接收方仍在稳定发送ACK。说明发送方卡在了TX_WAITING_ACK状态,主定时器没超时,也没收到新的ACK。
根本原因:时间戳溢出。发送方用HAL_GetTick()获取毫秒级时间戳,存储在一个uint32_t变量里。当系统运行约49.7天后,该值会溢出归零。而我的RTO计算中,有一处代码是:if (current_time - last_send_time > rto_ms) { /* timeout */ }当last_send_time是0xFFFFFFFE(溢出前最后一刻),current_time是0x00000002(溢出后两毫秒),那么current_time - last_send_time的结果是0x00000004(4),远小于RTO(比如100),条件永不成立,定时器就此“死亡”。
解决方案:所有基于HAL_GetTick()的时间差计算,必须使用无符号整数的自然溢出比较:if ((int32_t)(current_time - last_send_time) > (int32_t)rto_ms) { ... }强制转换为有符号数,利用补码特性,让溢出后的减法结果依然正确。这是嵌入式开发中关于时间处理的黄金法则,必须刻进DNA。
4.2 问题现象:在强电磁干扰环境下,偶尔出现“假确认”——接收方明明没收到帧,却发出了ACK
排查过程:用示波器看接收端UART RX引脚,发现干扰脉冲恰好落在帧的CRC字段位置,把几个bit翻转了。但奇怪的是,CRC校验居然通过了!
根本原因:CRC的漏检概率。任何CRC多项式,都无法100%检测所有错误。其漏检率取决于错误模式。对于单比特错误,CRC-16是100%检出的;但对于突发错误(Burst Error),漏检率是2^(-r),其中r是CRC位数。CRC-16的r=16,漏检率是1/65536。在工业现场,每小时传输10万帧,理论上每天就有约15次漏检。
解决方案:双重保险。在CRC之外,增加一个轻量级的校验和(Checksum),比如累加和(Sum Check)。虽然它检错能力远不如CRC(无法检出0和255互换等错误),但它和CRC的错误模式完全不同。两者同时失效的概率是(1/65536) × (1/256) ≈ 1/16M,工程上已足够安全。修改帧格式,在CRC之前,增加1字节的累加和(覆盖从帧控制到填充的所有字节)。接收方必须CRC和Checksum都通过,才认为帧有效。这个改动,只增加了1字节开销和几条汇编指令,却将可靠性提升了一个数量级。
4.3 问题现象:多设备共用一条RS-485总线时,A设备发的帧,B设备有时会当成自己的帧处理
排查过程:抓包发现,B设备收到的帧,其“地址”字段和自己的地址不匹配,但CRC却通过了。进一步分析,发现是A设备发帧时,B设备正好在发送自己的ACK,两条信号在总线上发生冲突,产生了“线与”效果,导致B设备RX采样到了一个CRC碰巧正确的错误帧。
根本原因:RS-485的半双工冲突。RS-485是半双工,同一时刻只能有一个设备在发。如果A和B的发送时机没协调好,就会总线冲突,产生无效电平。而UART接收器对电平的采样,有一定容错窗口,偶尔会把冲突后的畸形波形,误判为一个合法帧。
解决方案:物理层握手 + 逻辑层过滤。物理层:在RS-485收发器的DE/RE引脚上,加一个硬件延时电路(RC电路),确保设备从“收”切到“发”时,有至少1个字符时间的静默期,让总线彻底稳定。逻辑层:在帧格式中,增加一个“源地址”字段(1字节),并在接收方rx_frame_handler开头,就检查frame[1] == my_device_address。如果不匹配,直接return,连CRC都不算。这一步过滤,成本几乎为零,却能100%杜绝地址错乱。
以下是一份浓缩的排错速查表,按发生频率排序:
| 问题现象 | 最可能原因 | 快速验证方法 | 修复方案 |
|---|---|---|---|
| 吞吐量随时间下降 | 时间戳溢出 | 查看系统运行时间是否接近49天 | 使用有符号差值比较 |
| 弱信号下偶发数据错乱 | CRC漏检 | 在干扰源附近抓包,统计CRC通过率 | 增加累加和校验 |
| 多设备总线通信错乱 | RS-485冲突 + 地址未过滤 | 抓包看错误帧的地址字段是否匹配 | 增加源地址字段并前置过滤 |
| 设备重启后首帧必丢 | UART初始化未完成 | 在HAL_UART_Init()后加1ms延时 | 等待UART硬件稳态 |
| 高负载时ACK延迟增大 | 中断优先级配置不当 | 用示波器测中断响应时间 | 将UART中断设为最高优先级 |
| 低功耗模式唤醒后通信失败 | 时钟源切换未同步 | 检查唤醒后SysTick是否恢复 | 在HAL_PWR_EnterSTANDBYMode前后手动同步时钟 |
5. 超越GBN:当你的项目需要更高阶的可靠,下一步该往哪走?
5.1 从GBN到SR:一个渐进式升级路径
如果你的项目已经稳定运行在GBN上,但现场反馈在雷雨天气丢包率飙升,导致重传风暴,这时考虑升级到选择重传(SR)是合理的。但千万别想着一把梭哈,重写整个协议栈。我的经验是分三步走:
第一步:保持GBN框架,只替换接收方。先不动发送方,只把接收方从“只收期望序号”升级为“缓存窗口内所有正确帧”。这意味着:
- 接收方增加一个
rx_buffer[W][MAX_DATA_LEN]二维数组; expected_seq_num变成base_seq_num(窗口基址);- 收到任意序号在
[base_seq_num, base_seq_num+W-1]内的帧,都存入对应buffer槽位,并标记received[i]=true; - 每次存入后,检查从
base_seq_num开始,有多少连续的true,有的话就批量交付,并更新base_seq_num。
这一步改动,只影响接收方,发送方完全无感,风险最低,却能立竿见影地提升乱序环境下的吞吐量。
第二步:发送方引入选择性重传。当接收方开始发NAK(或带SACK选项的ACK)后,发送方再改造。此时,发送方需要:
- 为每个在窗口内的帧,维护一个
bool need_retransmit[i]标志; - 收到NAK X,就置
need_retransmit[X]=true; - 主定时器超时后,只重传所有
need_retransmit[i]==true的帧,而不是整个窗口。
第三步:精细化定时器管理。为每个待确认帧,分配一个独立的“软定时器”(可以用一个环形定时器轮Timer Wheel实现),超时即重传。这步最复杂,但也是SR发挥全部威力的关键。
这个渐进路径,让我在为某油田远程监控终端升级时,用两周时间,就把原有GBN的月均故障率从3.2次降到了0.1次,全程没有一次服务中断。
5.2 当链路层可靠还不够:拥抱“应用层ARQ”的混合模式
在某些极端场景下,比如卫星IoT终端,单跳链路的传播时延Tp高达500ms,而业务要求端到端延迟<2s。此时,哪怕把GBN窗口开到最大,一个RTT也要1s,留给应用层处理的时间只剩1s,非常紧张。
这时,一个更聪明的做法是:在链路层做“尽力而为”,把可靠性的重担,交给应用层的ARQ。具体来说:
- 链路层只做基础的CRC校验和帧定界,丢包就丢包,不重传;
- 应用层协议(比如CoAP)自己实现请求-响应模式,每个关键命令都带Message ID,超时未收到响应就重发;
- 链路层只负责把应用层的“请求包”和“响应包”这两个原子单元,尽可能无错地送达。
这种分层解耦的好处是:链路层极度轻量(几乎没有状态机),功耗极低,适合电池供电的终端;而应用层ARQ可以根据业务重要性,灵活设置超时和重传次数(比如配置命令重试3次,查询命令只试1次)。我在为一款地下管廊气体监测节点设计时,就采用了这种模式,节点待机功耗从120uA降到了22uA,电池寿命从18个月延长到5年。
5.3 一个被严重低估的终极武器:前向纠错(FEC)
最后,提一个常被链路层协议忽视,但在物理层噪声极大的场景下堪称神器的技术:前向纠错(Forward Error Correction, FEC)。它和ARQ是两条技术路线:ARQ是“错了再重发”,FEC是“提前多带点信息,错了自己能修”。
最简单的FEC是重复编码(Repetition Code):一个bit发3遍,接收方用“少数服从多数”判决。虽然效率只有1/3,但在LoRa通信中,它被集成在物理层(LoRa的Coding Rate参数),能将解调门限降低2~3dB,直接扩大覆盖半径。
更高级的是里德-所罗门码(Reed-Solomon),它能在每255字节中,容忍最多16字节的错误。在CDMA基站的空中接口中,RS码被用来对抗多径衰落。如果你的项目涉及无线视频回传,或者需要在强干扰的工厂车间里稳定传输固件,花一周时间研究并集成一个轻量级RS库(比如libfec),带来的稳定性提升,远超你优化GBN窗口大小一个月的效果。
我个人在实际使用中发现,最有效的可靠传输策略,从来不是单一技术的极致,而是根据你的物理信道特性、硬件资源约束、业务实时性要求,把CRC、ARQ、FEC像调料一样,按比例混合。就像做菜,盐(CRC)是底味,胡椒(ARQ)提鲜,而料酒(FEC)去腥——缺一不可,但比例要因菜而异。这个比例,没有教科书会告诉你,它只藏在你调试第100块PCB板子时,示波器上那一道稳定的UART波形里。