Skip to content

TCP/IP 协议深度解析

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]
连接建立,可以传输数据

防止历史连接的干扰(历史重复连接初始化)

场景:客户端发送了一个 SYN(seq=100),因网络延迟很久才到达服务端。
客户端认为超时,重新发 SYN(seq=200)建立了新连接,使用完毕关闭。
此时旧的 SYN(seq=100)才到达服务端。
两次握手:服务端直接建立连接,进入 ESTABLISHED 状态,等待数据 → 浪费资源
三次握手:服务端回 SYN+ACK(ack=101),客户端发现不是期望的 ack(期望201),
发送 RST 给服务端,服务端关闭无效连接 → 正确处理历史连接

三次握手的本质是:双方都需要确认对方的初始序列号(ISN),且需要对方的确认。两次握手只有客户端确认了服务端的 ISN,服务端无法确认客户端的 ISN 是否被正确接收。

SYN+ACK 合并在一个包中发送,服务端在确认客户端 SYN 的同时发出自己的 SYN,节省了一次往返。

服务端维护两个队列:
半连接队列(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]

TCP 是全双工的,连接的两个方向需要分别关闭。

  • 客户端发 FIN 表示”我不再发数据了”
  • 服务端的 ACK 只是确认收到了 FIN
  • 服务端可能还有数据要发送(半关闭状态),需要等应用处理完才能发 FIN
  • 因此 ACK 和 FIN 不能合并(不像三次握手可以合并 SYN+ACK)

作用

  1. 保证最后一个 ACK 能到达对方:如果 ACK 丢失,服务端会重发 FIN,客户端需要在 TIME_WAIT 期间重新发 ACK,而不是直接 RST。
  2. 让网络中旧连接的残留报文消亡:等待 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 过多 = 服务端应用程序忘记关闭连接(BUG!)
原因:
服务端收到 FIN 后(进入 CLOSE_WAIT),应用代码没有调用 close()/shutdown()
→ 连接长期停留在 CLOSE_WAIT,占用文件描述符
排查:
netstat -an | grep CLOSE_WAIT | wc -l # 统计数量
查看应用日志和代码,找未关闭连接的地方
常见代码问题:
InputStream/OutputStream/Connection 没有在 finally 中关闭
连接池未正确归还连接

流量控制防止发送方发送速度超过接收方处理能力。

接收方在 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),直到接收方通告窗口不为零。


拥塞控制防止网络整体过载(不是防止某个接收方过载,那是流量控制的职责)。

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, 重新慢启动

对比项TCPUDP
连接面向连接(三次握手)无连接
可靠性可靠(ACK、重传)不可靠(Best Effort)
有序性保证有序不保证
流量控制有(滑动窗口)
拥塞控制
头部大小20~60 字节8 字节
传输单位字节流(无边界)数据报(有边界)
适用场景HTTP、FTP、邮件等要求可靠性的场景DNS、视频直播、游戏、QUIC
粘包:多个小数据包合并成一个 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?

  1. 保证最后一个 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,造成文件描述符泄漏。