3. TCP协议详解:报文结构、三次握手与四次挥手、滑动窗口与流量控制

TCP 这东西,说白了就是给数据包装了个「可靠快递」系统。我刚开始接触网络时,总觉得 UDP 简单粗暴挺好用,直到有一次做文件传输项目,丢包丢到怀疑人生……嗯,从那以后我才真正理解 TCP 为什么是互联网的基石。

3.1 TCP报文结构:头里藏着什么秘密?

TCP 报文段由头部和数据两部分组成。头部最少 20 字节,最多 60 字节。我习惯把头部想象成快递面单——上面写满了收发地址、包裹状态和特殊要求。

字段 长度(位) 作用
源端口 / 目的端口 16 + 16 标识应用进程
序列号(SEQ) 32 字节流编号,保证有序
确认号(ACK) 32 期望收到的下一个序列号
数据偏移 4 头部长度(单位 4 字节)
标志位 9 SYN、ACK、FIN 等控制位
窗口大小 16 接收方剩余缓冲区
校验和 16 端到端完整性校验
紧急指针 16 紧急数据位置(URG=1 时有效)

核心要点:序列号和确认号是 TCP 可靠传输的「双保险」。序列号标记每个字节的位置,确认号告诉对方「我收到哪了,下一个要哪个」。

我记得有一次排查一个诡异的断连问题,抓包一看,发现校验和字段全是错的——原来是网卡硬件 offload 没关,驱动把校验和算错了。从那以后我排查 TCP 问题,第一件事就是看校验和。

3.2 三次握手:建立连接的三步舞

为什么是三次?不是两次也不是四次?我当年面试时也被问过这个问题。你想想看,如果只有两次握手,服务器怎么知道客户端收到了自己的 SYN+ACK?

  1. 第一次握手(SYN):客户端发送 SYN=1,SEQ=x,进入 SYN_SENT 状态。
  2. 第二次握手(SYN+ACK):服务器回复 SYN=1,ACK=1,SEQ=y,ACK_NUM=x+1,进入 SYN_RCVD 状态。
  3. 第三次握手(ACK):客户端发送 ACK=1,SEQ=x+1,ACK_NUM=y+1,进入 ESTABLISHED 状态。

实战技巧:我习惯用 tcpdump -S 抓三次握手包,看 SEQ 和 ACK 的对应关系。如果第三次握手丢了,服务器会重传 SYN+ACK,客户端收到后重新发 ACK。这个重传机制救过我好几次。

我曾经遇到过一个线上问题:客户端连接耗时突然飙升到 3 秒。抓包发现,三次握手的 SYN 包被防火墙拦截了,重传了 3 次才成功。嗯,后来加了白名单,问题解决。

3.3 四次挥手:优雅地分手

TCP 是全双工的,所以断开连接需要四次交互。每次关闭一个方向的数据流,就需要一次 FIN 和一次 ACK。

  1. 第一次挥手(FIN):主动关闭方发送 FIN=1,SEQ=u,进入 FIN_WAIT_1。
  2. 第二次挥手(ACK):被动关闭方回复 ACK=1,SEQ=v,ACK_NUM=u+1,进入 CLOSE_WAIT。
  3. 第三次挥手(FIN):被动关闭方发送 FIN=1,SEQ=w,ACK_NUM=u+1,进入 LAST_ACK。
  4. 第四次挥手(ACK):主动关闭方回复 ACK=1,SEQ=u+1,ACK_NUM=w+1,进入 TIME_WAIT。

注意:TIME_WAIT 状态会持续 2MSL(约 2 分钟)。我见过很多高并发服务器因为 TIME_WAIT 过多导致端口耗尽,最后用 net.ipv4.tcp_tw_reuse 和 tcp_tw_recycle 缓解。但注意,tcp_tw_recycle 在 NAT 环境下有坑,慎用。

我曾经帮一个游戏公司排查连接断开慢的问题。客户端直接发 RST 而不是 FIN,服务器端 CLOSE_WAIT 堆积,最后连接数爆了。解决方案很简单:应用层加心跳,超时主动关闭。

3.4 滑动窗口与流量控制

滑动窗口是 TCP 的「流量调节阀」。接收方告诉发送方「我还能收多少」,发送方据此控制发送速度。说白了,就是防止发送方太快,把接收方撑死。

窗口大小由接收方的剩余缓冲区决定。每次 ACK 包里都带着这个值。如果窗口为 0,发送方就停止发送,启动「窗口探测」定时器。

关键机制:

  • 发送窗口:由接收方通告窗口和拥塞窗口共同决定,取较小值。
  • 接收窗口(rwnd):接收方剩余缓冲区大小,动态变化。
  • 拥塞窗口(cwnd):网络拥塞控制用,后面章节会细讲。

我记得有一次做视频流传输优化,发现吞吐量上不去。抓包一看,接收窗口经常缩到 16KB——原来是应用层读数据太慢,缓冲区一直满着。后来改成非阻塞 I/O + 多线程读取,窗口立马大了,吞吐量翻倍。

3.5 核心流程全景图

下面这张图把 TCP 的核心流程串起来了。我每次给团队培训都会画一遍,因为理解了这张图,TCP 就算入门了。

TCP 核心流程全景图 三次握手 客户端 服务器 ① SYN=1, SEQ=x ② SYN=1, ACK=1, SEQ=y, ACK_NUM=x+1 ③ ACK=1, SEQ=x+1, ACK_NUM=y+1 数据传输(滑动窗口) 发送窗口(cwnd + rwnd) 接收窗口(rwnd) 数据段 + ACK 窗口通告(rwnd 更新) 四次挥手 主动关闭方 被动关闭方 ① FIN=1, SEQ=u ② ACK=1, SEQ=v, ACK_NUM=u+1 ③ FIN=1, SEQ=w, ACK_NUM=u+1 ④ ACK=1, SEQ=u+1, ACK_NUM=w+1 状态流转:CLOSED → SYN_SENT → ESTABLISHED → FIN_WAIT_1 → CLOSE_WAIT → TIME_WAIT → CLOSED

我的经验:排查 TCP 问题,我一般三步走:先看三次握手是否完成,再看数据传输窗口是否正常,最后看四次挥手有没有异常。80% 的问题都能在这三步里找到线索。

TCP 的可靠不是免费的。每个 ACK 都要消耗带宽,每个重传都要增加延迟。但正是这些机制,让 TCP 成为互联网上最值得信赖的传输协议。你想想看,如果没有 TCP,我们下载文件、浏览网页、发邮件,随时都可能丢数据——那画面太美我不敢看。