TCP 与连接管理

TCP 为什么可靠

TCP 通过以下机制提供可靠、有序的字节流:

  • 序列号和确认应答。
  • 超时重传与快速重传。
  • 滑动窗口流量控制。
  • 拥塞窗口和拥塞控制。
  • 校验和与有序重组。

“可靠”不等于“请求一定成功”。连接可能超时、中断,应用层仍然需要重试、幂等和错误处理。

三次握手

TCP 是面向连接的协议。这里的“连接”不是一根真实存在的线,而是客户端和服务端内核中共同维护的一组状态,包括四元组、序列号、确认号、窗口大小、重传定时器、拥塞控制状态等。

三次握手的核心目的有三个:

  • 确认双方的发送能力和接收能力都正常。
  • 同步双方的初始序列号,也就是 ISN(Initial Sequence Number)。
  • 避免历史失效连接或旧报文干扰新连接。

典型过程如下:

客户端                                             服务端
CLOSED                                             LISTEN

SYN(seq = x)                         ────────────>
SYN_SENT                                           SYN_RECEIVED

                                     <──────────── SYN(seq = y), ACK(ack = x + 1)
ESTABLISHED

ACK(ack = y + 1)                     ────────────>
                                                    ESTABLISHED

第一次握手:客户端发送 SYN

客户端主动打开连接,向服务端发送:

SYN = 1, seq = x

其中 x 是客户端选择的初始序列号。发送后,客户端进入 SYN_SENT 状态。

SYN 是 TCP 报文头里的控制标志位,全称是 Synchronize,表示“我要建立连接,并同步初始序列号”。它本身不是序列号,而是告诉对方:这个报文里的 seq 字段有特殊含义,用来声明本端的初始序列号。

seq 是 Sequence Number,也就是序列号字段。它是一个数字,表示这个 TCP 报文段从哪个序列号开始。第一次握手里的 seq = x,意思就是客户端告诉服务端:“我的初始序列号是 x。”

这一步表达两层意思:

  • 客户端希望建立连接。
  • 客户端告诉服务端:“我后续发送的数据会从序列号 x + 1 开始计算。”

SYN 报文虽然通常不携带应用层数据,但它会占用一个序列号,所以服务端后续确认号要写成 x + 1

第二次握手:服务端回复 SYN + ACK

服务端处于 LISTEN 状态,收到客户端的 SYN 后,如果愿意建立连接,会回复:

SYN = 1, ACK = 1, seq = y, ack = x + 1

其中:

  • ack = x + 1 表示服务端已经收到客户端的 SYN
  • seq = y 是服务端选择的初始序列号。

这里的 SYN 不是服务端“重复”客户端的 SYN,而是服务端也要同步自己的初始序列号。也就是说,ACK = 1, ack = x + 1 表示“我收到了你的 SYN”;SYN = 1, seq = y 表示“这是我的初始序列号”。

发送后,服务端进入 SYN_RECEIVED 状态。

这一步说明服务端至少确认了三件事:

  • 客户端发送能力正常,因为服务端收到了客户端的 SYN
  • 服务端接收能力正常,因为它能接收客户端的报文。
  • 服务端发送能力正在接受客户端验证,因为服务端已经把自己的 SYN 发出去了。

第三次握手:客户端回复 ACK

客户端收到服务端的 SYN + ACK 后,回复:

ACK = 1, ack = y + 1

发送后,客户端进入 ESTABLISHED 状态。ESTABLISHED 表示连接已经建立完成,可以正式传输数据。服务端收到这个 ACK 后,也进入 ESTABLISHED 状态。

客户端进入 ESTABLISHED,说明客户端已经确认自己收到了服务端的初始序列号;服务端进入 ESTABLISHED,说明服务端也确认客户端收到了自己的初始序列号。双方都进入这个状态后,TCP 连接才算真正建立好了。

这一步让服务端确认:

  • 服务端发送能力正常,因为客户端收到了服务端的 SYN + ACK
  • 客户端接收能力正常,因为客户端能收到服务端报文。
  • 双方都已经知道对方的初始序列号,后续可以用序列号和确认号保证可靠、有序传输。

为什么需要同步初始序列号

TCP 是面向字节流的协议,后续每个字节都会落在序列号空间里。接收方通过确认号告诉发送方:“我已经收到了哪里,下一次希望从哪里继续发。”

如果没有初始序列号同步,双方就无法判断:

  • 哪些数据是新的;
  • 哪些数据已经收到;
  • 哪些数据需要重传;
  • 网络中延迟到达的旧报文是否应该丢弃。

初始序列号不是简单从 0 开始。它通常会随时间变化并带有一定随机性,这样可以降低旧连接报文被新连接误认为有效数据的概率,也能提高被猜测攻击的难度。

为什么不是两次握手

如果只有两次握手:

客户端                      服务端
SYN(seq = x)       ───────>
SYN(seq = y), ACK  <───────

客户端收到第二次握手后知道自己的发送能力、接收能力和服务端的发送能力、接收能力都没问题。但服务端还不知道客户端是否收到了自己的 SYN,也就无法确认客户端的接收能力和服务端的发送能力是否正常。

更重要的是,两次握手更容易受到历史失效连接影响。比如客户端很久以前发送的一个 SYN 因网络原因滞留,连接早已超时失效;后来这个旧 SYN 到达服务端。如果服务端只要收到 SYN 并回复 SYN + ACK 就直接认为连接建立,那么它可能为一个客户端并不需要的旧连接分配资源。

三次握手中,服务端必须等到客户端对自己 SYN 的最终 ACK,才认为连接真正建立。这样可以避免服务端过早进入已连接状态,减少历史报文造成的资源浪费。

为什么不是四次握手

建立连接时,服务端收到客户端的 SYN 后,需要做两件事:

  • 确认客户端的 SYN,也就是发送 ACK。
  • 发送自己的 SYN,把服务端的初始序列号告诉客户端。

这两件事没有先后等待关系,可以合并在同一个报文里发送,所以第二次握手是 SYN + ACK。因此三次已经足够完成双方能力确认和初始序列号同步。

关闭连接通常需要四次,是因为 TCP 是全双工的。收到对方 FIN 只能说明对方不再发送数据,本端可能还有数据没有发送完,所以 ACK 和 FIN 常常不能立即合并。

三次握手中的队列

从服务端角度看,握手过程中通常会涉及两个队列:

  • 半连接队列:服务端收到 SYN 并回复 SYN + ACK 后,连接处于 SYN_RECEIVED,还没有收到第三次 ACK。
  • 全连接队列:服务端收到第三次 ACK 后,连接进入 ESTABLISHED,等待应用程序通过 accept() 取走。

如果半连接队列满,新的 SYN 可能被丢弃或触发 SYN Cookie 等保护机制。如果全连接队列满,即使握手已经基本完成,应用层也可能来不及接受连接,客户端表现为连接建立慢、超时或被重置。

第三次 ACK 丢失会怎样

第三次握手的 ACK 如果丢失,客户端通常已经进入 ESTABLISHED,但服务端还停留在 SYN_RECEIVED。服务端没有收到最终确认,会重传 SYN + ACK

客户端收到重传的 SYN + ACK 后,会再次发送 ACK。只要服务端最终收到 ACK,就可以进入 ESTABLISHED

如果客户端在第三次 ACK 后马上发送应用层数据,这个数据包通常也会带 ACK 信息。服务端收到后,同样可以确认握手完成。

三次握手可以携带数据吗

普通 TCP 握手里,前两次握手通常不携带应用层数据。第三次 ACK 从协议语义上可以携带数据,因为此时客户端已经确认服务端的初始序列号;但具体是否发送、是否被应用使用,还要看操作系统和应用协议行为。

需要和 TCP Fast Open 区分开。TCP Fast Open 允许客户端在 SYN 阶段携带数据,以减少一次往返延迟,但它需要额外机制支持,并不是普通三次握手的默认行为。

面试回答可以这样说

TCP 三次握手用于建立连接、确认双方收发能力,并同步双方的初始序列号。第一次客户端发送 SYN,告诉服务端自己的初始序列号;第二次服务端回复 SYN + ACK,确认客户端 SYN,同时告诉客户端自己的初始序列号;第三次客户端回复 ACK,确认收到了服务端 SYN。不能只有两次,是因为服务端无法确认客户端是否收到了自己的 SYN,也更容易让历史失效 SYN 造成资源浪费。三次已经足够,因为服务端的 SYN 和 ACK 可以合并发送。

四次挥手与 TIME_WAIT

TCP 是全双工协议,客户端到服务端、服务端到客户端是两条相对独立的数据通道。一个方向发送 FIN,只表示发送方不再发送数据,但仍然可以接收另一个方向的数据。因此,两个方向通常需要分别关闭,形成四次挥手。

下面假设客户端主动关闭连接:

客户端(主动关闭方)                         服务端(被动关闭方)
ESTABLISHED                                ESTABLISHED

FIN(seq = u)                 ────────────>
FIN_WAIT_1                                  CLOSE_WAIT

                             <──────────── ACK(ack = u + 1)
FIN_WAIT_2

                             <──────────── FIN(seq = v)
TIME_WAIT                                  LAST_ACK

ACK(ack = v + 1)             ────────────>
等待 2MSL                                   CLOSED

CLOSED

第一次挥手:主动关闭方发送 FIN

客户端调用 close() 或关闭写方向,发送:

FIN = 1, seq = u

客户端进入 FIN_WAIT_1 状态。FIN 会占用一个序列号,因此服务端应答的确认号是 u + 1

需要注意,发送 FIN 只表示客户端后续不会再发送数据。此时客户端仍然可以接收服务端尚未发送完的数据,这种状态称为半关闭(half-close)

第二次挥手:被动关闭方确认 FIN

服务端收到 FIN 后返回:

ACK = 1, ack = u + 1

服务端进入 CLOSE_WAIT,客户端收到 ACK 后进入 FIN_WAIT_2

此时客户端到服务端的方向已经关闭,但服务端到客户端的方向仍然可以继续传输数据。服务端需要等待应用程序处理完剩余数据并主动关闭连接,所以这个 ACK 和服务端自己的 FIN 通常不能立即合并。

第三次挥手:被动关闭方发送 FIN

服务端应用程序完成剩余数据发送并关闭连接后,服务端发送:

FIN = 1, seq = v

服务端进入 LAST_ACK,等待客户端对该 FIN 的最终确认。

实际报文中 FIN 通常也会带有 ACK 标志。这里单独强调 FIN,是为了表达服务端关闭自己的发送方向。

第四次挥手:主动关闭方确认 FIN

客户端收到服务端的 FIN 后返回:

ACK = 1, ack = v + 1

客户端进入 TIME_WAIT。服务端收到 ACK 后进入 CLOSED;客户端等待 2MSL 后才进入 CLOSED

为什么通常需要四次挥手

建立连接时,服务端收到 SYN 后可以同时发送 SYN 和 ACK,所以三次握手就能完成。

关闭连接时,服务端收到 FIN 只能说明客户端不再发送数据,服务端可能还有:

  • 已经接收但尚未处理的请求;
  • 尚未发送完的响应;
  • 应用程序还没有调用关闭操作。

因此服务端通常先发送 ACK,等应用程序完成处理后再发送 FIN,ACK 和 FIN 就分成了两次。

“四次挥手”是典型过程,不是任何情况下都必须出现四个独立报文。如果被动关闭方收到 FIN 时也已经没有数据需要发送,ACK 和 FIN 可以合并为一个报文,从抓包结果看可能只有三次。

为什么主动关闭方要等待 2MSL

MSL(Maximum Segment Lifetime)表示一个 TCP 报文在网络中的最大生存时间。主动关闭方等待 2MSL 主要有两个原因:

  1. 保证连接可靠关闭
    • 如果第四次挥手的 ACK 丢失,服务端会超时重传 FIN。
    • 客户端仍处于 TIME_WAIT,可以再次发送 ACK。
    • 如果客户端直接进入 CLOSED,服务端可能一直重传 FIN,或者收到新连接返回的 RST。
  2. 让旧连接的重复报文从网络中消失
    • TCP 连接由源 IP、源端口、目标 IP、目标端口四元组标识。
    • 等待足够长的时间,可以避免旧连接中延迟到达的报文被使用相同四元组的新连接误收。

之所以是 2MSL,可以理解为允许一个报文和它的应答各自在网络中最多存活一个 MSL。不同操作系统对 MSL 和 TIME_WAIT 时长的具体取值可能不同。

TIME_WAIT 过多意味着什么

TIME_WAIT 出现在主动关闭连接的一方。短连接、高并发且由本机主动关闭时,可能出现大量 TIME_WAIT,并占用临时端口和内核连接表。

排查和优化时应优先考虑:

  • 使用 HTTP 持久连接或连接池,减少频繁建立和关闭 TCP 连接;
  • 确认连接是否应该由本端主动关闭;
  • 合理设置服务端和客户端超时,避免无意义的短连接;
  • 扩大可用临时端口范围,并评估内核参数调整的影响。

不能简单地把 TIME_WAIT 当作连接泄漏并强行缩短。它是 TCP 正确性设计的一部分,绕过它可能增加旧报文污染新连接的风险。

CLOSE_WAIT 过多意味着什么

CLOSE_WAIT 出现在被动关闭连接的一方,表示已经收到对端的 FIN,也已经回复 ACK,但本地应用程序迟迟没有关闭自己的连接。

大量连接长期停留在 CLOSE_WAIT,通常说明应用层存在问题,例如:

  • 异常分支没有关闭 socket、响应体或流;
  • 连接由多个对象持有,资源没有及时释放;
  • 程序阻塞或死锁,无法执行关闭逻辑。

因此:

  • TIME_WAIT 多,常见原因是本端主动关闭了大量短连接;
  • CLOSE_WAIT 多,通常要检查应用程序是否遗漏了资源关闭。

同时关闭会发生什么

如果双方几乎同时发送 FIN,双方都可能从 FIN_WAIT_1 进入 CLOSING,收到对方对自己 FIN 的 ACK 后再进入 TIME_WAIT。这属于合法的 TCP 状态转换,只是没有典型的四次挥手常见。

close 和 shutdown 的区别

  • close() 释放 socket。若这是该 socket 的最后一个引用,内核会开始关闭连接。
  • shutdown() 可以只关闭读方向或写方向。例如关闭写方向后发送 FIN,但仍可以继续读取对端数据,更适合显式控制半关闭。

长连接、短连接与连接复用

  • HTTP/1.0 默认更接近短连接。
  • HTTP/1.1 默认使用持久连接,但并发请求常需要多个 TCP 连接。
  • HTTP/2 在一个连接上多路复用多个流。
  • HTTP/3 在 QUIC 连接内为不同流提供相对独立的可靠传输,不再直接使用 TCP。

队头阻塞

  • HTTP/1.1 管线化:前一个响应阻塞后一个响应。
  • HTTP/2:应用层流可并发,但 TCP 丢包会阻塞整个连接的数据交付。
  • HTTP/3:QUIC 的不同流不会因某个流丢包而全部等待。

TCP 粘包与拆包

TCP 是面向字节流的协议,不保留应用层消息边界。发送方多次发送的数据,接收方可能一次读取到;一次发送的数据,也可能分多次读取,这就是常说的粘包和拆包。

解决方式由应用层定义消息边界,例如:

  • 固定长度消息。
  • 在消息前增加长度字段。
  • 使用特殊分隔符。
  • 使用具备明确边界的应用层协议,如 HTTP 的请求头和消息体。

HTTP 通常不需要业务代码自己处理 TCP 粘包,因为 HTTP 解析器会根据 Content-LengthTransfer-Encoding 或协议帧格式还原消息边界。

高频追问

  • TCP 为什么需要三次握手而不是两次?
  • TCP 为什么挥手通常需要四次?
  • 为什么 FIN 会占用一个序列号?
  • 为什么主动关闭方需要等待 2MSL?
  • TIME_WAIT 和 CLOSE_WAIT 过多分别说明什么?
  • 四次挥手一定是四个报文吗?
  • TCP 半关闭是什么?close()shutdown() 有什么区别?
  • HTTP keep-alive 与 TCP keepalive 是一回事吗?
  • HTTP/2 为什么仍然存在 TCP 队头阻塞?