mirror of
https://github.com/CyC2018/Interview-Notebook.git
synced 2026-10-30 07:33:45 +00:00
Update 计算机网络 - 传输层.md
补充 UDP 延迟低的原因和 DNS 使用 TCP 的情况、可靠传输的机制,新增确认与重传、超时重传时间两节,补充零窗口与窗口探测、窗口扩大选项,附录 A5~A7。
This commit is contained in:
+168
-1
@@ -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-零窗口与窗口探测)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
@@ -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-发送位置更新)):
|
||||
|
||||
<div align="center"> <img src="pics/tcp-udp-latency.png" width="760px"/> </div><br>
|
||||
@@ -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
|
||||
|
||||
其中 RTT<sub>d</sub> 为偏差的加权平均值。RFC 6298 建议 a 取 1/8,计算 RTT<sub>d</sub> 时的权重取 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)
|
||||
|
||||
Reference in New Issue
Block a user