TCP、UDP、ICMP — “不通”的三副面孔
一句话总结
TCP 通过 SYN、SYN-ACK、ACK 三次交互建立连接,用 FIN 关闭连接,而访问已关闭的端口时会以 RST 应答。UDP 没有连接,访问已关闭的端口时会通过 ICMP port unreachable 告知。超过 MTU 的数据包,在未设置 DF 时会被分片,设置 DF 时则被丢弃,而通知这一结果的同样是 ICMP。
为什么需要它
“连接不上”这一句话里,混着三种原因不同的情况。发出 SYN 后收到 RST,说明对方是活的,只是那个端口上没有任何进程。什么都没收到,说明防火墙丢弃了数据包,或者根本没有路径。已经收到 SYN-ACK 却没有数据到达,说明应用程序卡住了。对用户来说三者都是“不通”,但需要动手修复的人完全不同。只要会读标志位,前 30 秒就能把这三种情况区分开。
工作原理
TCP 握手之所以需要三次,是因为双方都必须收到对方对自己起始序列号的确认的确认。
主动关闭的一方会在 TIME-WAIT 状态停留 60 秒——原因是它有责任在最后一个 ACK 丢失时重新发送,还要封存四元组,避免延迟到达的旧报文段混入新连接。每秒打开数千个短连接的客户端会因此耗尽临时端口(
ip_local_port_range,通常为 32768–60999)。客户端端口由内核在这个范围内选取,只有服务器端口才是事先约定的。
UDP 发出后就结束了。它不知道对方是否在监听,也不知道对方是否收到。向已关闭的端口发送时,内核会返回 ICMP port unreachable;但如果防火墙整体屏蔽了 ICMP,这个回复也会消失,只剩下“无响应”。这就是 DNS 自己负责重试的原因。
MTU 是链路所能承载的 IP 数据包的最大长度。以太网为 1500,减去 IP 头 20 和 ICMP 头 8,ping 的载荷最大为 1472。用 -M do 开启 DF 后,一旦超出就会出现 message too long,关闭 DF 则会被分片。分片虽然能工作,但只要丢失一个分片就必须重传整个数据包,而且防火墙看不到后续分片的端口。因此现在通常开启 DF 并探测路径 MTU(PMTUD),而传递这一信号的 ICMP fragmentation needed 一旦被防火墙屏蔽,就会出现小数据包能通过、只有大数据包消失的故障。
在现场相遇的样子
SSH 可以用,只有文件传输卡住。 问题出现在新接入 VPN 之后。隧道头使实际 MTU 小于 1500,而 DF 数据包超出了它,ICMP 又被屏蔽。提示符这类小数据包可以通过,只有正文会丢失。用 ping -M do -s 1472 就能找出是在哪里断掉的。
临时端口耗尽。 代理服务器每秒向后端打开数千个短连接。ss -s 显示 TIME-WAIT 有 2 万个,新连接因 Cannot assign requested address 而失败。解决办法是用 keep-alive 复用连接。
连接中断的三种方式
“连接断了”同样有多种原因,每种原因要在不同的位置修复。
用 FIN 正常关闭。 一方告知自己没有更多数据要发送。这表示应用程序关闭了套接字,或者进程正常退出,因此应检查那一端的代码或重启情况,而不是网络。
用 RST 强制关闭。 对方回应“没有这个连接”。进程突然退出、中间设备把该连接从会话表中删除,或者应用程序在还有未读数据时关闭了套接字,都会出现这种情况。
没有任何信号,悄悄卡住。 这最麻烦。双方都以为连接还活着,实际上连接已在中途断开。常见原因是 NAT 或防火墙的会话过期。这类设备会把一段时间内没有流量的连接从表中删除,却不会通知双方。于是安静了几分钟的数据库连接,在下一次查询时就永远得不到响应。
防止这种悄无声息的中断,方法是定期发送点什么。TCP 自带的 keepalive 默认值是两小时,远远晚于大多数会话过期时间,所以要调小这个值,或者在应用层自行发送信号。连接池具备“借出前确认连接是否存活”的功能,也是针对同一问题的解决办法。
通过症状区分这三者很简单。 立即报错的是 FIN 或 RST;什么都没发生、挂了很久才超时的是悄悄中断。而且悄悄中断只会在空闲时间较长之后的第一个请求上出现,所以往往以“只有早上的第一个请求失败”这样的反馈形式上报。
下一项实验要做什么
在 lo 上搭建 nc 服务器和客户端,用 tcpdump 捕获握手、关闭和 RST,再用 ss 查看 TIME-WAIT 和临时端口。接着在两个命名空间之间捕获 ICMP echo,用 ping -M do 找出 MTU 的边界,并捕获 3000 字节的 ping 被分片的情况以及已关闭 UDP 端口的 unreachable。