From 8fcb897662cdfe656f62af188e51db88913b2ed8 Mon Sep 17 00:00:00 2001 From: CyC2018 Date: Thu, 29 Oct 2026 04:55:49 +0800 Subject: [PATCH] =?UTF-8?q?Update=20=E8=AE=A1=E7=AE=97=E6=9C=BA=E7=BD=91?= =?UTF-8?q?=E7=BB=9C=20-=20=E4=BC=A0=E8=BE=93=E5=B1=82.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补充 UDP 延迟低的原因和 DNS 使用 TCP 的情况、可靠传输的机制,新增确认与重传、超时重传时间两节,补充零窗口与窗口探测、窗口扩大选项,附录 A5~A7。 --- notes/计算机网络 - 传输层.md | 169 ++++++++++++++++++++++++++++++++++- 1 file changed, 168 insertions(+), 1 deletion(-) diff --git a/notes/计算机网络 - 传输层.md b/notes/计算机网络 - 传输层.md index 39828fcd..0edee9c5 100644 --- a/notes/计算机网络 - 传输层.md +++ b/notes/计算机网络 - 传输层.md @@ -13,6 +13,8 @@ * [TIME_WAIT](#time_wait) * [大量 TIME_WAIT 与 CLOSE_WAIT](#大量-time_wait-与-close_wait) * [TCP 可靠传输](#tcp-可靠传输) + * [确认与重传](#确认与重传) + * [超时重传时间](#超时重传时间) * [TCP 滑动窗口](#tcp-滑动窗口) * [TCP 流量控制](#tcp-流量控制) * [TCP 拥塞控制](#tcp-拥塞控制) @@ -25,6 +27,9 @@ * [A2 握手报文丢失](#a2-握手报文丢失) * [A3 三次挥手](#a3-三次挥手) * [A4 最后的 ACK 丢失](#a4-最后的-ack-丢失) + * [A5 DNS 改用 TCP](#a5-dns-改用-tcp) + * [A6 快重传与 SACK](#a6-快重传与-sack) + * [A7 零窗口与窗口探测](#a7-零窗口与窗口探测) * [参考资料](#参考资料) @@ -36,6 +41,8 @@ - 用户数据报协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信; - 传输控制协议 TCP(Transmission Control Protocol)是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块),每一条 TCP 连接只能是点对点的(一对一)。 +UDP 延迟低,不是因为它发得更快,而是省掉了 TCP 为可靠付出的等待:不需要握手,第一个数据报就带着数据;丢包时不等重传,后面的数据照常交给应用;没有拥塞控制,不会因为丢包而主动降低发送速率;首部只有 8 字节,TCP 至少 20 字节。需要注意的是,这也意味着网络拥塞时,UDP 应用要自己控制发送速率,否则会加重拥塞。 + ### 选择 TCP 还是 UDP TCP 的可靠和按序是有代价的:一个报文段丢失后,后面已经到达的数据也要等它重传成功,才能按顺序交给应用。 @@ -46,6 +53,8 @@ TCP 的可靠和按序是有代价的:一个报文段丢失后,后面已经 直播常用的 HLS 基于 HTTP,也就是 TCP,观众能接受几秒的延迟,换来画面完整。 +DNS 也不是只用 UDP。不带 EDNS 扩展时,UDP 上的 DNS 响应限制为 512 字节,超出时服务器只返回放得下的部分并设置 TC(截断)标志,客户端改用 TCP 重新查询;主从 DNS 服务器之间同步整个区域的数据(区域传送)也用 TCP。例如不带 EDNS 查询 google.com 的 TXT 记录,UDP 响应被截断,dig 改用 TCP 后收到 1176 字节的完整响应(实测,见[附录 A5](#a5-dns-改用-tcp))。RFC 7766 要求 DNS 实现同时支持 UDP 和 TCP。 + 例如游戏客户端每 50 ms 上报一次位置,在 RTT 为 50 ms、丢包率为 5% 的网络中分别用 TCP 和 UDP 发送 600 次(Linux 实测,见[附录 A1](#a1-tcp-与-udp-发送位置更新)):

@@ -153,7 +162,40 @@ CLOSE_WAIT 出现在被动关闭的一方,表示对方已经关闭,本方的 ## TCP 可靠传输 -TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。 +IP 层只尽力交付,分组可能出错、丢失、重复或者乱序到达。TCP 在此之上用以下机制保证应用读到的字节流与对方写入的完全一致: + +- 检验和:首部和数据都参与计算,出错的报文段直接丢弃,当作丢失处理; +- 序号和确认:每个字节都有序号,接收方据此丢弃重复的数据、把乱序到达的数据排好顺序,并用确认号告诉发送方哪些已经收到; +- 重传:发送方超时没有收到确认,或者收到多个重复确认,就重传数据; +- 滑动窗口、流量控制和拥塞控制:在等待确认的同时连续发送多个报文段,并让发送速率不超过接收方和网络的处理能力,见后面几节。 + +三次握手也是可靠传输的一部分:双方在握手中交换并确认了各自的初始序号,之后的序号和确认才有共同的起点。 + +### 确认与重传 + +TCP 的确认是累积的:确认号为 N 表示 N 之前的字节都已收到。例如发送方连续发出 8 个报文段,每段 1000 字节,第 2 段丢失,双方都支持选择确认(Selective Acknowledgment, SACK): + +```text +发送方 接收方 +序号 1~1000 至 7001~8000,共 8 段 → 1001~2000 丢失 + ← ack 1001 + ← ack 1001,SACK 2001~3000 + ← ack 1001,SACK 2001~4000 + …… + ← ack 1001,SACK 2001~8000 +重传 1001~2000 → + ← ack 8001 +``` + +第 3 段到达时,接收方仍然只能确认到 1001,于是重复发送 ack 1001,称为重复确认。发送方收到 3 个重复确认,就知道 1001 开始的这一段丢了,不等超时立即重传,即快重传;接收方补上空洞后,一次确认到 8001。 + +SACK 是握手时协商的选项,确认中额外携带已经收到的区间。上例中 SACK 2001~8000 告诉发送方只缺 1001~2000,发送方只重传这一段;没有 SACK 时,发送方只知道 1001 之后有缺失,不知道后面哪些已经到达。 + +在 Linux 上丢弃第 2 段,抓包结果与上面一致:接收方发出 1 个确认和 6 个重复确认,SACK 区间逐步扩大到 2001~8000,发送方在第 2 段首次发出后约 28 ms 重传,远小于 200 ms 的最小超时时间(Linux 6.10 实测,见[附录 A6](#a6-快重传与-sack))。快重传之后拥塞窗口的变化见[快重传与快恢复](#2-快重传与快恢复)。 + +### 超时重传时间 + +如果一个已经发送的报文段在超时时间内没有收到确认,发送方就重传这个报文段。超时时间太短会把晚到的确认误判为丢失,造成不必要的重传;太长则丢包后要等很久才恢复,所以要根据测得的往返时间动态计算。 一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT,加权平均往返时间 RTTs 计算如下: @@ -171,6 +213,13 @@ RTO = RTTs + 4 × RTTd 其中 RTTd 为偏差的加权平均值。RFC 6298 建议 a 取 1/8,计算 RTTd 时的权重取 1/4,并建议 RTO 不小于 1 秒;Linux 的下限为 200 ms。 +例如先后测得的 RTT 为 100、120、300、110 ms,按 RFC 6298 计算,RTO 依次为 300、273、452、386 ms(不计 1 秒的下限)。RTT 稳定时 RTO 逐渐向 RTT 靠拢;一次 300 ms 的抖动让偏差项变大,RTO 随之变长,避免下一次抖动时误判丢失。 + +此外还有两条规则: + +- 重传过的报文段不用来测量 RTT(Karn 算法),因为收到确认时分不清它确认的是原来那次还是重传的那次。TCP 的时间戳选项把发送时间放在报文段中,对方确认时原样带回,可以准确测量; +- 每次超时后 RTO 翻倍,称为指数退避,网络拥塞时可以减少重传的报文。例如[附录 A4](#a4-最后的-ack-丢失)中服务器重传 FIN 的间隔从约 0.2 秒增长到约 3.3 秒。 + ## TCP 滑动窗口 窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。 @@ -187,6 +236,22 @@ RTO = RTTs + 4 × RTTd 接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。 +接收方通告的窗口就是接收缓冲区中还空着的部分。应用读得慢,缓冲区逐渐填满,窗口就逐渐变小直到 0,发送方随之停下;应用读走数据后,接收方发出窗口更新,发送方继续发送。例如服务器把接收缓冲区设为 16 KB 并且暂不读取,客户端不停地写入(Linux 6.10 实测,见[附录 A7](#a7-零窗口与窗口探测)): + +```text +时间(秒) 服务器通告的窗口(字节) 说明 +0.000 15928、15000、14000、13000 数据陆续到达,服务器不读取 +0.042 5792 +0.083 1448 +0.124 0 客户端停止发送 +0.34、0.77、1.69 0 客户端发送零窗口探测,回复的窗口仍为 0 +3.002 15928 服务器读走数据,客户端立即恢复发送 +``` + +窗口为 0 时,发送方启动持续计时器,定期发送零窗口探测报文段,间隔逐次翻倍,对方收到后必须回复当前的窗口。这是因为窗口更新报文不携带数据,丢失后不会重传,如果发送方只是等待,双方就会一直互相等待。 + +首部的窗口字段只有 16 位,最大 65535 字节。在带宽大、延迟高的网络中不够用,例如 RTT 为 100 ms 时,64 KB 的窗口最多只能达到约 5 Mbit/s,而 1 Gbit/s 的链路一个 RTT 内可以发出 12.5 MB。窗口扩大选项在握手时协商一个移位数,实际窗口为窗口字段左移这么多位,移位数最大为 14,窗口最大约 1 GB。 + ## TCP 拥塞控制 如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。 @@ -450,6 +515,103 @@ TIME-WAIT 0 0 127.0.0.1:50966 127.0.0.1:9000 timer:(timewait,5 8.5 秒时 TIME_WAIT 还剩 58 秒,说明 6.578 秒收到重传的 FIN 时重新开始了计时,否则只剩约 51 秒。 +### A5 DNS 改用 TCP + +同一个容器,dig 9.18.39,向公共 DNS 223.5.5.5 查询,+noedns 表示不带 EDNS 扩展,+ignore 表示收到截断的响应后不改用 TCP(节选): + +```text +$ dig @223.5.5.5 www.baidu.com A +;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1 +;; SERVER: 223.5.5.5#53(223.5.5.5) (UDP) +;; MSG SIZE rcvd: 88 + +$ dig +ignore +noedns @223.5.5.5 google.com TXT +;; flags: qr tc rd ra; QUERY: 1, ANSWER: 8, AUTHORITY: 0, ADDITIONAL: 0 +;; SERVER: 223.5.5.5#53(223.5.5.5) (UDP) +;; MSG SIZE rcvd: 493 + +$ dig +noedns @223.5.5.5 google.com TXT +;; Truncated, retrying in TCP mode. +;; flags: qr rd ra; QUERY: 1, ANSWER: 17, AUTHORITY: 0, ADDITIONAL: 0 +;; SERVER: 223.5.5.5#53(223.5.5.5) (TCP) +;; MSG SIZE rcvd: 1176 +``` + +UDP 响应只放得下 17 条记录中的 8 条,并设置了 tc 标志。默认带 EDNS 时,dig 声明可以接收 1232 字节的 UDP 响应,这条 1176 字节的响应不会被截断。 + +### A6 快重传与 SACK + +回环网卡的 MTU 设为 1500,加入 10 ms 的时延,用 iptables 丢弃发往 9000 端口的第 2 个数据报文段。客户端关闭 Nagle 算法,连续写入 8 次,每次 1000 字节: + +```bash +ip link set lo mtu 1500 +tc qdisc add dev lo root netem delay 10ms +iptables -A INPUT -p tcp --dport 9000 -m length --length 100: -m statistic --mode nth --every 1000 --packet 1 -j DROP +``` + +```python +cl = socket.create_connection(("127.0.0.1", 9000)) +cl.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) +for i in range(8): + cl.send(bytes([65 + i]) * 1000) +``` + +tcpdump 的结果(时间为毫秒,省略了部分字段): + +```text + 130.5 52904 > 9000: Flags [P.], seq 1:1001, length 1000 + 130.6 52904 > 9000: Flags [P.], seq 1001:2001, length 1000 被丢弃 + 130.6 52904 > 9000: Flags [P.], seq 2001:3001, length 1000 + ... + 130.6 52904 > 9000: Flags [P.], seq 7001:8001, length 1000 + 143.2 9000 > 52904: Flags [.], ack 1001 + 143.2 9000 > 52904: Flags [.], ack 1001, sack 1 {2001:3001} + 143.2 9000 > 52904: Flags [.], ack 1001, sack 1 {2001:4001} + 143.2 9000 > 52904: Flags [.], ack 1001, sack 1 {2001:5001} + 143.3 9000 > 52904: Flags [.], ack 1001, sack 1 {2001:6001} + 143.3 9000 > 52904: Flags [.], ack 1001, sack 1 {2001:7001} + 143.3 9000 > 52904: Flags [.], ack 1001, sack 1 {2001:8001} + 158.1 52904 > 9000: Flags [P.], seq 1001:2001, length 1000 重传 + 169.3 9000 > 52904: Flags [.], ack 8001 +server 收到 8000 字节 +``` + +tcpdump 中的区间左闭右开,{2001:3001} 即 2001~3000。容器中 net.ipv4.tcp_recovery 为 1,Linux 除了重复确认的个数,还按时间判断丢失(RACK,RFC 8985)。 + +### A7 零窗口与窗口探测 + +为了让 tcpdump 显示的窗口值就是字节数,启动容器时关闭窗口扩大选项(docker run --sysctl net.ipv4.tcp_window_scaling=0)。服务器在 listen 之前把接收缓冲区设为 16 KB,accept 之后 3 秒不读取;客户端把发送缓冲区设为 16 KB,以非阻塞方式循环写入 1 秒: + +```python +srv.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 16384) +... +c, _ = srv.accept() +time.sleep(3) # 3 秒后才开始读取 +while True: + d = c.recv(65536) +``` + +服务器发出的报文段和客户端的探测(时间为秒,省略了部分字段): + +```text +0.000 9000 > 39616: Flags [S.], seq 1554460745, ack 3376993815, win 15928, length 0 +0.000 9000 > 39616: Flags [.], ack 1001, win 15928, length 0 +0.000 9000 > 39616: Flags [.], ack 3001, win 15000, length 0 +0.000 9000 > 39616: Flags [.], ack 5001, win 14000, length 0 +0.000 9000 > 39616: Flags [.], ack 7001, win 13000, length 0 +0.042 9000 > 39616: Flags [.], ack 22449, win 5792, length 0 +0.083 9000 > 39616: Flags [.], ack 28241, win 1448, length 0 +0.124 9000 > 39616: Flags [.], ack 29689, win 0, length 0 +0.341 39616 > 9000: Flags [.], ack 1, win 64240, length 0 客户端的零窗口探测 +0.341 9000 > 39616: Flags [.], ack 29689, win 0, length 0 +0.774 39616 > 9000: Flags [.], ack 1, win 64240, length 0 +1.689 39616 > 9000: Flags [.], ack 1, win 64240, length 0 +1.689 9000 > 39616: Flags [.], ack 29689, win 0, length 0 +3.002 9000 > 39616: Flags [.], ack 29689, win 15928, length 0 服务器开始读取,客户端随即继续发送 +``` + +连续运行 3 次,窗口变为 0 的时间为 0.124~0.127 秒,3 次探测的时间分别为 0.34、0.77~0.79、1.65~1.69 秒,间隔约 0.2、0.4、0.9 秒。 + ## 参考资料 - 谢希仁. 计算机网络 [M]. 电子工业出版社, 2017. @@ -466,3 +628,8 @@ TIME-WAIT 0 0 127.0.0.1:50966 127.0.0.1:9000 timer:(timewait,5 - [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) +- [RFC 2018: TCP Selective Acknowledgment Options](https://www.rfc-editor.org/rfc/rfc2018) +- [RFC 7323: TCP Extensions for High Performance](https://www.rfc-editor.org/rfc/rfc7323) +- [RFC 8985: The RACK-TLP Loss Detection Algorithm for TCP](https://www.rfc-editor.org/rfc/rfc8985) +- [RFC 1035: Domain Names - Implementation and Specification](https://www.rfc-editor.org/rfc/rfc1035) +- [RFC 7766: DNS Transport over TCP - Implementation Requirements](https://www.rfc-editor.org/rfc/rfc7766)