多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

AnLiu(暗流):参考 KCP 实现的加密多流 UDP 可靠传输协议,附带音视频传输增强

AnLiu(暗流):参考 KCP 实现的加密多流 UDP 可靠传输协议,附带音视频传输增强 AnLiu暗流参考 KCP 实现的加密多流 UDP 可靠传输协议附带音视频传输增强开源地址https://github.com/wanghengwen/AnLiu 还在测试暂时没有上传代码就两个文件anliu.h和anliu.cC99不依赖第三方库。为什么要写这个KCPikcp大家应该都用过或者听说过一个 C 文件实现的 ARQ 协议不碰 socket不读系统时钟发包走回调、收包调ikcp_input嵌到哪里都方便。我一直很喜欢这种做法但在实际项目里用下来也有几个地方不太满意没有真正的拥塞控制。ikcp 自带的拥塞窗口通常都会关掉nc1带宽受限的时候发送量远超链路多出来的全变成排队和丢包。一个 conv 就是一条流。要同时传几类数据只能开几个实例拥塞控制各算各的也没有优先级。不加密包头是明文特征很明显。我的本行是音视频很多数据是“过期就没用”的全可靠传输并不合适。所以就有了 AnLiu。名字是“暗流”的拼音“暗”是说整个数据报都加密线上看不到任何明文字段“流”是说一条连接里可以跑很多条独立的流。AnLiu 的用法仍然是 ikcp 那一套应用负责 socket 和时钟协议只管状态机。在这个基础上做了下面这些事能力说明多流一条连接最多 8192 条流两端随时可以打开、关闭所有流共享拥塞控制和 ACK按优先级加权调度拥塞控制BBRv2 的思路带宽探测按 BBRv3 的节奏每 2~3 秒一次加上运营商限速器检测丢包恢复区间确认SACK加基于时间的丢包判定RACK能适应乱序平滑发送连接级令牌桶 pacing大块数据不会整窗突发加密ChaCha20 SipHash-2-4 组成的 SIV 构造整包认证加密两个方向各用一套密钥随机填充半可靠帧流以帧为单位发送过期整帧丢弃处理关键帧依赖流级 FECReed-Solomon按流开关冗余率可以固定也可以自适应带宽反馈把带宽估计和“建议编码码率”交给应用前五项是一个可靠 UDP 协议本来就该做好的后三项是给音视频加的。只拿它当可靠传输用后面这些完全可以不管。协议设计多流与调度一条连接里的每条流有自己的序号空间和收发窗口某条流丢包只会阻塞它自己不会拖住别的流这一点和 QUIC 的思路一样。和“开多个连接”相比多条流共享同一份 RTT 估计、拥塞窗口和 pacing 令牌数据和 ACK 可以拼进同一个数据报。调度规则比较简单默认流sid 0随连接创建严格优先适合放控制信令其他流分 4 个优先级按 8 : 4 : 2 : 1 加权轮询低优先级也有最小份额不会饿死重传优先于新数据但最多占 3/4 的发送令牌丢包严重的大流挤不掉别的流的新数据RACK 看的是整条连接的送达情况一条很少发数据的控制流也能借其他流的 ACK 及时发现自己丢了包。在单元测试里做过一个对比3 Mbps 瓶颈、RTT 40 ms、1% 丢包一条视频流把链路压满同时每 50 ms 发一条 200 字节控制消息。控制消息放在默认流时 p99 是 204 ms故意把它放到最低优先级、视频放最高p99 就到了 1.9 秒。可见优先级确实起作用。如果知道上行配额用pace_rate设一个比配额略低的上限瓶颈处不再排队控制消息 p99 能降到 45 ms。拥塞控制拥塞控制是自己实现的 BBR没有用丢包作为主要信号而是持续测两个量瓶颈带宽最近 10 个往返的最大交付速率和传播时延10 秒内的最小 RTT按“带宽 × 增益”平滑发送在途数据保持在两倍 BDP 左右。照搬 BBR 在实际网络上会遇到问题所以做了一些调整。随机丢包无线、跨境链路很常见只有在同时出现排队的时候才算拥塞这样 20% 随机丢包的链路也能维持速率。带宽探测改成按墙钟每 2~3 秒一次低 RTT 链路上不会每几百毫秒就把队列顶一下。另外参照 BBRv1 的 lt_bw 加了限速器检测这一块后面会细说是真实网络测试里花时间最多的地方。加密tag SipHash-2-4-128(k_mac, P)截断成 12 字节再拿它当 ChaCha20 的 nonce 加密整个数据报wire tag || ChaCha20(k_enc, tag, P)。这种 SIV 构造不需要维护 nonce 计数器。conv、版本号、包类型、序号全在密文里线上只看得到 12 字节随机样子的 tag 和后面的密文每个数据报默认还会加 1~32 字节随机填充。认证失败的包必须静默丢弃不回任何东西主动探测拿不到响应。两个方向的密钥从同一个 32 字节 PSK 派生把对方的包原样反射回去也会认证失败。目前只有 PSK没有密钥交换也没有前向保密这个在“不足”里再说。音视频增强半可靠帧流音视频数据有几个特点晚到等于没到帧之间有依赖I 帧丢了后面的 P 帧都解不了一个丢包不应该把整条流卡住。所以 AnLiu 除了可靠流还有一种半可靠流接口是按帧收发的帧号由协议分配接收端拿到frame_no和lost_before前面跳过了几帧可以直接喂给解码器帧在发送队列里超过max_age_ms就整帧丢掉已经发出去但超时的帧不再重传改发一个 FWD 让接收端跳过设了drop_until_key发送端丢帧后会一直丢到下一个关键帧已经在途的依赖帧也一起清掉接收端的rcv_drop_until_key负责丢掉解不了的 P 帧接收端也有期限等不到的帧自己跳过不依赖发送端的 FWD。FEC长 RTT 链路上重传经常来不及RTT 200 ms 时一个丢失的音频包至少晚到 300 ms。FEC 用带宽换时间。算法是 GF(2⁸) 上 Cauchy 矩阵的 Reed-Solomon一个块内任意 m 个包丢了都能恢复。最早我用的是 XOR一个校验包只能修一个丢包为了时延只能把组做得很小音频基本等于每包复制一份。换成 RS 以后块最多攒 100 ms或 64 个包就封块校验包每隔 5 ms 发一个避免和数据落进同一次突发丢包。参数只有一个冗余率fec_ratio。设成 0 是自适应在 10%~100% 之间调。自适应只把“FEC 没修好、而且重传也赶不上”的丢失算进去低 RTT 链路上重传来得及就不会白白加冗余。半可靠流默认是ANL_FEC_RTT_AUTO只有在max_age_ms小于实测 RTT也就是重传肯定来不及的时候才自动打开。带宽反馈视频编码码率最好刚好等于可用带宽。BBR 本身就有一个带宽模型所以直接把它交给应用bw_estimate是瓶颈带宽target_rate是扣掉包头、校验包、重传和 10% 余量以后应用所有流加起来还能发的载荷速率变化超过 5% 时回调一次。接收端还会定期把抖动、排队时延、帧时延、跳帧数报告给发送端。和 RTP/RTCP 比做音视频的同学可能会问为什么不直接用 RTP/RTCP。我的理解是两者定位不一样RTP/RTCP以 WebRTC 的常见用法为例AnLiu可靠性RTP 本身不保证送达重传靠 RTCP NACK RTXFEC 用 ULPFEC / FlexFEC都是扩展可靠流和半可靠帧流都在协议内重传、FEC、过期丢弃由协议统一管理非媒体数据信令、文件要另外走通道WebRTC 里是基于 SCTP 的 DataChannel或者另开 TCP同一条连接里开可靠流拥塞控制不在标准里由实现决定WebRTC 用 GCC transport-cc连接级 BBR媒体和非媒体数据共用一个拥塞控制帧的概念有时间戳和 marker 位但丢帧、跳帧由应用层的 jitter buffer 决定帧号、过期、关键帧依赖都在传输层加密SRTP 加密载荷RTP 头SSRC、序号、时间戳是明文密钥用 DTLS-SRTP 协商整包加密没有明文头只有 PSK没有密钥交换生态标准协议能互通各种编码的负载格式都有规范私有协议两端都得用 AnLiu需要和浏览器、SIP 设备、媒体服务器互通RTP 是唯一选择。如果两端都是自己的程序媒体和信令、文件要走同一条连接又希望流量没有明显特征AnLiu 这种做法会简单很多。真实网络上遇到的问题第一版是 9 月 25 日提交的在模拟网络上结果很好看。我原本以为再调调参数就差不多了结果放到真实服务器上跑问题一个接一个。下面挑主要的讲完整过程记在仓库的progress.md里。模拟阶段进真实网络之前先在模拟器里修了一轮。单元测试每次换一个随机种子1000 个种子在 ASan UBSan 下逐个跑抓到了几个单种子测试发现不了的问题默认配置下 RACK 被关掉了丢包只能等翻倍退避的 RTO控制消息 p99 到了 1.7 秒一次丢包事件里cwnd 在几次 flush 中被连续减半新连接拿到第一个 RTT 样本之前打开流的请求连丢几次就要等好几秒。还有一个体会拥塞控制改一点点整条发送轨迹就不一样了单个种子的差异大多是噪声。所以后来判断任何改动都用多组种子对比修改前后。时钟第一个真实网络问题跟时钟有关。协议时间由应用传进来RTT 样本按最近一次anl_update的时间计算。测试程序一次读一批数据报一批只更新一次时钟批次大的时候协议时钟最多落后 147 ms。结果min_rtt被压低正常的 RTT 被当成排队cwnd 一路降到 4 个分片。协议这边把 RTT 下限定为更新间隔API 文档里也写明了收到包以后、调anl_input之前要用当前时间更新一次。第二个时钟问题藏得更深。接收端的时延报告用rp_next记下一次发送时刻初值是 0比较用的是 32 位带符号差值。主机的单调时钟毫秒数模 2³² 超过 2³¹也就是开机超过大约 24.8 天0 就被判断成“未来”报告永远发不出去。我的几台服务器正好开机很久之前以它们为接收端测的媒体数据依赖报告的那些机制自适应 FEC、码率下调其实都没生效。修完之后加了一个两端时钟从0x90000000起步的单元测试。运营商限速器这是花时间最多的一块。几台海外服务器的出口限速很奇怪连接开始后 1 秒左右能跑到 200 Mbps 以上然后稳定在 30 Mbps 或 10 Mbps超出的部分直接丢不排队。这是一个桶很大的令牌桶限速器policer。BBR 靠测带宽过日子碰上它很难受。最开始的现象是 STARTUP 冲过头。开头 1 秒测到 26 MB/s实际限速 3.75 MB/s桶用完以后在途的 4000 个分片大部分被丢峰值在带宽过滤器里还要留 1 秒左右这期间重传按 7 倍速率发出去又被丢前 2 秒就有 2 万次重传。修复办法是 STARTUP 结束后 8 轮内如果某一轮丢得很多、交付又不到估计值一半就把过滤器里的峰值压到这一轮的交付速率。还碰到过一次速率死循环在途量里算上了已经判定丢失、还没重传的分片DRAIN 永远结束不了速率一路降到 0.4 Mbps。于是参照 BBRv1 加了限速器检测连续两个区间丢包超过 10%、交付速率相近就进入测试按交付速率发一个区间丢失明显减少就认定是限速器之后按测到的速率发送。问题接着就来了限速状态每 15 秒左右到期一次到期后带宽估计从过滤器里捡回 30~128 MB/s 的旧样本每次带来约 1 万次重传有一次还停了 43 秒。后来改成到期不硬切而是按lt_rate × (1 k/4)一级一级往上探。RTT 跳动大的路径上交付速率偶尔被低估按低速发丢包自然少了被误判成限速器锁在低速上。真正的限速器和“容量更大但有波动的瓶颈”在“降速后丢包变少”这一点上表现一样很难区分。杭州出口是一个可以短时突发到 2 倍的整形器探测尾段的样本被突发额度抬高限速值定高了重传是 TCP 的 6~8 倍。这个到现在也没完全解决。路径容量真的下降时原来的逻辑每次只能降 1/8大约 10 秒一次太慢。中间否掉的方案也不少。比如“STARTUP 一轮丢包超过 40% 就退出”遇上开头 1 秒路径不通会在接近 0 的速率结束 STARTUP70 秒才爬到 60 Mbps。又比如“丢包超过 10% 且交付不到一半就按拥塞处理”模拟里 20% 随机丢包场景的视频准时率从 96% 掉到 8%。每个改动都要同时通过真实网络和模拟回归两关按下葫芦浮起瓢是常事。亚毫秒 RTT同机房的两台服务器之间 RTT 只有 0.5~1 ms我本以为是最简单的场景结果问题一点不少。pacing 有个下限是每个 srtt 至少发 4 个分片srtt 1 ms 时这个下限就是 44 Mbps比 30 Mbps 的限速还高大约 40% 的包被丢。改成计算下限用的 RTT 不小于更新间隔。更严重的是连接直接断掉。用tc把限速从 8 Mbps 切到 3 Mbps3 次测试全部在 3~4 秒内断开。查下来是 RACK 驱动的快速重传不用等 RTO0.7 ms 的 RTT 加 61% 丢包同一个分片连续被丢 20 次只要几秒触发了dead_link。同样的切换加 50 ms 时延就没事因为重传节奏慢得多。改了两处降速复测时 pacing 下限不再抬高目标速率同一分片的重复快速重传拉开间隔。改完 3 次全部存活改之前是 6 次全断。音视频相关276 ms RTT 的路径上前 1 秒的视频帧额外延迟 90~458 ms。原因是初始窗口 16 个分片首个关键帧大概 30 KB约 25 个分片要等一个 RTT 才发得完后面的帧都排在它后面。初始窗口改成 64首个关键帧的额外时延从 377 ms 降到 24 ms稳态不受影响。init_cwnd现在可以配置。阿里云到海外服务器的跨境路径RTT 在 60~85 ms 和 100~110 ms 两档之间来回切。min_rtt取到低档之后大部分轮次被判断为有排队媒体流每轮只有十来个分片丢一个就超过 2% 的拥塞门槛。于是随机丢包被当成了拥塞带宽下限被压到应用自己的发送速率cwnd 几次之后只剩十几个分片视频帧开始超时。修复办法是应用受限的轮次里带宽下限不低于本轮实际发送速率也不再收紧在途上限。这条路径上 23 轮对比视频超时丢弃从 1340 帧降到 575 帧。还有一个反直觉的问题可用带宽低于媒体码率时冗余反而帮倒忙。800 kbit 的整形器下丢包是容量不够造成的自适应 FEC 看到丢包就加冗余冗余占了将近 40% 的带宽又造成更多丢包视频准时率只有 3%~12%不开冗余的版本反而有 35%~40%。修复方案已经写好还没有在实网验证。顺带发现发送端的优先级管不到路由器里面。瓶颈是 FIFO 的时候音频在队列里照样被尾丢要保护音频只能让总发送速率不超过容量。带宽反馈准不准开环测试码率固定的数据显示不太准。整形器有突发额度突发以线速通过读出来的是假峰值bw_estimate高了 1.3~1.9 倍。target_rate在整形器下偏低在限速器下又偏高。开环数据本身有偏差所以给测试工具加了一个模拟编码器跟随target_rate的闭环模式这部分还在测。测试本身真实网络测试有一半时间花在测试环境上本机走代理 TUNUDP 发不到部分服务器本机的 TCP 在本地就被截断了没法做对照VPN 断了会把远端测试进程一起带走tc的自动清理时间设短了测到一半限速规则没了那一轮只能作废tc改 police 参数时内核会重新填满令牌桶切换后第一个区间测得偏高。这些都要一个个排除才能确认问题出在协议上。测试方法三层测试单元测试test.c直接#include anliu.c可以检查内部状态自带一个可复现的模拟网络丢包、指定丢某个包、时延、乱序、重复、带宽瓶颈覆盖密码学测试向量、可靠和半可靠流、FEC、8192 条流、优先级、5 秒断网、2 万个随机变异数据报的模糊测试等。模拟对照bench/anl_bench用 1 ms 的虚拟时钟在同一个模拟网络上同时跑 AnLiu 和原版 ikcp场景有批量传输、交互消息、音视频、混合业务、带宽阶梯、10 分钟长稳、令牌桶限速器等同一个种子结果完全一样。真实网络bench/realnet在真实 UDP 路径上跑可以对比 AnLiu、ikcp 和系统 TCP。时延统计用的是“单向时延减去这条流的最小单向时延”也就是排队、重传、抖动带来的额外时延两端不需要对时。# 单元测试1000 个种子gcc-stdc99-Wall-Wextra-O1-g-fsanitizeaddress,undefined test.c-oanl_test-lmseq10012000|xargs-P6-I{}sh-cANL_TEST_SEED{} ./anl_test out_{}.txt 21 || echo {} failed# 模拟对照cdbenchmake./anl_bench--quick# 批量、交互、视频、音频、混合10 种链路./anl_bench--quickbwstep# 带宽 8→2→5→1→8 Mbps 阶梯./anl_bench soak--soak600# 10 分钟长稳含 5 秒断网# 真实网络两端各跑一个./realnet server--protoanl--teststream--dirup--dur75--wnd4096./realnet client--host服务器--protoanl--teststream--dirup--dur75--wnd4096服务器代号情况出口带宽备注s_hz杭州公司机房约 50 Mbps整形器可短时突发到 2 倍没有 sudo只开了两个 UDP 端口s_zjg、s_lsj、s_1t、s_500g海外同一机房的 4 台 1 核 960 MB 云主机s_zjg 30 Mbpss_lsj 10 Mbps有 sudo可以用 tc互相之间 RTT 0.6~1.2 mss_rb另一个机房1 核 960 MB30 Mbps到上面那组约 100 ms到杭州 250~280 mss_ali_ecu、s_ali_dev阿里云NAT 后面只能主动往外连共享的生产机只跑低码率测试到海外服务器 160~220 msRTT 从同机房的 0.5 ms 到杭州与 s_rb 之间的 280 ms 都有限速方式有硬丢弃的限速器也有可以突发的整形器。真实路径的问题往往等不来比如容量下降可能一天都碰不上一次所以写了一个tc脚本在服务器出口搭受控瓶颈只作用于发往对端测试端口的 UDP不影响 ssh。支持硬丢弃限速police、tbf 排队整形、netem 随机丢包和附加时延测试中途可以切换用来做 25 → 8 → 25 Mbps 这样的阶跃。脚本启动时会设一个定时自动清理防止规则留在服务器上。排查问题主要靠 trace。REALNET_TRACE250每 250 ms 打印一行拥塞控制内部状态BBR 状态、cwnd、在途、带宽估计、限速器状态、丢包率REALNET_MDIAG1逐帧记录发送和接收情况。日志写在服务器本地测完再取回来免得诊断流量影响测试。测试中慢慢形成了几条规矩同一个问题在 3 次测试中出现才改代码真实网络的噪声太大一次异常说明不了什么改完先做针对性测试和 20 个种子的模拟回归都过了才上全量一台主机同一时间只跑一个测试每次对比都冻结源码、日志里记录二进制哈希保证比的确实是想比的版本。目前的结果可靠传输真实网络6 对服务器每对 15 轮每轮上下行各 75 秒满载30 Mbps 和 10 Mbps 限速的路径上AnLiu 吞吐中位数是系统 TCPcubic的 96%~98%同机房短 RTT 路径的重传次数从最初的每轮 4~7 万降到 1.5 万左右和 TCP 差不多杭州出口可突发整形器吞吐和 TCP 接近但重传多很多前面说过。和 ikcp 比nodelay 1, 10, 2, 1即关闭拥塞控制的快速模式条件AnLiuikcp 窗口 128 / 256ikcp 窗口 1024真实路径RTT 约 180 ms无瓶颈约束26~27 Mbps5.2~5.6 / 10.5~11.5 Mbps27.5 Mbps线上多发约 30%重传是 AnLiu 的 3~6 倍同一路径加 20 Mbit 限速器16~17 Mbps2.5~2.9 Mbps开头突发后吞吐跌到 0.06~0.29 Mbps模拟的带宽阶梯场景8 → 2 → 5 → 1 → 8 Mbps每 10 秒切一次里AnLiu 的带宽估计和实际带宽之比在 0.97~1.03链路利用率 100%ikcp 的发送量是链路容量的 1.94 倍多出来的都堆在瓶颈队列里。音视频媒体测试用的是 64 kbps 音频加约 940 kbps、30 fps 的视频准时标准是音频 150 ms、视频 300 ms。服务器之间杭州上行除外没有额外丢包的路径上15 轮视频额外时延 p99 的中位数在 3~7 ms。其他条件条件音频准时视频准时RTT 200 ms5 Mbit 整形5% 随机丢包100%100%同上10% 随机丢包100%99.9%阿里云到海外服务器的真实跨境路径5 分钟98.0%96.0%2400 kbit 限速器突发额度只有 16 KiB100%100%关键帧 240/240有丢包的长 RTT 路径上这些数字主要靠 FEC代价是线路多用 30%~40% 的带宽。模拟里对比开不开 FEC 更明显RTT 200 ms、20% 丢包时不开 FEC 音频准时率 8.7%固定 25% 冗余是 50.5%自适应冗余 96.1%。不足还在测试阶段。前面提到的几个修复在单独的分支上验证还没合进主线“容量不足时冗余挤占带宽”的修复还没做实网验证。可突发整形器上重传偏高是 TCP 的 6~8 倍最差的一秒也比 TCP 低。容量下降之后再回升280 ms RTT 下要 20~40 秒才能回到新的上限。干净的路径上音频冗余有时也会打开多用 10%~20% 的字节。带宽估计和target_rate在整形器、限速器下都有偏差闭环测试还没做完。没测过 4G/5G 和 Wi-Fi 弱网也没接过真实编码器。加密是自己组合的没有安全证明也没有第三方审计只有 PSK没有密钥交换和前向保密一个 PSK 总共不应超过 2⁴⁰ 个数据报包的时序和大小分布仍然可以被统计分析。没有连接迁移Wi-Fi 和蜂窝网络切换时地址会变没有路径 MTU 探测固定 1400 字节。线程模型和 ikcp 一样一个连接上的所有调用由调用方串行化。使用说明编译把anliu.h、anliu.c放进工程就行gcc-stdc99-O2-canliu.c建立连接#includeanliu.htypedefstruct{intfd;structsockaddr_storageaddr;socklen_talen;}peer_t;/* 发包回调里面不能再调用 anl_* 函数 */staticintudp_output(constchar*buf,intlen,anl_t*w,void*user){peer_t*puser;sendto(p-fd,buf,len,0,(structsockaddr*)p-addr,p-alen);return0;}anl_config cfg;anl_config_default(cfg,ANL_ROLE_CLIENT);/* 对端用 ANL_ROLE_SERVER两端角色必须不同 */memcpy(cfg.psk,my_psk,ANL_PSK_SIZE);/* 两端相同的 32 字节密钥 */cfg.interval10;/* 默认 20 ms实时业务建议 10 */anl_t*wanl_create(conv,cfg,peer);/* conv 两端相同不能为 0 */anl_setoutput(w,udp_output);没有握手创建完就能发数据流参数会随第一批数据带过去。主循环uint32_tnownow_ms();/* 单调时钟毫秒 */anl_update(w,now);for(;;){intwait(int)(int32_t)(anl_check(w,now)-now);if(wait0)wait0;if(wait5)wait5;/* 实时业务建议 1~5 ms 精度 */poll(pfd,1,wait);charpkt[2048];ssize_tn;while((nrecv(fd,pkt,sizeof(pkt),MSG_DONTWAIT))0){anl_update(w,now_ms());/* 收到包之后、anl_input 之前更新时钟 */if(anl_input(w,pkt,(long)n)ANL_EAUTH)continue;/* 认证失败直接丢不要回应 */}nownow_ms();anl_update(w,now);/* 驱动重传、pacing、FEC 封块等定时任务 */if(anl_state(w)0)break;/* 连接已失效 */handle_streams(w);}anl_input用最近一次anl_update的时间计算 RTT。如果只在poll之前更新时钟收到包的时候协议时钟已经落后了一个等待时间批量读包时落后更多RTT 会算错。前面“时钟”一节的问题就是这么来的。可靠流默认流sid 0随连接创建可靠字节流语义和 ikcp 一样严格优先于其他流不能关闭anl_send(w,HELLO,5);intnanl_recv(w,buf,sizeof(buf));/* 没数据返回 ANL_EAGAIN */另外开一条可靠流比如传文件anl_stream_opt o;anl_stream_opt_default(o,ANL_RELIABLE);o.tagTAG_FILE;/* 应用自己定义0..65535对端靠它区分用途 */o.prio3;/* 0 最高3 最低 */o.snd_wndo.rcv_wnd1024;/* 单位是分片长 RTT 高带宽要调大上限 8192 */interr;anl_stream_t*fileanl_stream_open(w,o,err);anl_stream_send(file,data,len);/* ... */anl_stream_close(file);/* 可靠流是有序关闭没发完的数据还会继续发完 */对端打开的流通过 accept 回调拿到staticinton_accept(anl_t*w,anl_stream_t*s,anl_stream_opt*opt,void*user){/* opt 里对端决定的 mode、tag、rcv_wnd 已经填好其他本端参数可以改 */switch(anl_stream_tag(s)){caseTAG_FILE:opt-prio3;break;caseTAG_VIDEO:opt-rcv_drop_until_key1;break;default:return-1;/* 拒绝对端会收到 RST */}anl_stream_set_user(s,ctx_for(s));/* 回调里只能调这一个 anl_* 函数 */return0;/* 接受后由应用负责 anl_stream_close */}anl_set_accept(w,on_accept);读数据时用anl_readable找出可读的流anl_stream_t*rd[16];intcntanl_readable(w,rd,16);for(inti0;icnti16;i){intnanl_stream_recv(rd[i],buf,sizeof(buf));if(nANL_ECLOSED){/* 对端关闭并且数据已读完 */anl_stream_close(rd[i]);continue;}/* 处理 n 字节 */}音视频流打开音频、视频两条半可靠流anl_stream_opt a,v;anl_stream_opt_default(a,ANL_SEMI);a.tagTAG_AUDIO;a.prio0;a.max_age_ms200;/* 超过 200 ms 的音频帧直接丢 */anl_stream_t*audioanl_stream_open(w,a,NULL);anl_stream_opt_default(v,ANL_SEMI);v.tagTAG_VIDEO;v.prio1;v.max_age_ms500;v.drop_until_key1;/* 丢帧后一直丢到下一个关键帧 */anl_stream_t*videoanl_stream_open(w,v,NULL);半可靠流默认只在max_age_ms小于实测 RTT 时自动开 FEC。想从第一帧就开显式设置一下两端都要开v.fec1;v.fec_ratio0;/* 0 自适应1..100 为固定冗余率比如 25 */发送intranl_stream_send_frame(video,is_key?ANL_FRAME_KEY:0,frame,frame_len,NULL);if(rANL_EDROPPED){/* 正在丢到下一个关键帧这一帧没发。可以让编码器尽快出一个关键帧 */}anl_stream_send_frame(audio,0,audio_pkt,audio_len,NULL);接收anl_frame_info fi;intn;while((nanl_stream_recv_frame(video,buf,sizeof(buf),fi))0){if(fi.lost_before0){/* 前面有 lost_before 帧没收到必要时通过默认流请求关键帧 */}decoder_feed(buf,n,fi.frame_no,fi.flagsANL_FRAME_KEY);}/* 循环结束时 n ANL_EAGAIN表示暂时没有完整的帧 */码率自适应把回调接到编码器上staticvoidon_rate(anl_t*w,uint32_ttarget_rate,void*user){/* target_rate 是所有流合计可发的载荷字节/秒 */encoder_set_bitrate(user,(target_rate-AUDIO_BYTES_PER_SEC)*8);/* 这里只能调用 anl_get_stats / anl_stream_get_stats 这类只读函数 */}anl_set_rate_callback(w,on_rate);统计信息anl_stats st;anl_get_stats(w,st);/* srtt、min_rtt、bw_estimate、target_rate、retrans 等 */anl_stream_stats ss;anl_stream_get_stats(video,ss);/* ss.peer 是对端最近一次的时延报告抖动、排队时延、帧时延、跳帧数、FEC 恢复数。 frame_delay_max_ms 持续升高说明帧开始晚到了可以考虑降码率 */几个要注意的地方两端role不同conv和 PSK 相同anl_input返回ANL_EAUTH时直接丢包不能回应发包、accept、码率、报告这几个回调里不能调anl_*例外是 accept 里的anl_stream_set_user和码率、报告回调里的只读统计每收到一个包先用当前时间anl_update再anl_input知道上行配额的话把cfg.pace_rate设得比配额略低瓶颈处不排队时延更低。最后这一轮测下来最大的感受是模拟网络里的好结果在真实网络上不一定站得住。运营商的限速方式、服务器开机了多久、跨境线路的 RTT 怎么跳这些在模拟器里都想不到只能在真实环境里复现用 trace 定位再回到模拟器和受控瓶颈里验证。等主要问题收敛以后代码会放到 GitHub 上https://github.com/wanghengwen/AnLiu 。做可靠 UDP 或者实时音视频传输的朋友有问题或者建议欢迎留言。
返回列表