TCP 传输层硬核整理:三次握手、四次挥手、拥塞控制一次讲透

文章目录

前言

这篇文章把 TCP 传输层讲透:三次握手、四次挥手、可靠传输、粘包、拥塞控制,全是面试和线上排查的高频问题。读完你能把 TCP 的完整生命周期串起来,也能说清 TIME_WAIT、CLOSE_WAIT、SYN 攻击这些坑到底是怎么回事。


一、TCP 首部:先认识这几个关键字段

序列号

建立连接时由计算机生成一个随机数作为初始值,通过 SYN 包传给对端。每发送一次数据,序列号就累加一次"数据字节数"。作用:解决网络包乱序。

确认应答号

指下一次期望收到的序列号。发送端收到这个确认,就认为这个序号以前的数据都被正常接收。作用:解决丢包。

控制位

  • ACK:为 1 时确认应答字段生效。TCP 规定,除了最初建立连接的 SYN 包,其余包必须置 1。
  • RST:为 1 表示连接出现异常,必须强制断开。
  • SYN:为 1 表示希望建立连接,同时序列号字段写入初始值。
  • FIN:为 1 表示今后不再发送数据,希望断开连接。

二、三次握手:建立连接的流程

TCP 是面向连接的协议,传输前必须先建立连接,靠三次握手完成。三次握手的过程如下图:

复制代码
客户端                          服务端
CLOSED                         LISTEN
  │   ── SYN(seq=x) ────────►   │   客户端进入 SYN_SENT
  │   ◄─ SYN+ACK(seq=y,ack=x+1) │   服务端进入 SYN_RCVD
  │   ── ACK(ack=y+1) ────────► │   客户端进入 ESTABLISHED
  │                             │   服务端进入 ESTABLISHED
  • 初始状态:客户端 CLOSED,服务端主动监听端口,处于 LISTEN。
  • 第一次握手:客户端随机初始化序列号,置 SYN=1,发 SYN 包(不带数据),进入 SYN_SENT。
  • 第二次握手:服务端随机初始化自己的序列号,确认应答号填 client_isn + 1,置 SYN+ACK,回包(不带数据),进入 SYN_RCVD。
  • 第三次握手:客户端回 ACK,确认应答号填 server_isn + 1,进入 ESTABLISHED。服务端收到后也进入 ESTABLISHED。

第三次握手可以携带数据,前两次不可以。 这是面试常考题。


三、为什么必须是三次握手

三次握手的原因,按重要性排:

  • 阻止重复的历史连接初始化(主要原因
  • 同步双方的初始序列号
  • 避免资源浪费

原因一:阻止历史连接

RFC 793 原话:三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。

场景:客户端先发了 SYN(seq=90),报文被网络阻塞,服务端没收到。客户端重启后重新建连,发 SYN(seq=100)

  • 旧 SYN 先到,服务端回 SYN+ACK(ack=91)
  • 客户端发现期望的确认号是 100+1,不是 90+1,判断这是历史连接,立刻回 RST。
  • 服务端收到 RST 释放连接,最新 SYN 到了再正常握手。

两次握手为什么不行? 服务端收到 SYN 就进入 ESTABLISHED,没有中间状态阻止历史连接。它可能在不知道的情况下建立一个历史连接,还白白发了数据,浪费资源。

原因二:同步双方初始序列号

序列号是可靠传输的关键:接收方靠它去重、按序接收、确认收到哪些数据。所以客户端发的初始序列号要服务端确认,服务端发的也要客户端确认。一来一回正好两次。第二步和第三步能优化成一步,于是三次握手就能可靠同步双方的初始序列号。

原因三:避免资源浪费

只有两次握手时,客户端 SYN 在网络中阻塞,没收到 ACK 就重发。服务端每收到一个 SYN 只能建一个连接,最终建立一堆冗余无效连接,白白占用资源。


四、握手中的丢包:三种情况分别怎么办

第一次握手 SYN 丢了

客户端迟迟收不到 SYN+ACK,触发超时重传,重传 SYN(序列号相同)。次数由 tcp_syn_retries 控制,默认 5。超时时间 1、2、4、8、16 秒(每次翻倍),最后一次后再等 32 秒,仍没回应就断开连接。总耗时约 63 秒。

第二次握手 SYN+ACK 丢了

有意思的来了。客户端觉得自己的 SYN 丢了,重传 SYN;服务端收不到第三次 ACK,也重传 SYN+ACK。两边同时重传

  • 客户端重传 SYN,次数由 tcp_syn_retries 控制。
  • 服务端重传 SYN+ACK,次数由 tcp_synack_retries 控制(默认 5)。

第三次握手 ACK 丢了

客户端收到 SYN+ACK 后已进入 ESTABLISHED。服务端迟迟收不到确认,触发超时重传 SYN+ACK,直到收到或达到最大重传次数后断开连接。

注意:ACK 报文本身不会重传。ACK 丢了,由对方重传对应的报文(SYN+ACK 或数据)。


五、半连接队列、全连接队列、SYN 攻击

服务端收到 SYN 后,内核把连接放进半连接队列 ,回 SYN+ACK。收到第三次握手的 ACK 后,把连接从半连接队列移到全连接队列 (accept 队列)。应用调 accept(),就是从全连接队列取连接。

复制代码
SYN  ──►  半连接队列   ── 收到 ACK  ──►  全连接队列  ── accept() ──►  应用程序

两个队列都有长度上限,满了就丢弃,或回 RST。

大量 SYN 进来会发生什么

半连接队列被打满,后续 SYN 被丢弃,正常客户端也建不了连接------这就是 SYN 攻击。

防 SYN 攻击的四种方式:

  1. 调大 netdev_max_backlog:网卡收包速度大于内核处理速度时,数据包先排队,控制这个队列的上限(默认 1000,可调到 10000)。
  2. 增大半连接队列 :同时调大 tcp_max_syn_backloglisten() 的 backlog、net.core.somaxconn 三个参数。
  3. 开启 tcp_syncookies :半连接队列满时不再丢包,根据算法算出一个 cookie,放进 SYN+ACK 的序列号。收到客户端 ACK 时校验合法性,合法就直接把连接放进 accept 队列。设置 tcp_syncookies=1(仅当队列放不下时启用)即可绕过 SYN 半连接建立连接。
  4. 调小 tcp_synack_retries:让处于 SYN_RCVD 状态的连接快速断开,减少半连接占用。

六、四次挥手:断开连接的流程

四次挥手的过程:

复制代码
主动方                             被动方
  │  ── FIN ────────────────►       │  主动方进入 FIN_WAIT_1
  │  ◄── ACK ────────────────       │  被动方进入 CLOSE_WAIT
  │                                │  被动方应用读完数据后 close
  │  ◄── FIN ────────────────       │  被动方进入 LAST_ACK
  │  ── ACK ────────────────►       │  主动方进入 TIME_WAIT
  │                                │  被动方收到 ACK 进入 CLOSE
  │  2MSL 后进入 CLOSE
  • 主动方调 close,发 FIN(代表不再发数据),进入 FIN_WAIT_1。
  • 被动方收到 FIN,立刻回 ACK,进入 CLOSE_WAIT。协议栈会给 FIN 包插一个 EOF 到接收缓冲区末尾,应用通过 read 感知,读到 EOF 时 read() 返回 0。
  • 被动方应用读完后,有数据就先发完,再调 close 发 FIN,进入 LAST_ACK。
  • 主动方收到 FIN,回 ACK,进入 TIME_WAIT。
  • 被动方收到 ACK,进入 CLOSE。主动方等 2MSL 后也进入 CLOSE。

为什么中间两次不能合并

被动方收到 FIN 时,内核马上回 ACK,但应用可能还有数据要发,FIN 的控制权在应用层。由应用决定什么时候调 close,所以 ACK 和 FIN 一般分开发。

特殊情况:三次挥手

如果被动方没有数据要发 ,且开启了延迟确认机制,第二次和第三次挥手会合并成一个报文,挥手就从四次变三次。


七、第三次挥手一直没发、2MSL、TIME_WAIT

第三次挥手一直没发会发生什么

主动方收到 ACK 后停在 FIN_WAIT_2,等对方发 FIN。

  • shutdown 关闭的连接:可以一直停在 FIN_WAIT_2,因为它可能还要收发数据。
  • close 关闭的孤儿连接:不能收发数据,不能持续太久。tcp_fin_timeout 控制这个状态的时长,默认 60 秒,超时没收到 FIN 就强制关闭。

被动方则一直停在 CLOSE_WAIT------这个状态没有超时,应用不 close,连接就永远挂着。大量 CLOSE_WAIT 堆积 = 代码漏 close,是 bug 不是参数问题。

为什么四次挥手后要等 2MSL

MSL 是报文最大生存时间。IP 头里有个 TTL 字段,是数据报能经过的最大路由数,每过一个路由器减 1,减到 0 就丢弃。Linux 认为报文经过 64 个路由器不会超过 30 秒,所以 Linux 的 MSL = 30 秒

等 2 倍 MSL,是因为网络中可能同时存在双向的报文,一来一回正好 2 个 MSL。更重要的是:2MSL 相当于允许最后一个 ACK 丢一次。如果主动方最后的 ACK 丢了,被动方会重发 FIN,这个 FIN 在第二个 MSL 内能到达,TIME_WAIT 状态的连接还能应对。

服务端出现大量 TIME_WAIT 的原因

核心一句:谁主动 close,谁进 TIME_WAIT。服务端大量 TIME_WAIT = 服务端在频繁主动关连接。三种常见场景:

场景一:HTTP 没用长连接。 HTTP/1.0 默认关 keep-alive,HTTP/1.1 默认开。只要请求或响应 header 里有 Connection: close,长连接就失效,变成短连接。每次请求都走"建连→请求→响应→断连",而多数 Web 服务是服务端主动关闭,于是大量 TIME_WAIT。

场景二:HTTP 长连接超时。 Web 服务一般有 keep-alive 超时参数,比如 nginx 的 keepalive_timeout。客户端完成请求后 60 秒没发新请求,定时器到点,服务端主动关。现象往往是:大量连接建立后长时间没数据。

场景三:HTTP 长连接请求数达上限。 nginx 的 keepalive_requests 默认 100,即一条长连接最多跑 100 个请求。QPS 到 10000 甚至更高时,很快就关一批连接,产生大量 TIME_WAIT。解法:调大 keepalive_requests。


八、TCP vs UDP

维度 TCP UDP
连接 面向连接,传输前先建连 无连接,即刻传输
服务对象 一对一 一对一、一对多、多对多
可靠性 可靠交付,不丢不重不乱序 不可靠,丢了就丢了
拥塞/流量控制 没有
首部开销 20 字节起(有选项字段会更长) 固定 8 字节
传输方式 流式,无边界,保序 报文式,有边界,可能丢包乱序

九、TCP 为什么可靠

靠六招:连接管理、序列号、确认应答、超时重传、流量控制、拥塞控制

  • 连接管理:三次握手 + 四次挥手,可靠传输的前提。
  • 序列号:给每个字节编号,防止丢失、避免重复、保证有序,还能实现"多次发送、一次确认"。
  • 确认应答:接收方回 ACK 告知收到情况。
  • 超时重传:数据包丢了重发;确认包丢了,接收方靠序列号识别重复数据并丢弃,重发 ACK。
  • 流量控制:接收方处理能力有限,通过滑动窗口限制发送速度,防止缓冲区溢出。
  • 拥塞控制:网络拥堵时减少发送,通过拥塞窗口实现。

发送方的发送速度 = min(滑动窗口, 拥塞窗口)。


十、粘包问题怎么解

粘包的本质:不知道一条用户消息的边界在哪。知道了边界,接收方就能划出有效消息。一般三种分界方式:

  1. 固定长度消息:每条消息固定 N 字节,接满 N 字节就算一条。最简单,但灵活性差,实际很少用。
  2. 特殊字符作边界:两个消息之间插特殊字符串,读到它就认为读完一条。HTTP 就是例子,用回车换行作边界。注意:消息内容里出现这个字符要转义。
  3. 自定义消息结构:包头 + 数据,包头固定大小且带数据长度字段。收到包头就解析出数据长度,读满即组装成一条完整消息。
c 复制代码
struct message {
    u_int32_t message_length;  // 4 字节,表示后面数据的长度
    char message_data[];       // 真正的数据
};

十一、拥塞控制:四件套

TCP 被设计成"无私"的协议,网络拥堵时自我牺牲、降低发送量,避免把网络填满。核心变量是拥塞窗口 cwnd:网络没拥塞就增大,出现拥塞就减小。判断依据:发送方没在限定时间内收到 ACK,即超时重传,就认为拥塞了。

四种算法按顺序:

1. 慢启动

刚建连时一点点提高发送量。规则:每收到一个 ACK,cwnd + 1。初始 cwnd=1,发 1 个 MSS;收到 1 个 ACK 变 2,收到 2 个 ACK 变 4......发包数指数增长。

涨到哪停?看慢启动门限 ssthresh

  • cwnd < ssthresh:慢启动(指数增长)
  • cwnd >= ssthresh:拥塞避免(线性增长)

2. 拥塞避免

规则:每收到一个 ACK,cwnd 加 1/cwnd。从指数增长变成线性增长,增长放缓。

3. 拥塞发生

触发重传就进入拥塞发生。两种重传处理不同:

  • 超时重传 :说明网络很糟。ssthresh = cwnd/2cwnd 重置为 1,回到慢启动重新开始。一夜回到解放前,反应很激烈。
  • 快速重传 :接收方发现丢中间包时,连发三次前一个包的 ACK,发送端快速重传,不必等超时。TCP 认为情况不严重:cwnd = cwnd/2ssthresh = cwnd,进入快速恢复。

4. 快速恢复

进入前 cwnd 和 ssthresh 已更新。规则:

  1. cwnd = ssthresh + 3(3 表示确认收到了 3 个重复 ACK)。
  2. 重传丢失的数据包。
  3. 再收到重复 ACK,cwnd + 1。
  4. 收到新数据的 ACK,把 cwnd 设为 ssthresh,进入拥塞避免。

比超时重传温和得多,不会从高水位直接打回原形,后续再线性增长。


收尾

TCP 的知识点其实是一张网:三次握手 → 可靠传输 → 流量/拥塞控制 → 四次挥手 → TIME_WAIT,一环扣一环。把连接的生命周期串起来,面试和排障都能快人一步。

相关推荐
AndrewHZ1 小时前
图像处理入门009 | OpenCV 图像读取与显示:imread/imshow 全解析
图像处理·python·opencv·算法·计算机视觉·图像显示
xx~t1 小时前
嵌入式学习22
数据结构·学习·算法·排序算法
海鸥两三1 小时前
【websocket合集2】websocket.js源码解析和页面调用示例
javascript·websocket·网络协议
我是苏苏1 小时前
C#基础:不写for循环的五种方式
数据结构·算法·c#
砚凝霜1 小时前
软考网络工程师|案例分析:华为全套综合实验核心知识点 & 考点总结
网络·华为
都叫我大帅哥1 小时前
@EnableTransactionManagement 详解:Spring 声明式事务的基石
java·spring
三8442 小时前
webshell缓存绕过/哈希碰撞
算法·哈希算法
Wang's Blog2 小时前
PostgreSQL笔记20: 实例、集群、模板数据库与表空间 —— 逻辑体系架构深度解析
数据库·笔记·postgresql
2602_959960922 小时前
大厂Java面试:Spring Boot、JVM、Redis、Kafka与Elasticsearch在内容社区UGC场景下的实战拷问
java·jvm·spring boot·redis·面试题