diff --git a/notes/pics/tcp-udp-latency.png b/notes/pics/tcp-udp-latency.png new file mode 100644 index 00000000..7f28daa0 Binary files /dev/null and b/notes/pics/tcp-udp-latency.png differ diff --git a/notes/计算机网络 - 传输层.md b/notes/计算机网络 - 传输层.md index 04332144..e8f4d99c 100644 --- a/notes/计算机网络 - 传输层.md +++ b/notes/计算机网络 - 传输层.md @@ -2,10 +2,13 @@ * [计算机网络 - 传输层](#计算机网络---传输层) * [UDP 和 TCP 的特点](#udp-和-tcp-的特点) + * [选择 TCP 还是 UDP](#选择-tcp-还是-udp) + * [在 UDP 上实现可靠传输](#在-udp-上实现可靠传输) * [UDP 首部格式](#udp-首部格式) * [TCP 首部格式](#tcp-首部格式) * [TCP 的三次握手](#tcp-的三次握手) * [三次握手的原因](#三次握手的原因) + * [握手报文丢失](#握手报文丢失) * [TCP 的四次挥手](#tcp-的四次挥手) * [四次挥手的原因](#四次挥手的原因) * [TIME_WAIT](#time_wait) @@ -17,6 +20,11 @@ * [1. 慢开始与拥塞避免](#1-慢开始与拥塞避免) * [2. 快重传与快恢复](#2-快重传与快恢复) * [TCP 粘包与拆包](#tcp-粘包与拆包) + * [附录:实测](#附录实测) + * [A1 TCP 与 UDP 发送位置更新](#a1-tcp-与-udp-发送位置更新) + * [A2 握手报文丢失](#a2-握手报文丢失) + * [A3 三次挥手](#a3-三次挥手) + * [A4 最后的 ACK 丢失](#a4-最后的-ack-丢失) * [参考资料](#参考资料) @@ -28,6 +36,40 @@ - 用户数据报协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信; - 传输控制协议 TCP(Transmission Control Protocol)是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块),每一条 TCP 连接只能是点对点的(一对一)。 +### 选择 TCP 还是 UDP + +TCP 的可靠和按序是有代价的:一个报文段丢失后,后面已经到达的数据也要等它重传成功,才能按顺序交给应用。 + +- 数据必须完整、晚一点到也可以的场景用 TCP,例如网页(HTTP/1.1、HTTP/2)、文件传输、SSH、数据库连接; +- 数据过时就没有用、宁可丢掉的场景用 UDP,例如实时对战游戏的位置同步、视频通话(WebRTC 优先使用 UDP); +- 只有一问一答的场景用 UDP 可以省去握手,例如 DNS 查询。 + +直播常用的 HLS 基于 HTTP,也就是 TCP,观众能接受几秒的延迟,换来画面完整。 + +例如游戏客户端每 50 ms 上报一次位置,在 RTT 为 50 ms、丢包率为 5% 的网络中分别用 TCP 和 UDP 发送 600 次(Linux 实测,见[附录 A1](#a1-tcp-与-udp-发送位置更新)): + +

+ +5 次实测中,TCP 的 600 条更新全部到达,但有 24~53 条的延迟超过 100 ms,最长 307~939 ms,到达时位置早已过时;UDP 丢失了 23~30 条,到达的延迟都不超过 44 ms,丢失的那一条由 50 ms 后的下一次更新补上。 + +### 在 UDP 上实现可靠传输 + +游戏中的位置可以丢,购买道具、释放技能这类消息却必须送达。既要 UDP 的低延迟、又有一部分数据必须可靠时,就要在应用层按 TCP 的思路自己实现: + +- 序号:给每个数据包编号,接收方据此去重和排序; +- 确认和重传:接收方回复确认,发送方超时没有收到确认就重传,超时时间根据测得的 RTT 计算; +- 流量控制和拥塞控制:限制已发送但未确认的数据量,避免压垮接收方和网络。 + +与直接使用 TCP 相比,好处在于可以按需取舍:只对需要的消息重传,不相关的消息互不阻塞,重传和拥塞控制的参数也可以按业务调整。 + +HTTP/3 使用的 QUIC 就是在 UDP 上实现的可靠传输,并改进了 TCP 的几处设计: + +- 包号只增不减,丢失的数据放进一个新编号的包中重传。收到确认时能分清确认的是哪一次发送,RTT 测得更准;TCP 重传时沿用原来的序号,无法区分; +- 数据按流(Stream)组织,每个流按帧中的偏移量单独排序,一个流丢包不影响其它流; +- 一个确认帧可以携带很多个已收到的区间,TCP 的选择确认(SACK)受首部长度限制,通常最多携带 3 个区间。 + +QUIC 选择在 UDP 上重新实现,而不是改进 TCP,是因为 TCP 实现在操作系统内核中,改动要等操作系统升级才能普及;防火墙、NAT 等中间设备还会检查甚至改写 TCP 首部,不认识的新选项可能被丢弃。QUIC 运行在用户态,随浏览器和 App 一起更新,并且加密了大部分首部,中间设备无法干预。QUIC 的握手和连接迁移见 [HTTP/3](HTTP.md#http3)。 + ## UDP 首部格式

@@ -53,38 +95,60 @@ 假设 A 为客户端,B 为服务器端。 - 首先 B 处于 LISTEN(监听)状态,等待客户的连接请求; -- A 向 B 发送连接请求报文,SYN=1,ACK=0,选择一个初始的序号 x; -- B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y; -- A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 y+1,序号为 x+1; -- B 收到 A 的确认后,连接建立。 +- A 向 B 发送连接请求报文,SYN=1,ACK=0,选择一个初始的序号 x,进入 SYN_SENT 状态; +- B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y,进入 SYN_RCVD 状态; +- A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 y+1,序号为 x+1,A 进入 ESTABLISHED 状态; +- B 收到 A 的确认后也进入 ESTABLISHED 状态,连接建立。 ### 三次握手的原因 +双方都要把自己的初始序号告诉对方,并得到对方的确认:A 发送 x,B 确认;B 发送 y,A 确认。这本来是四个动作,B 的确认和 B 的 SYN 合并在了一个报文中,所以只需要三次。只握手两次时,B 的初始序号没有得到确认,B 无法知道 A 是否收到了它。 + 第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。 客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。有了第三次握手,客户端收到服务器对滞留请求的确认时,发现确认号不是自己期望的,就回复 RST,服务器收到后放弃这个连接,不会多打开一个连接。 +### 握手报文丢失 + +每个握手报文都可能丢失,丢失后靠超时重传恢复(Linux 6.10 实测,见[附录 A2](#a2-握手报文丢失)): + +- 第一次握手的 SYN 丢失:客户端收不到 SYN+ACK,超时后重传 SYN。Linux 6.5 之前按 tcp_syn_retries(默认 6)重传,间隔从 1 秒起每次翻倍,约 127 秒后放弃;6.5 起前几次重传改为固定间隔 1 秒(tcp_syn_linear_timeouts,默认 4),实测共重传 10 次,134 秒后 connect() 返回超时错误; +- 第二次握手的 SYN+ACK 丢失:客户端以为 SYN 丢了,重传 SYN;服务器收不到第三次握手,也重传 SYN+ACK(tcp_synack_retries 默认 5 次,间隔 1、2、4、8、16 秒),任何一方的重传到达,连接都能建立; +- 第三次握手的 ACK 丢失:客户端发出 ACK 后已经进入 ESTABLISHED,服务器仍处于 SYN_RCVD,继续重传 SYN+ACK,客户端每收到一次就再回复一次 ACK。客户端发送的数据报文段也带有 ACK,服务器收到后同样会建立连接;如果客户端一直不发数据,服务器重传 5 次后放弃,之后客户端再发数据会收到 RST。 + ## TCP 的四次挥手

以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。 -- A 发送连接释放报文,FIN=1; -- B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据; -- 当 B 不再需要连接时,发送连接释放报文,FIN=1; +- A 发送连接释放报文,FIN=1,进入 FIN_WAIT_1 状态; +- B 收到之后发出确认,进入 CLOSE_WAIT 状态,A 收到确认后进入 FIN_WAIT_2 状态。此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据; +- 当 B 不再需要连接时,发送连接释放报文,FIN=1,进入 LAST_ACK 状态; - A 收到后发出确认,进入 TIME_WAIT 状态,等待 2 MSL(Maximum Segment Lifetime,最大报文段寿命)后释放连接; -- B 收到 A 的确认后释放连接。 +- B 收到 A 的确认后释放连接,进入 CLOSED 状态。 + +FIN 和数据一样,丢失后由发送方超时重传。A 在 FIN_WAIT_2 中等待 B 的 FIN,如果 A 的应用已经调用了 close(),而 B 迟迟不发 FIN,Linux 会在 tcp_fin_timeout(默认 60 秒)后直接释放这个连接。 ### 四次挥手的原因 客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE_WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文。 +如果 B 收到 FIN 时已经没有数据要发送,应用立即调用 close(),B 的确认和 FIN 就可以合并在一个报文段中,挥手只有三次。Linux 收到 FIN 后,确认会延迟几十毫秒再发送,应用在这期间调用 close() 就会合并(Linux 6.10 实测,见[附录 A3](#a3-三次挥手)): + +```text +B 读到 EOF 后立即 close() B 读到 EOF 后 1 秒再 close() +A → B FIN A → B FIN +B → A FIN + ACK B → A ACK 47 ms 后 +A → B ACK B → A FIN + ACK 1 秒后 + A → B ACK +``` + ### TIME_WAIT 客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由: -- 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生; +- 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生。A 在 TIME_WAIT 中每收到一次重传的 FIN,就再确认一次,并重新开始计时(Linux 6.10 实测,见[附录 A4](#a4-最后的-ack-丢失)); - 等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。 2MSL 正好覆盖一个来回:A 的确认最多在网络中存活 1 MSL,B 超时重传的 FIN 最多再存活 1 MSL。Linux 中 TIME_WAIT 固定为 60 秒(TCP_TIMEWAIT_LEN)。 @@ -229,6 +293,159 @@ static String receive(DataInputStream in) throws IOException { 需要注意的是,长度字段来自网络,必须限制上限,否则对方发来一个 2 GB 的长度,服务器就会为它分配 2 GB 内存。 +## 附录:实测 + +以下实验都在 Docker Desktop(Apple M1 Pro)中的 Ubuntu 24.04 容器里进行,内核为 6.10.14-linuxkit,客户端和服务器都在 127.0.0.1 上,用 iptables 丢弃指定的报文,用 tc netem 模拟时延和丢包,tcpdump 的时间换算为相对第一个报文的秒数。容器中的默认参数: + +```text +net.ipv4.tcp_syn_retries = 6 +net.ipv4.tcp_synack_retries = 5 +net.ipv4.tcp_syn_linear_timeouts = 4 +net.ipv4.tcp_fin_timeout = 60 +``` + +### A1 TCP 与 UDP 发送位置更新 + +在回环网卡上加入 25 ms 的单向时延(RTT 50 ms)和 5% 的丢包,发送方每 50 ms 发送一条 12 字节的更新(序号和发送时间),TCP 关闭 Nagle 算法,接收方用收到的时间减去发送时间得到延迟: + +```bash +tc qdisc add dev lo root netem delay 25ms loss 5% +``` + +```python +# TCP:长度固定为 12 字节,接收方按 12 字节切分 +s = socket.create_connection(("127.0.0.1", 9100)) +s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) +for i in range(600): + s.sendall(struct.pack("!Id", i, time.time())); time.sleep(0.05) + +# UDP:每条更新一个数据报 +for i in range(600): + s.sendto(struct.pack("!Id", i, time.time()), ("127.0.0.1", 9101)); time.sleep(0.05) +``` + +连续运行 5 次,配图使用第 3 次的数据: + +```text +TCP: 到达 600/600,延迟 p50 32 ms,p99 654 ms,最大 939 ms,超过 100 ms 的 52 条 +UDP: 到达 572/600,延迟 p50 33 ms,p99 37 ms,最大 40 ms,超过 100 ms 的 0 条 +TCP: 到达 600/600,延迟 p50 30 ms,p99 152 ms,最大 307 ms,超过 100 ms 的 24 条 +UDP: 到达 570/600,延迟 p50 33 ms,p99 37 ms,最大 37 ms,超过 100 ms 的 0 条 +TCP: 到达 600/600,延迟 p50 32 ms,p99 253 ms,最大 416 ms,超过 100 ms 的 43 条 +UDP: 到达 577/600,延迟 p50 33 ms,p99 37 ms,最大 44 ms,超过 100 ms 的 0 条 +TCP: 到达 600/600,延迟 p50 30 ms,p99 363 ms,最大 466 ms,超过 100 ms 的 53 条 +UDP: 到达 570/600,延迟 p50 33 ms,p99 37 ms,最大 39 ms,超过 100 ms 的 0 条 +TCP: 到达 600/600,延迟 p50 32 ms,p99 191 ms,最大 363 ms,超过 100 ms 的 35 条 +UDP: 到达 571/600,延迟 p50 33 ms,p99 37 ms,最大 42 ms,超过 100 ms 的 0 条 +``` + +### A2 握手报文丢失 + +第一次握手的 SYN 丢失:丢弃所有发往 9000 端口的 SYN,客户端调用 connect(): + +```text +$ iptables -A INPUT -p tcp --dport 9000 --tcp-flags SYN,ACK SYN -j DROP + 0.000 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799 + 1.038 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799 + 2.065 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799 + 3.085 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799 + 4.112 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799 + 5.140 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799 + 7.182 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799 + 11.220 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799 + 19.726 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799 + 36.110 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799 + 68.369 127.0.0.1.53094 > 127.0.0.1.9000: Flags [S], seq 3381671799 +connect 失败: TimeoutError(110, 'Connection timed out'),耗时 134.4 秒 +``` + +第二次握手的 SYN+ACK 丢失:前 4 秒丢弃服务器发出的 SYN+ACK(tcpdump 在丢弃之前抓包,所以被丢弃的报文也会显示): + +```text +$ iptables -A INPUT -p tcp --sport 9000 --tcp-flags SYN,ACK SYN,ACK -j DROP + 0.000 127.0.0.1.56260 > 127.0.0.1.9000: Flags [S] 客户端的 SYN + 0.000 127.0.0.1.9000 > 127.0.0.1.56260: Flags [S.] 被丢弃 + 1.062 127.0.0.1.9000 > 127.0.0.1.56260: Flags [S.] 服务器重传 SYN+ACK,被丢弃 + 1.062 127.0.0.1.56260 > 127.0.0.1.9000: Flags [S] 客户端重传 SYN + 1.062 127.0.0.1.9000 > 127.0.0.1.56260: Flags [S.] 被丢弃 + ... + 4.135 127.0.0.1.56260 > 127.0.0.1.9000: Flags [S] 4 秒时删除规则后,客户端再次重传 SYN + 4.135 127.0.0.1.9000 > 127.0.0.1.56260: Flags [S.] + 4.135 127.0.0.1.56260 > 127.0.0.1.9000: Flags [.], ack 1 +client: connect() 返回,4.1 秒 +``` + +第三次握手的 ACK 丢失:丢弃客户端发往 9000 端口的、不带数据的纯 ACK(长度不超过 52 字节),客户端连接成功后 5 秒发送 hello: + +```text +$ iptables -A INPUT -p tcp --dport 9000 --tcp-flags SYN,ACK,FIN,RST,PSH ACK -m length --length 0:52 -j DROP + 0.000 127.0.0.1.53690 > 127.0.0.1.9000: Flags [S] + 0.000 127.0.0.1.9000 > 127.0.0.1.53690: Flags [S.] + 0.000 127.0.0.1.53690 > 127.0.0.1.9000: Flags [.], ack 1 被丢弃 + 1.044 127.0.0.1.9000 > 127.0.0.1.53690: Flags [S.] 服务器重传 SYN+ACK + 1.044 127.0.0.1.53690 > 127.0.0.1.9000: Flags [.], ack 1 被丢弃 + 3.093 127.0.0.1.9000 > 127.0.0.1.53690: Flags [S.] + 3.093 127.0.0.1.53690 > 127.0.0.1.9000: Flags [.], ack 1 被丢弃 + 5.003 127.0.0.1.53690 > 127.0.0.1.9000: Flags [P.], seq 1:6, ack 1, length 5 + 5.003 127.0.0.1.9000 > 127.0.0.1.53690: Flags [.], ack 6 + +--- 3 秒时的连接状态(ss -tan) +ESTAB 0 0 127.0.0.1:53690 127.0.0.1:9000 +SYN-RECV 0 0 127.0.0.1:9000 127.0.0.1:53690 +client: 5 秒后发送 hello +server: accept() 返回 +server: 收到 b'hello' +``` + +同样丢弃纯 ACK,但客户端 70 秒后才发送数据:服务器在 1、3、7、15、31 秒重传 SYN+ACK 后放弃,客户端的数据收到 RST: + +```text + 31.822 127.0.0.1.9000 > 127.0.0.1.42608: Flags [S.] + 31.822 127.0.0.1.42608 > 127.0.0.1.9000: Flags [.], ack 1 + 70.009 127.0.0.1.42608 > 127.0.0.1.9000: Flags [P.], seq 1:6, ack 1, length 5 + 70.009 127.0.0.1.9000 > 127.0.0.1.42608: Flags [R], seq 4123133762, win 0, length 0 +client: ConnectionResetError(104, 'Connection reset by peer') +``` + +### A3 三次挥手 + +客户端发送 bye 后 0.2 秒调用 close(),服务器循环读取直到读到 EOF,然后立即或 1 秒后调用 close(): + +```text +=== 服务器读到 EOF 后立即 close() + 0.203 127.0.0.1.44056 > 127.0.0.1.9000: Flags [F.], seq 4, ack 1 + 0.204 127.0.0.1.9000 > 127.0.0.1.44056: Flags [F.], seq 1, ack 5 + 0.204 127.0.0.1.44056 > 127.0.0.1.9000: Flags [.], ack 2 +=== 服务器读到 EOF 后 1 秒再 close() + 0.210 127.0.0.1.44058 > 127.0.0.1.9000: Flags [F.], seq 4, ack 1 + 0.257 127.0.0.1.9000 > 127.0.0.1.44058: Flags [.], ack 5 + 1.220 127.0.0.1.9000 > 127.0.0.1.44058: Flags [F.], seq 1, ack 5 + 1.220 127.0.0.1.44058 > 127.0.0.1.9000: Flags [.], ack 2 +``` + +### A4 最后的 ACK 丢失 + +连接建立后丢弃客户端发往 9000 端口的纯 ACK,客户端主动关闭,服务器读到 EOF 后立即关闭,3.5 秒后删除规则。ss -tano 的 timer 一列显示 TIME_WAIT 剩余的时间: + +```text + 0.002 127.0.0.1.50966 > 127.0.0.1.9000: Flags [F.], seq 1, ack 1 + 0.002 127.0.0.1.9000 > 127.0.0.1.50966: Flags [F.], seq 1, ack 2 + 0.002 127.0.0.1.50966 > 127.0.0.1.9000: Flags [.], ack 2 被丢弃 + 0.210 127.0.0.1.9000 > 127.0.0.1.50966: Flags [F.], seq 1, ack 2 服务器重传 FIN + 0.210 127.0.0.1.50966 > 127.0.0.1.9000: Flags [.], ack 2 被丢弃 + 0.418 ... 0.826 ... 1.650 ... 3.314 重传间隔翻倍 + 6.578 127.0.0.1.9000 > 127.0.0.1.50966: Flags [F.], seq 1, ack 2 + 6.578 127.0.0.1.50966 > 127.0.0.1.9000: Flags [.], ack 2 规则已删除,到达 + +--- 2 秒时的连接状态 +LAST-ACK 0 1 127.0.0.1:9000 127.0.0.1:50966 timer:(on,1.254ms,3) +TIME-WAIT 0 0 127.0.0.1:50966 127.0.0.1:9000 timer:(timewait,59sec,0) +--- 8.5 秒时的连接状态 +TIME-WAIT 0 0 127.0.0.1:50966 127.0.0.1:9000 timer:(timewait,58sec,0) +``` + +8.5 秒时 TIME_WAIT 还剩 58 秒,说明 6.578 秒收到重传的 FIN 时重新开始了计时,否则只剩约 51 秒。 + ## 参考资料 - 谢希仁. 计算机网络 [M]. 电子工业出版社, 2017. @@ -239,4 +456,9 @@ static String receive(DataInputStream in) throws IOException { - [RFC 6928: Increasing TCP's Initial Window](https://www.rfc-editor.org/rfc/rfc6928) - [RFC 9438: CUBIC for Fast and Long-Distance Networks](https://www.rfc-editor.org/rfc/rfc9438) - [BBR: Congestion-Based Congestion Control](https://queue.acm.org/detail.cfm?id=3022184) -- [Linux ip-sysctl: tcp_tw_reuse、ip_local_port_range](https://www.kernel.org/doc/html/latest/networking/ip-sysctl.html) +- [Linux ip-sysctl: tcp_tw_reuse、ip_local_port_range、tcp_syn_retries、tcp_synack_retries、tcp_fin_timeout](https://www.kernel.org/doc/html/latest/networking/ip-sysctl.html) +- [RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport](https://www.rfc-editor.org/rfc/rfc9000) +- [RFC 9002: QUIC Loss Detection and Congestion Control](https://www.rfc-editor.org/rfc/rfc9002) +- [Langley A, et al. The QUIC Transport Protocol: Design and Internet-Scale Deployment. SIGCOMM 2017](https://dl.acm.org/doi/10.1145/3098822.3098842) +- [RFC 8835: Transports for WebRTC](https://www.rfc-editor.org/rfc/rfc8835) +- [RFC 8216: HTTP Live Streaming](https://www.rfc-editor.org/rfc/rfc8216)