☰
AnLiu(暗流):参考 KCP 实现的加密多流 UDP 可靠传输协议,附带音视频传输增强
2026/10/1 15:01:59 网站建设 项目流程

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

开源地址:https://github.com/wanghengwen/AnLiu (还在测试,暂时没有上传)

代码就两个文件anliu.h和anliu.c,C99,不依赖第三方库。

为什么要写这个

KCP(ikcp)大家应该都用过或者听说过,一个 C 文件实现的 ARQ 协议,不碰 socket,不读系统时钟,发包走回调、收包调ikcp_input,嵌到哪里都方便。我一直很喜欢这种做法,但在实际项目里用下来,也有几个地方不太满意:

  1. 没有真正的拥塞控制。ikcp 自带的拥塞窗口通常都会关掉(nc=1),带宽受限的时候发送量远超链路,多出来的全变成排队和丢包。
  2. 一个 conv 就是一条流。要同时传几类数据,只能开几个实例,拥塞控制各算各的,也没有优先级。
  3. 不加密,包头是明文,特征很明显。
  4. 我的本行是音视频,很多数据是“过期就没用”的,全可靠传输并不合适。

所以就有了 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 + RTX,FEC 用 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 的速率结束 STARTUP,70 秒才爬到 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 Mbps,3 次测试全部在 3~4 秒内断开。查下来是 RACK 驱动的快速重传不用等 RTO,0.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的闭环模式,这部分还在测。

测试本身

真实网络测试有一半时间花在测试环境上:本机走代理 TUN,UDP 发不到部分服务器,本机的 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-std=c99-Wall-Wextra-O1-g-fsanitize=address,undefined test.c-oanl_test-lmseq10012000|xargs-P6-I{}sh-c'ANL_TEST_SEED={} ./anl_test > out_{}.txt 2>&1 || echo "{} failed"'# 模拟对照cdbench&&make./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 Mbps,s_lsj 10 Mbps有 sudo,可以用 tc;互相之间 RTT 0.6~1.2 ms
s_rb另一个机房,1 核 960 MB30 Mbps到上面那组约 100 ms,到杭州 250~280 ms
s_ali_ecu、s_ali_dev阿里云,NAT 后面,只能主动往外连共享的生产机,只跑低码率测试;到海外服务器 160~220 ms

RTT 从同机房的 0.5 ms 到杭州与 s_rb 之间的 280 ms 都有,限速方式有硬丢弃的限速器,也有可以突发的整形器。

真实路径的问题往往等不来,比如容量下降可能一天都碰不上一次,所以写了一个tc脚本在服务器出口搭受控瓶颈,只作用于发往对端测试端口的 UDP,不影响 ssh。支持硬丢弃限速(police)、tbf 排队整形、netem 随机丢包和附加时延,测试中途可以切换,用来做 25 → 8 → 25 Mbps 这样的阶跃。脚本启动时会设一个定时自动清理,防止规则留在服务器上。

排查问题主要靠 trace。REALNET_TRACE=250每 250 ms 打印一行拥塞控制内部状态(BBR 状态、cwnd、在途、带宽估计、限速器状态、丢包率),REALNET_MDIAG=1逐帧记录发送和接收情况。日志写在服务器本地,测完再取回来,免得诊断流量影响测试。

测试中慢慢形成了几条规矩:同一个问题在 3 次测试中出现才改代码,真实网络的噪声太大,一次异常说明不了什么;改完先做针对性测试和 20 个种子的模拟回归,都过了才上全量;一台主机同一时间只跑一个测试;每次对比都冻结源码、日志里记录二进制哈希,保证比的确实是想比的版本。

目前的结果

可靠传输

真实网络,6 对服务器,每对 15 轮,每轮上下行各 75 秒满载:

  • 30 Mbps 和 10 Mbps 限速的路径上,AnLiu 吞吐中位数是系统 TCP(cubic)的 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 ms,5 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%。

不足

  1. 还在测试阶段。前面提到的几个修复在单独的分支上验证,还没合进主线;“容量不足时冗余挤占带宽”的修复还没做实网验证。
  2. 可突发整形器上重传偏高,是 TCP 的 6~8 倍,最差的一秒也比 TCP 低。
  3. 容量下降之后再回升,280 ms RTT 下要 20~40 秒才能回到新的上限。
  4. 干净的路径上音频冗余有时也会打开,多用 10%~20% 的字节。
  5. 带宽估计和target_rate在整形器、限速器下都有偏差,闭环测试还没做完。
  6. 没测过 4G/5G 和 Wi-Fi 弱网,也没接过真实编码器。
  7. 加密是自己组合的,没有安全证明,也没有第三方审计;只有 PSK,没有密钥交换和前向保密;一个 PSK 总共不应超过 2⁴⁰ 个数据报;包的时序和大小分布仍然可以被统计分析。
  8. 没有连接迁移(Wi-Fi 和蜂窝网络切换时地址会变),没有路径 MTU 探测,固定 1400 字节。
  9. 线程模型和 ikcp 一样,一个连接上的所有调用由调用方串行化。

使用说明

编译

把anliu.h、anliu.c放进工程就行:

gcc-std=c99-O2-canliu.c

建立连接

#include"anliu.h"typedefstruct{intfd;structsockaddr_storageaddr;socklen_talen;}peer_t;/* 发包回调,里面不能再调用 anl_* 函数 */staticintudp_output(constchar*buf,intlen,anl_t*w,void*user){peer_t*p=user;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.interval=10;/* 默认 20 ms,实时业务建议 10 */anl_t*w=anl_create(conv,&cfg,&peer);/* conv 两端相同,不能为 0 */anl_setoutput(w,udp_output);

没有握手,创建完就能发数据,流参数会随第一批数据带过去。

主循环

uint32_tnow=now_ms();/* 单调时钟,毫秒 */anl_update(w,now);for(;;){intwait=(int)(int32_t)(anl_check(w,now)-now);if(wait<0)wait=0;if(wait>5)wait=5;/* 实时业务建议 1~5 ms 精度 */poll(&pfd,1,wait);charpkt[2048];ssize_tn;while((n=recv(fd,pkt,sizeof(pkt),MSG_DONTWAIT))>0){anl_update(w,now_ms());/* 收到包之后、anl_input 之前更新时钟 */if(anl_input(w,pkt,(long)n)==ANL_EAUTH)continue;/* 认证失败,直接丢,不要回应 */}now=now_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);intn=anl_recv(w,buf,sizeof(buf));/* 没数据返回 ANL_EAGAIN */

另外开一条可靠流,比如传文件:

anl_stream_opt o;anl_stream_opt_default(&o,ANL_RELIABLE);o.tag=TAG_FILE;/* 应用自己定义,0..65535,对端靠它区分用途 */o.prio=3;/* 0 最高,3 最低 */o.snd_wnd=o.rcv_wnd=1024;/* 单位是分片,长 RTT 高带宽要调大,上限 8192 */interr;anl_stream_t*file=anl_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->prio=3;break;caseTAG_VIDEO:opt->rcv_drop_until_key=1;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];intcnt=anl_readable(w,rd,16);for(inti=0;i<cnt&&i<16;i++){intn=anl_stream_recv(rd[i],buf,sizeof(buf));if(n==ANL_ECLOSED){/* 对端关闭并且数据已读完 */anl_stream_close(rd[i]);continue;}/* 处理 n 字节 */}

音视频流

打开音频、视频两条半可靠流:

anl_stream_opt a,v;anl_stream_opt_default(&a,ANL_SEMI);a.tag=TAG_AUDIO;a.prio=0;a.max_age_ms=200;/* 超过 200 ms 的音频帧直接丢 */anl_stream_t*audio=anl_stream_open(w,&a,NULL);anl_stream_opt_default(&v,ANL_SEMI);v.tag=TAG_VIDEO;v.prio=1;v.max_age_ms=500;v.drop_until_key=1;/* 丢帧后一直丢到下一个关键帧 */anl_stream_t*video=anl_stream_open(w,&v,NULL);

半可靠流默认只在max_age_ms小于实测 RTT 时自动开 FEC。想从第一帧就开,显式设置一下,两端都要开:

v.fec=1;v.fec_ratio=0;/* 0 自适应;1..100 为固定冗余率,比如 25 */

发送:

intr=anl_stream_send_frame(video,is_key?ANL_FRAME_KEY:0,frame,frame_len,NULL);if(r==ANL_EDROPPED){/* 正在丢到下一个关键帧,这一帧没发。可以让编码器尽快出一个关键帧 */}anl_stream_send_frame(audio,0,audio_pkt,audio_len,NULL);

接收:

anl_frame_info fi;intn;while((n=anl_stream_recv_frame(video,buf,sizeof(buf),&fi))>=0){if(fi.lost_before>0){/* 前面有 lost_before 帧没收到,必要时通过默认流请求关键帧 */}decoder_feed(buf,n,fi.frame_no,fi.flags&ANL_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 或者实时音视频传输的朋友,有问题或者建议欢迎留言。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询