TCP/IP 协议深度解析
TCP 的核心特性
Section titled “TCP 的核心特性”TCP(Transmission Control Protocol)是面向连接的、可靠的、基于字节流的传输层协议。
可靠性保证机制: • 序列号(Sequence Number):为每个字节编号,保证有序 • 确认应答(ACK):接收方确认已收到的数据 • 超时重传:发送方超时未收到 ACK 则重发 • 流量控制:通过滑动窗口控制发送速率,防止接收方被压垮 • 拥塞控制:探测网络容量,防止网络过载 • 校验和:检测数据在传输过程中是否损坏客户端 服务端 │ │ │ ── SYN(seq=x) ──────────► │ [SYN-SENT] → [SYN-RECEIVED] │ │ │ ◄── SYN+ACK(seq=y,ack=x+1)│ [SYN-RECEIVED] │ [SYN-SENT → ESTABLISHED] │ │ │ │ ── ACK(ack=y+1) ──────────►│ [SYN-RECEIVED → ESTABLISHED] │ │ [ESTABLISHED] [ESTABLISHED] 连接建立,可以传输数据为什么是三次而不是两次?
Section titled “为什么是三次而不是两次?”防止历史连接的干扰(历史重复连接初始化):
场景:客户端发送了一个 SYN(seq=100),因网络延迟很久才到达服务端。 客户端认为超时,重新发 SYN(seq=200)建立了新连接,使用完毕关闭。 此时旧的 SYN(seq=100)才到达服务端。
两次握手:服务端直接建立连接,进入 ESTABLISHED 状态,等待数据 → 浪费资源
三次握手:服务端回 SYN+ACK(ack=101),客户端发现不是期望的 ack(期望201), 发送 RST 给服务端,服务端关闭无效连接 → 正确处理历史连接三次握手的本质是:双方都需要确认对方的初始序列号(ISN),且需要对方的确认。两次握手只有客户端确认了服务端的 ISN,服务端无法确认客户端的 ISN 是否被正确接收。
为什么不是四次?
Section titled “为什么不是四次?”SYN+ACK 合并在一个包中发送,服务端在确认客户端 SYN 的同时发出自己的 SYN,节省了一次往返。
半连接队列与全连接队列
Section titled “半连接队列与全连接队列”服务端维护两个队列:
半连接队列(SYN Queue): 收到 SYN 后,连接进入此队列(SYN-RECEIVED 状态) 大小由 /proc/sys/net/ipv4/tcp_max_syn_backlog 控制 SYN Flood 攻击会填满此队列,可用 SYN Cookie 防御
全连接队列(Accept Queue): 完成三次握手后,连接进入此队列(ESTABLISHED 状态) 等待应用调用 accept() 取走 大小 = min(backlog, /proc/sys/net/core/somaxconn)
队列满了会怎样? SYN Queue 满 → 丢弃新 SYN 或启用 SYN Cookie Accept Queue 满 → 默认丢弃 ACK(Linux),导致客户端重发,最终超时主动关闭方(客户端) 被动关闭方(服务端) │ │ │ ── FIN(seq=u)──────────────► │ [CLOSE_WAIT] │ [FIN-WAIT-1] │ 应用还可以继续发送数据 │ │ │ ◄── ACK(ack=u+1)─────────── │ │ [FIN-WAIT-2] │ │ │ 应用调用 close() │ ◄── FIN(seq=v)──────────────│ [LAST-ACK] │ [TIME-WAIT] │ │ │ │ ── ACK(ack=v+1)────────────►│ [CLOSED] │ │ 等待 2MSL 后 │ [CLOSED]为什么是四次而不是三次?
Section titled “为什么是四次而不是三次?”TCP 是全双工的,连接的两个方向需要分别关闭。
- 客户端发 FIN 表示”我不再发数据了”
- 服务端的 ACK 只是确认收到了 FIN
- 服务端可能还有数据要发送(半关闭状态),需要等应用处理完才能发 FIN
- 因此 ACK 和 FIN 不能合并(不像三次握手可以合并 SYN+ACK)
TIME_WAIT 状态
Section titled “TIME_WAIT 状态”作用:
- 保证最后一个 ACK 能到达对方:如果 ACK 丢失,服务端会重发 FIN,客户端需要在 TIME_WAIT 期间重新发 ACK,而不是直接 RST。
- 让网络中旧连接的残留报文消亡:等待 2MSL(Maximum Segment Lifetime,Linux 默认 60s,即 TIME_WAIT = 120s),确保旧连接的所有报文在网络中消失,防止被新连接误收。
TIME_WAIT 过多的问题与解决:
问题:高并发短连接场景(如爬虫、HTTP 短轮询)会产生大量 TIME_WAIT 占用端口资源(每个连接占用本地端口,Linux 默认端口范围 32768-60999,约 28000 个)
解决方案: net.ipv4.tcp_tw_reuse = 1 # 允许 TIME_WAIT 的端口被新连接复用(仅客户端) net.ipv4.tcp_timestamps = 1 # 配合 tw_reuse 使用,通过时间戳区分新旧报文 减少短连接:使用连接池、HTTP Keep-Alive、长连接
注意:tcp_tw_recycle 在 Linux 4.12 已移除,NAT 环境下会导致连接问题,不要使用。CLOSE_WAIT 过多的问题
Section titled “CLOSE_WAIT 过多的问题”CLOSE_WAIT 过多 = 服务端应用程序忘记关闭连接(BUG!)
原因: 服务端收到 FIN 后(进入 CLOSE_WAIT),应用代码没有调用 close()/shutdown() → 连接长期停留在 CLOSE_WAIT,占用文件描述符
排查: netstat -an | grep CLOSE_WAIT | wc -l # 统计数量 查看应用日志和代码,找未关闭连接的地方
常见代码问题: InputStream/OutputStream/Connection 没有在 finally 中关闭 连接池未正确归还连接流量控制:滑动窗口
Section titled “流量控制:滑动窗口”流量控制防止发送方发送速度超过接收方处理能力。
接收方在 TCP 头的 Window 字段(16 bit,最大 65535 字节,可通过窗口缩放选项扩展)中告知发送方自己的接收窗口大小(rwnd = Receive Window)。
发送方最多可以发送 min(cwnd, rwnd) 字节的未确认数据 cwnd = 拥塞窗口(由拥塞控制算法维护) rwnd = 接收方通告的接收窗口
滑动窗口示意:已确认 │ 已发送未确认 │ 可以发送 │ 不可发送(窗口外)───────┼─────────────┼─────────┼──────────────── seq<200 200~299 300~499 ≥500 ↑ 窗口左边沿 ↑ 窗口右边沿(= 左边沿 + rwnd)
收到 ACK 后,窗口向右滑动零窗口与窗口探测:接收方缓冲区满时通告 rwnd=0,发送方停止发送,但会定期发送窗口探测包(Zero Window Probe),直到接收方通告窗口不为零。
拥塞控制防止网络整体过载(不是防止某个接收方过载,那是流量控制的职责)。
四个核心算法
Section titled “四个核心算法”1. 慢启动(Slow Start) 初始 cwnd = 1 MSS(Maximum Segment Size,通常 1460 字节) 每收到一个 ACK:cwnd += 1 MSS 效果:每个 RTT 内 cwnd 翻倍(指数增长) 直到 cwnd 达到 ssthresh(慢启动阈值)
2. 拥塞避免(Congestion Avoidance) cwnd > ssthresh 后进入拥塞避免 每个 RTT:cwnd += 1 MSS(线性增长,更保守)
3. 快速重传(Fast Retransmit) 收到 3 个重复 ACK(丢包信号,但网络未完全拥塞) → 立即重传丢失的报文段(不等超时)
4. 快速恢复(Fast Recovery) 配合快速重传: ssthresh = cwnd / 2 cwnd = ssthresh + 3 MSS(不回到 1,从中间位置继续) → 重传后进入拥塞避免阶段(而非慢启动)
超时重传(严重拥塞): ssthresh = cwnd / 2 cwnd = 1 MSS → 重新进入慢启动(最激进的回退)cwnd 变化示意:
cwnd │ / │ / 拥塞避免(线性增长) │ /ssth │───────── │ ↗ 慢启动(指数) │0────┼────────────────────────────────► 时间 收到3dup ACK → ssthresh=cwnd/2, cwnd=ssthresh+3, 快速恢复 超时 → ssthresh=cwnd/2, cwnd=1, 重新慢启动TCP vs UDP
Section titled “TCP vs UDP”| 对比项 | TCP | UDP |
|---|---|---|
| 连接 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠(ACK、重传) | 不可靠(Best Effort) |
| 有序性 | 保证有序 | 不保证 |
| 流量控制 | 有(滑动窗口) | 无 |
| 拥塞控制 | 有 | 无 |
| 头部大小 | 20~60 字节 | 8 字节 |
| 传输单位 | 字节流(无边界) | 数据报(有边界) |
| 适用场景 | HTTP、FTP、邮件等要求可靠性的场景 | DNS、视频直播、游戏、QUIC |
TCP 粘包与拆包
Section titled “TCP 粘包与拆包”粘包:多个小数据包合并成一个 TCP 报文发送 原因:Nagle 算法(等待积累一定数据再发送);网络延迟导致多个包被合并
拆包:一个大数据包被拆成多个 TCP 报文发送 原因:数据大于 MSS;MTU 限制(以太网 MTU = 1500 字节)
解决方案(应用层协议设计): 1. 固定长度:每个消息固定 N 字节(简单,但灵活性差) 2. 分隔符:如 \n、\r\n(HTTP 使用) 3. 长度前缀:消息头包含消息体长度(Protobuf、RPC 框架常用) 4. 使用应用层协议:HTTP Content-Length 或 chunked 编码Q:TCP 三次握手为什么不能是两次?
防止历史失效连接请求到达服务端导致资源浪费,以及确保双方都能确认对方的初始序列号。两次握手时,服务端无法确认客户端的 SYN 是否是最新的,旧的延迟 SYN 可能导致服务端建立无效连接。
Q:TIME_WAIT 的作用是什么?为什么是 2MSL?
- 保证最后一个 ACK 能到达对方(如果丢失,服务端重发 FIN,客户端需要在 TIME_WAIT 期间能够处理);2. 让网络中残留的旧报文消亡(1 个 MSL 让旧报文到达对方,对方的响应再用 1 个 MSL 回来或消亡),2MSL 后可以安全使用相同端口建立新连接。
Q:TCP 的流量控制和拥塞控制有什么区别?
流量控制是端到端的,由接收方通过 Window 字段告知发送方自己的处理能力,防止发送方压垮接收方(接收缓冲区溢出)。拥塞控制是发送方对整个网络传输能力的感知与适应,通过拥塞窗口(cwnd)控制,防止向网络注入太多数据导致路由器队列溢出和大量丢包。
Q:CLOSE_WAIT 状态很多是什么原因?
服务端应用程序没有调用 close() 关闭连接。收到客户端 FIN 后服务端会自动发 ACK 进入 CLOSE_WAIT,但等待应用程序调用 close() 后才发 FIN。如果应用忘记关闭(比如未正确关闭 InputStream/Connection),连接会长期停留在 CLOSE_WAIT,造成文件描述符泄漏。