DataChannel 优化

发布时间:2026/7/27 17:24:03
DataChannel 优化 DataChannel 实现DataChannel 的实现有两种webrtc 目前使用自行基于 C 开发的 dcsctpmediasoup 和 libdatachannel 基于 usrsctpusrsctp 为开源 sctp 协议实现均遵守 rfc 4960 标准。RFCRFC 4960 - Stream Control Transmission Protocol能力概述DataChannel (sctp支持可靠传输、不可靠传输两种模式其中不可靠传输有两种方式一种基于配置包最大生命周期进行配置另一种是基于最大重传次数进行配置。如若使用可靠传输只需将Client 端的 reliable 配置为 true 即可server 的 usrsctp 中的 spa.sendv_prinfo.pr_policy 配置为 SCTP_PR_SCTP_NONE 即可如若需要使用不可靠传输模式则分以下两种方式基于生存时间和重传次数二者只能选其一基于最大生命周期 maxRetransTime 为数据报在 sctp 队列中最大存活时间该参数可以保证发送队列可以快速的情况尽量降低后续包发送失败的概率但是存在在网络拥塞的情况下因 CC 控制导致数据报尚未发送便被清理的问题即数据包没有经过一次发送便因超时而被清理了。基于最大重传次数 maxRetrans 为数据报在发送队列中最多重传的次数可保证数据至少在网络中发送一次但如若出现了网络丢包不会进行重传注意在 usrsctp 中此参数至少要配置为 1 才能保证至少发送一次与 client 不同。性能两者默认都较弱不论是其可靠传输还是不可靠传输抗弱网能力均较弱但测试下来 usrsctp 比 dcsctp 要强一些不过 usrsctp 基于 c 开发代码缩写、简写严重难以阅读理解而 dcsctp 基于 C 实现相对要容易理解一些弱网性能在可靠传输模式下30% 的丢包即为上限还不如 tcp具体数据暂不列了更高的丢包率测试无法进行下去。原因分析发送窗口sctp 在拥塞控制算法上与 tcp 类似分为慢启动、拥塞避免、快速恢复 和快速重传几个阶段。在出现弱网即出现 丢包 或者 重传超时的情况时发送窗口 cwnd 和 慢启动阈值 ssthresh 减半过快其 cwnd 最低值过低首先从这一点进行入手分析。以下 Url 是 RFC 中对 CWnd 和 SStreash 的配置策略https://datatracker.ietf.org/doc/html/rfc4960#section-6.3.3https://datatracker.ietf.org/doc/html/rfc4960#section-7.2.3原文如下7.2.3. Congestion Control Upon detection of packet losses from SACK (see Section 7.2.4), an endpoint should do the following: ssthresh max(cwnd/2, 4*MTU) cwnd ssthresh partial_bytes_acked 0 Basically, a packet loss causes cwnd to be cut in half. When the T3-rtx timer expires on an address, SCTP should perform slow start by: ssthresh max(cwnd/2, 4*MTU) cwnd 1*MTU and ensure that no more than one SCTP packet will be in flight for that address until the endpoint receives acknowledgement for successful delivery of data to that address.大致意思如下在检测到丢包时更新 sstresh 和 cwnd 机制如下对原 ssthresh 减半但最大为 4 个 MTU 大小这个是致命的同时将 cwnd 更新为 ssthresh当 T3-rtx 重传定时器超时时对原 ssthresh 减半但最大为 4 个 MTU 大小但对 cwnd 更新为仅 1 个 MTU。通过上述两种机制可以看到在高丢包率的弱网环境中很容易进入到上述两种 case 中结合 flighting 的数据大小导致 sctp 协议可发送数据量奇小 cwnd 过小同时因为重传包的优先级高于新生成的数据包导致协议唯一的一个发送窗口一直在重传旧包新包很难获取发送机会这就导致了明显的队头阻塞现象。解决方案目标只是为了不使 CWnd 的大小降低的过快即在检测到丢包和 T3-rtx 超时时不要立即对 ssthresh 减半而是更加平滑的去降低并且提高最小值的上限通过这两种策略的优化可以保证 sctp 协议有很高的带宽抢占能力因为发送窗口越大带宽抢占能力就越高。清空队列通过上述策略优化发送窗口后可以大幅度提高 sctp 在弱网双向各30%丢包率下的带宽传输能力但是在更极端的网损40%丢包甚至更高场景下依然会出现队头阻塞现象发送端不再主动发送新插入的数据而是优先发送旧包未收到 Ack在实际应用场景中我们希望不论是否收到对端的确认都不应该阻塞新包的发送和旧包的清理以保证发送队列有一个快速的清理速度且保证所有的数据均有机会被快速的发送到网络中去。如果双方均为开源的实现我们可以随意修改 sctp可快速解决上述问题但是当一端为浏览器时我们只能考虑修改 Server 侧的协议实现以期可以间接的影响到对端浏览器让对端浏览器最为发送端时可以快速的清空发送队列避免队头阻塞答案就在 SACK 包中首先看下 SACK 包的协议定义https://datatracker.ietf.org/doc/html/rfc4960#section-3.3.43.3.4. Selective Acknowledgement (SACK) (3) This chunk is sent to the peer endpoint to acknowledge received DATA chunks and to inform the peer endpoint of gaps in the received subsequences of DATA chunks as represented by their TSNs. The SACK MUST contain the Cumulative TSN Ack, Advertised Receiver Window Credit (a_rwnd), Number of Gap Ack Blocks, and Number of Duplicate TSNs fields. By definition, the value of the Cumulative TSN Ack parameter is the last TSN received before a break in the sequence of received TSNs occurs; the next TSN value following this one has not yet been received at the endpoint sending the SACK. This parameter therefore acknowledges receipt of all TSNs less than or equal to its value. The handling of a_rwnd by the receiver of the SACK is discussed in detail in Section 6.2.1. The SACK also contains zero or more Gap Ack Blocks. Each Gap Ack Block acknowledges a subsequence of TSNs received following a break in the sequence of received TSNs. By definition, all TSNs acknowledged by Gap Ack Blocks are greater than the value of the Cumulative TSN Ack. 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -------------------------------- | Type 3 |Chunk Flags | Chunk Length | -------------------------------- | Cumulative TSN Ack | -------------------------------- | Advertised Receiver Window Credit (a_rwnd) | -------------------------------- | Number of Gap Ack Blocks N | Number of Duplicate TSNs X | -------------------------------- | Gap Ack Block #1 Start | Gap Ack Block #1 End | -------------------------------- / / \ ... \ / / -------------------------------- | Gap Ack Block #N Start | Gap Ack Block #N End | -------------------------------- | Duplicate TSN 1 | -------------------------------- / / \ ... \ / / -------------------------------- | Duplicate TSN X | -------------------------------- Chunk Flags: 8 bits Set to all 0s on transmit and ignored on receipt. Cumulative TSN Ack: 32 bits (unsigned integer) This parameter contains the TSN of the last DATA chunk received in sequence before a gap. In the case where no DATA chunk has been received, this value is set to the peers Initial TSN minus one. Advertised Receiver Window Credit (a_rwnd): 32 bits (unsigned integer) This field indicates the updated receive buffer space in bytes of the sender of this SACK; see Section 6.2.1 for details. Number of Gap Ack Blocks: 16 bits (unsigned integer) Indicates the number of Gap Ack Blocks included in this SACK. Number of Duplicate TSNs: 16 bit This field contains the number of duplicate TSNs the endpoint has received. Each duplicate TSN is listed following the Gap Ack Block list. Gap Ack Blocks: These fields contain the Gap Ack Blocks. They are repeated for each Gap Ack Block up to the number of Gap Ack Blocks defined in the Number of Gap Ack Blocks field. All DATA chunks with TSNs greater than or equal to (Cumulative TSN Ack Gap Ack Block Start) and less than or equal to (Cumulative TSN Ack Gap Ack Block End) of each Gap Ack Block are assumed to have been received correctly. Gap Ack Block Start: 16 bits (unsigned integer) Indicates the Start offset TSN for this Gap Ack Block. To calculate the actual TSN number the Cumulative TSN Ack is added to this offset number. This calculated TSN identifies the first TSN in this Gap Ack Block that has been received. Gap Ack Block End: 16 bits (unsigned integer) Indicates the End offset TSN for this Gap Ack Block. To calculate the actual TSN number, the Cumulative TSN Ack is added to this offset number. This calculated TSN identifies the TSN of the last DATA chunk received in this Gap Ack Block.每一个 SACK 包中均包括一个字段Cumulative TSN该字段标识接收端已确认通知给 Application的最大包序号TSN通过此字段告诉发送端不要再发送小于此 TSN 的包并清空所有队列中小于等于此 TSN 的数据包其它的字段均不是最重要的可以自行决定是否调整。只有当发送端不断的接收到接收端发来的 SACK 包后通过其携带的强制前移的 “Cumulative TSN” 来清理本地的发送队列可以保证自己的发送队列一直处于一个较小的 Cache这样新插入的数据包便可以被快速的发送出去避免了因旧包未被 Ack 而导致的队头阻塞问题更加的贴近 UDP 的行为。SACK Chunk 源码首先整理下 sctp 协议内接收端组装sack 的数据结构 https://tools.ietf.org/html/rfc4960#section-3.3.4在 dcsctp 的代码中比较易懂定义如下class SackChunk : public Chunk, public TLVTraitSackChunkConfig { public: static constexpr int kType SackChunkConfig::kType; // 可以看出包序号使用的是 uint16_t只需两个字节 struct GapAckBlock { GapAckBlock(uint16_t start, uint16_t end) : start(start), end(end) {} uint16_t start; uint16_t end; bool operator(const GapAckBlock other) const { return start other.start end other.end; } }; SackChunk(TSN cumulative_tsn_ack, uint32_t a_rwnd, std::vectorGapAckBlock gap_ack_blocks, std::setTSN duplicate_tsns) : cumulative_tsn_ack_(cumulative_tsn_ack), a_rwnd_(a_rwnd), gap_ack_blocks_(std::move(gap_ack_blocks)), duplicate_tsns_(std::move(duplicate_tsns)) {} static absl::optionalSackChunk Parse(rtc::ArrayViewconst uint8_t data); void SerializeTo(std::vectoruint8_t out) const override; std::string ToString() const override; TSN cumulative_tsn_ack() const { return cumulative_tsn_ack_; } uint32_t a_rwnd() const { return a_rwnd_; } rtc::ArrayViewconst GapAckBlock gap_ack_blocks() const { return gap_ack_blocks_; } const std::setTSN duplicate_tsns() const { return duplicate_tsns_; } private: static constexpr size_t kGapAckBlockSize 4; static constexpr size_t kDupTsnBlockSize 4; const TSN cumulative_tsn_ack_; // 最大已确认包序号TSN 即为内部传输块的序号 const uint32_t a_rwnd_; // 接收窗口的大小 std::vectorGapAckBlock gap_ack_blocks_; // GAP 包序号范围一次聚合多个 std::setTSN duplicate_tsns_; // 收到的已确认的包序号 };这里可能有些疑问每次打包的 SACK 中其 gap_ack_blocks_ 和 duplicate_tsns_ 是否有最大个数/长度限制因为它们两个毕竟是要全部一个个的序列化到网络包中的答案是有的。两者 gap_ack_blocks_ 和 duplicate_tsns_ 单次限制上限为 20 个kMaxGapAckBlocksReported 和 kMazhimaxDuplicateTsnReported 均定义为 constexpr 值20声明在代码文件data_tracker.h 中。SACK 发包时机在 dcsctp 的实现中sack 的发送时机是动态的。在乜有数据包丢失时每秒钟发送一次当监测到丢包时针对每个数据包发送一次 sack当不直接发送 SACKS 时将使用 timer 控制 sack 的延迟发送比如 min(RTO/2200ms)。发送端接收到对端回应的 SACK 后判断如果其 cumulative tsn 比本地记录的最大的 last cumulative tsn 要大即没有回退则重启 T3_rtx_ 定时器。遗留问题RTT 计算有明显问题如下两种在 dcsctp 中看到 RTT 的计算是有漏洞的其在 sack 中没有 gap_ack_blocks 时通过 cumulative tsn 计算其 rtt存在两种风险:1、这个 sack 整体是有延迟的其计算 rtt 时是直接通过 sendts - current ts明显没有考虑 delay ack time2、其只有在没有 gap_ack_blocks 时即没有出现丢包时才统计其 RTT这样在丢包严重时可能会出现长时间无法更新 rtt或者在 rtt 和 loss 突然增大时出现长时间无法更新 rtt 的情况一直以旧的 rtt 为准。