TCP 与连接管理
TCP 为什么可靠
TCP 通过以下机制提供可靠、有序的字节流:
- 序列号和确认应答。
- 超时重传与快速重传。
- 滑动窗口流量控制。
- 拥塞窗口和拥塞控制。
- 校验和与有序重组。
“可靠”不等于“请求一定成功”。连接可能超时、中断,应用层仍然需要重试、幂等和错误处理。
三次握手
TCP 是面向连接的协议。这里的“连接”不是一根真实存在的线,而是客户端和服务端内核中共同维护的一组状态,包括四元组、序列号、确认号、窗口大小、重传定时器、拥塞控制状态等。
三次握手的核心目的有三个:
- 确认双方的发送能力和接收能力都正常。
- 同步双方的初始序列号,也就是
ISN(Initial Sequence Number)。 - 避免历史失效连接或旧报文干扰新连接。
典型过程如下:
第一次握手:客户端发送 SYN
客户端主动打开连接,向服务端发送:
其中 x 是客户端选择的初始序列号。发送后,客户端进入 SYN_SENT 状态。
SYN 是 TCP 报文头里的控制标志位,全称是 Synchronize,表示“我要建立连接,并同步初始序列号”。它本身不是序列号,而是告诉对方:这个报文里的 seq 字段有特殊含义,用来声明本端的初始序列号。
seq 是 Sequence Number,也就是序列号字段。它是一个数字,表示这个 TCP 报文段从哪个序列号开始。第一次握手里的 seq = x,意思就是客户端告诉服务端:“我的初始序列号是 x。”
这一步表达两层意思:
- 客户端希望建立连接。
- 客户端告诉服务端:“我后续发送的数据会从序列号
x + 1开始计算。”
SYN 报文虽然通常不携带应用层数据,但它会占用一个序列号,所以服务端后续确认号要写成 x + 1。
第二次握手:服务端回复 SYN + ACK
服务端处于 LISTEN 状态,收到客户端的 SYN 后,如果愿意建立连接,会回复:
其中:
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 后,回复:
发送后,客户端进入 ESTABLISHED 状态。ESTABLISHED 表示连接已经建立完成,可以正式传输数据。服务端收到这个 ACK 后,也进入 ESTABLISHED 状态。
客户端进入 ESTABLISHED,说明客户端已经确认自己收到了服务端的初始序列号;服务端进入 ESTABLISHED,说明服务端也确认客户端收到了自己的初始序列号。双方都进入这个状态后,TCP 连接才算真正建立好了。
这一步让服务端确认:
- 服务端发送能力正常,因为客户端收到了服务端的
SYN + ACK。 - 客户端接收能力正常,因为客户端能收到服务端报文。
- 双方都已经知道对方的初始序列号,后续可以用序列号和确认号保证可靠、有序传输。
为什么需要同步初始序列号
TCP 是面向字节流的协议,后续每个字节都会落在序列号空间里。接收方通过确认号告诉发送方:“我已经收到了哪里,下一次希望从哪里继续发。”
如果没有初始序列号同步,双方就无法判断:
- 哪些数据是新的;
- 哪些数据已经收到;
- 哪些数据需要重传;
- 网络中延迟到达的旧报文是否应该丢弃。
初始序列号不是简单从 0 开始。它通常会随时间变化并带有一定随机性,这样可以降低旧连接报文被新连接误认为有效数据的概率,也能提高被猜测攻击的难度。
为什么不是两次握手
如果只有两次握手:
客户端收到第二次握手后知道自己的发送能力、接收能力和服务端的发送能力、接收能力都没问题。但服务端还不知道客户端是否收到了自己的 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,只表示发送方不再发送数据,但仍然可以接收另一个方向的数据。因此,两个方向通常需要分别关闭,形成四次挥手。
下面假设客户端主动关闭连接:
第一次挥手:主动关闭方发送 FIN
客户端调用 close() 或关闭写方向,发送:
客户端进入 FIN_WAIT_1 状态。FIN 会占用一个序列号,因此服务端应答的确认号是 u + 1。
需要注意,发送 FIN 只表示客户端后续不会再发送数据。此时客户端仍然可以接收服务端尚未发送完的数据,这种状态称为半关闭(half-close)。
第二次挥手:被动关闭方确认 FIN
服务端收到 FIN 后返回:
服务端进入 CLOSE_WAIT,客户端收到 ACK 后进入 FIN_WAIT_2。
此时客户端到服务端的方向已经关闭,但服务端到客户端的方向仍然可以继续传输数据。服务端需要等待应用程序处理完剩余数据并主动关闭连接,所以这个 ACK 和服务端自己的 FIN 通常不能立即合并。
第三次挥手:被动关闭方发送 FIN
服务端应用程序完成剩余数据发送并关闭连接后,服务端发送:
服务端进入 LAST_ACK,等待客户端对该 FIN 的最终确认。
实际报文中 FIN 通常也会带有 ACK 标志。这里单独强调 FIN,是为了表达服务端关闭自己的发送方向。
第四次挥手:主动关闭方确认 FIN
客户端收到服务端的 FIN 后返回:
客户端进入 TIME_WAIT。服务端收到 ACK 后进入 CLOSED;客户端等待 2MSL 后才进入 CLOSED。
为什么通常需要四次挥手
建立连接时,服务端收到 SYN 后可以同时发送 SYN 和 ACK,所以三次握手就能完成。
关闭连接时,服务端收到 FIN 只能说明客户端不再发送数据,服务端可能还有:
- 已经接收但尚未处理的请求;
- 尚未发送完的响应;
- 应用程序还没有调用关闭操作。
因此服务端通常先发送 ACK,等应用程序完成处理后再发送 FIN,ACK 和 FIN 就分成了两次。
“四次挥手”是典型过程,不是任何情况下都必须出现四个独立报文。如果被动关闭方收到 FIN 时也已经没有数据需要发送,ACK 和 FIN 可以合并为一个报文,从抓包结果看可能只有三次。
为什么主动关闭方要等待 2MSL
MSL(Maximum Segment Lifetime)表示一个 TCP 报文在网络中的最大生存时间。主动关闭方等待 2MSL 主要有两个原因:
- 保证连接可靠关闭
- 如果第四次挥手的 ACK 丢失,服务端会超时重传 FIN。
- 客户端仍处于
TIME_WAIT,可以再次发送 ACK。 - 如果客户端直接进入
CLOSED,服务端可能一直重传 FIN,或者收到新连接返回的 RST。
- 让旧连接的重复报文从网络中消失
- 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-Length、Transfer-Encoding 或协议帧格式还原消息边界。
高频追问
- TCP 为什么需要三次握手而不是两次?
- TCP 为什么挥手通常需要四次?
- 为什么 FIN 会占用一个序列号?
- 为什么主动关闭方需要等待 2MSL?
- TIME_WAIT 和 CLOSE_WAIT 过多分别说明什么?
- 四次挥手一定是四个报文吗?
- TCP 半关闭是什么?
close()和shutdown()有什么区别? - HTTP keep-alive 与 TCP keepalive 是一回事吗?
- HTTP/2 为什么仍然存在 TCP 队头阻塞?

