网络-07 TCP 连接管理
前置阅读:网络-06 UDP 与 TCP 对比 TCP 内容较多,本系列拆成四篇:本篇讲连接的建立与关闭,网络-08 讲可靠传输,网络-09 讲流量控制与拥塞控制,网络-10 讲线上调优与故障排查。
1. TCP 是什么
TCP(Transmission Control Protocol 传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
2. 三次握手
先举个形象的例子,两个人AB进行电话沟通,为了保证双向是通的,常进行以下对话:
A:你能听到我说话吗?
B:可以,你能听到吗?
A:我也可以。
这样双方就知道无论是“去”还是“来”都是正常的,就可以聊天了。
第一次握手中seq=i的i值是随机的。

2.1 为什么是三次,两次不行吗?
打电话那个类比解释了"确认双向通道都是通的",但没解释清楚为什么两次就一定不够。核心原因有两个:
1. 两次握手无法让服务端确认客户端已收到自己的 SYN+ACK
如果只有两次(客户端 SYN → 服务端 SYN+ACK 就建立连接),服务端发出 SYN+ACK 后就单方面认为连接已建立,开始发数据。但这个 SYN+ACK 可能丢了,客户端根本不知道有这个连接存在,服务端却在白白发送数据。第三次握手的意义就在于让服务端确认"客户端确实收到了我的序列号"。
2. 防止历史连接(失效的连接请求)造成资源浪费
这是更本质的原因。假设客户端发出的第一个 SYN 因为网络拥塞在某个路由器里滞留了很久,客户端超时后重发 SYN 并正常完成了通信、关闭了连接。此时那个滞留的旧 SYN 才姗姗来迟地到达服务端:
- 两次握手:服务端无法分辨这是新请求还是旧的,直接就建立了连接并分配缓冲区,而客户端早就不认这个连接了——服务端的资源就这么被白白占住。
- 三次握手:服务端回 SYN+ACK 后,客户端发现这个 ACK 号与自己当前的状态对不上(不是自己期待的序列号),会回一个 RST 把这个连接终止掉。服务端因此不会陷入错误状态。
所以三次握手的本质是:用三次交互同步双方的初始序列号(ISN),并让双方都确认对方已经收到了自己的 ISN。这也解释了为什么 ISN 要随机——固定的话就无法区分历史连接,还容易被攻击者猜测并伪造报文注入连接。
2.2 半连接队列与全连接队列
三次握手在内核里对应两个队列,排查线上"连接超时"问题时经常要看:
- 半连接队列(SYN queue):服务端收到 SYN、回了 SYN+ACK,但还没收到第三次握手的 ACK,此时连接处于
SYN_RCVD状态,放在这里。大小受net.ipv4.tcp_max_syn_backlog影响。SYN Flood 攻击打满的就是这个队列。 - 全连接队列(accept queue):三次握手已完成,连接处于
ESTABLISHED,等待应用进程调用accept()把它取走。大小取listen(fd, backlog)的 backlog 与net.core.somaxconn的较小值。
队列满了会发生什么?
- 半连接队列满:新来的 SYN 被丢弃(若开启
tcp_syncookies则不受此限制,见下文 SYN Flood 一节)。 - 全连接队列满:Linux 默认不会立即回 RST,而可能忽略客户端最后的 ACK、保留握手请求并重传 SYN+ACK,期待应用尽快
accept()腾出位置;客户端此时可能已经认为连接建立成功,于是发送的数据得不到正常处理并发生重传,最终表现为连接或请求超时。net.ipv4.tcp_abort_on_overflow=1时内核会更积极地回 RST,让客户端快速失败,但这通常不是首选方案。
这是一个很典型的故障:应用进程 accept() 太慢(比如业务逻辑阻塞在 accept 循环里),全连接队列堆积溢出,客户端大面积超时,而服务端日志上什么都看不到。排查命令:
# 看全连接队列溢出次数(overflowed 计数持续增长就是它)
netstat -s | grep -i "listen"
# LISTEN 状态下 Recv-Q 是当前 accept 队列长度,Send-Q 是队列上限
ss -lnt
3. 四次挥手
Tcp断开连接时需要4次挥手。 还是先举个例子,两个人进行电话沟通时突然有一个想挂断,一般会有如下对话:
A:我不想说了(第一次挥手。A发送 FIN,表示不A不再说什么事情)
B:好的 (第二次挥手。B发送ACK表示知道了,但是他可能有话没说话,会继续向A说)
B:对了,还有一个事情我要说完…(B没说完话的话继续说)
B:说完了,我也不想说了(第三次挥手。B发送FIN表示他也说完了)
A:好的(第四次挥手。A发送ACK表示知道了,自此完全断开)
在释放连接时,由于TCP是全双工的,因此最后要由两端分别进行关闭,这个流程如下:
假设Client端发起中断连接请求,也就是发送FIN报文。Server端接到FIN报文后,意思是说"我Client端没有数据要发给你了",但是如果你还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据。所以你先发送ACK,“告诉Client端,你的请求我收到了,但是我还没准备好,请继续你等我的消息”。这个时候Client端就进入FIN_WAIT状态,继续等待Server端的FIN报文。当Server端确定数据已发送完成,则向Client端发送FIN报文,“告诉Client端,好了,我这边数据发完了,准备好关闭连接了”。Client端收到FIN报文后,“就知道可以关闭连接了,但是他还是不相信网络,怕Server端不知道要关闭,所以发送ACK后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。“,Server端收到ACK后,“就知道可以断开连接了”。Client端等待了2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,我Client端也可以关闭连接了。Ok,TCP连接就这样关闭了!
关闭连接有主动关闭和被动关闭一说,这里为了简化理解,我们以客户端作为主动关闭方,服务器为被动关闭方。

三次握手,四次挥手完整流程:

整个过程Client端所经历的状态如下:

而Server端所经历的过程如下:

【注意】 在TIME_WAIT状态中,如果TCP client端最后一次发送的ACK丢失了,它将重新发送。TIME_WAIT状态中所需要的时间是依赖于实现方法的。典型的值为30秒、1分钟和2分钟。等待之后连接正式关闭,并且所有的资源(包括端口号)都被释放。
我们不仅疑问,为啥断开的时候会比握手的时候多一次? 第二次握手时,SYN+ACK其实可以放一起,但是第二次挥手时ACK 和 ACK却不能放一起,因为B也许有话没说完
4. SYN Flood 洪泛攻击
原理:攻击者首先伪造地址对 服务器发起SYN请求,服务器回应(SYN+ACK)包,而真实的IP会认为,我没有发送请求,不作回应。服务 器没有收到回应,这样的话,服务器不知 道(SYN+ACK)是否发送成功,默认情况下会重试5次(tcp_syn_retries)。这样的话,对于服务器的内存,带宽都有很大的消耗。攻击者 如果处于公网,可以伪造IP的话,对于服务器就很难根据IP来判断攻击者,给防护带来很大的困难。
linux内核参数调优主要有下面三个:
- tcp_max_syn_backlog 从字面上就可以推断出是什么意思。在内核里有个队列用来存放还没有确认ACK的客户端请求,当等待的请求数大于tcp_max_syn_backlog时,后面的会被丢弃。 所以,适当增大这个值,可以在压力大的时候提高握手的成功率。手册里推荐大于1024。
- tcp_synack_retries 这个是三次握手中,服务器回应 SYN+ACK 给客户端时,重试的次数。默认是5。显然攻击者是不会完成整个三次握手的,因此服务器发出的 SYN+ACK 包在没有回应的情况下,会重试发送。当发送者是伪造IP时,服务器的 SYN+ACK 回应自然是无效的。 这个值可以降低以缩短半连接占用时间,但不应把 0 或 1 当作通用推荐。弱网、跨地域链路上的正常客户端也可能丢失 SYN+ACK,设置过低会直接降低建连成功率。防御 SYN Flood 应优先结合 SYN cookies、边界限速/清洗、队列容量和监控,再通过压测决定是否调整重试次数。
- tcp_syncookies
Linux中SYN cookie是非常巧妙地利用了TCP规范来绕过了TCP连接建立过程的验证过程,从而让服务器的负载可以大大降低。
在三次握手中,当服务器回应(SYN + ACK)包后,客户端要回应一个 n + 1 的 ACK 到服务器,其中 n 是服务器自己指定的初始序列号。启用
tcp_syncookies后,内核不再把这个半连接放入半连接队列(即不为它保存任何状态),而是把连接的关键信息(源/目的 IP 和端口、时间戳、MSS 等)经过哈希编码,塞进 n 这个序列号本身。当客户端提交第三次握手的 ACK 包时,内核从 ack 号中取出并校验这个 cookie,校验通过就地重建连接状态,认为这是一个合法连接。
这样一来,服务器在握手完成前完全不占用内存,半连接队列被打满也就不再是问题——这正是它能缓解 SYN Flood 的原因。代价是序列号空间有限,装不下 TCP 选项的全部信息,所以开启 syncookies 时部分选项(如窗口扩大因子、SACK)可能协商不上,属于高负载下的降级手段。
5. 本章面试题
为什么是三次握手,两次不行吗?
两个原因:
- 两次握手时服务端无法确认客户端收到了自己的 SYN+ACK。服务端发出后就单方面认为连接已建立并开始发数据,但这个包可能丢了,客户端根本不知道有这个连接。
- 防止历史连接浪费资源(更本质)。滞留在网络中的旧 SYN 迟到抵达时,两次握手下服务端无法分辨新旧,会直接建连并分配缓冲区;三次握手下客户端发现 ACK 号对不上会回 RST 终止它。
本质是:用三次交互同步双方的初始序列号(ISN),并让双方都确认对方收到了自己的 ISN。这也是 ISN 必须随机的原因——固定的话无法区分历史连接,还容易被伪造报文注入。
为什么挥手要四次,比握手多一次?
因为 TCP 是全双工的,两个方向要独立关闭。
握手时服务端的 SYN 和 ACK 可以合并成一个包发出(它没有"还有数据要发"的问题)。而挥手时被动关闭方收到 FIN 后,往往还有数据没发完,所以只能先回 ACK(“我知道了”),等自己的数据发完再单独发 FIN,无法合并。
这中间的状态就是半关闭。
什么是半关闭?怎么触发?
TCP 全双工,两个方向可独立关闭。一方发 FIN 表示"我不再发数据了”,但仍可继续接收对方数据——这就是半关闭。
主动触发:shutdown(fd, SHUT_WR) 只关写方向(发 FIN),读方向保持可用;这与 close() 同时关闭两个方向不同。
状态上:主动方进入 FIN_WAIT_2,被动方处于 CLOSE_WAIT,此时被动方仍可继续发数据。
如果对方迟迟不发 FIN(常见于代码忘记 close(),表现为 CLOSE_WAIT 堆积),主动方可能长期处于 FIN_WAIT_2。Linux 的 tcp_fin_timeout 主要约束已经脱离应用、成为 orphan 的 FIN_WAIT_2 socket;应用仍持有 fd 时不能指望内核参数替代码收尾。详见 网络-10。
TIME_WAIT 为什么要等 2MSL?一个 MSL 行不行?
不行。 MSL 是一个 TCP 报文段在网络中存活的最大时间。
主动关闭方发出最后的 ACK 后,它不知道对方是否收到:
- 若对方没收到,会超时重传 FIN,此时主动方需要还能响应;
- 若对方收到了,则不会再发任何消息。
两种情况都要等,取最坏情况:去向 ACK 的最大存活时间(1 MSL)+ 来向重传 FIN 的最大存活时间(1 MSL)= 2 MSL。
另一个作用:让本次连接的所有残留报文在网络中消亡,避免它们干扰之后复用同一四元组的新连接。如果不等,新连接可能莫名收到上一个连接的 FIN。
半连接队列和全连接队列的区别?队列满了会怎样?
- 半连接队列(SYN queue):收到 SYN、回了 SYN+ACK,但还没收到第三次握手 ACK,连接处于
SYN_RCVD。受tcp_max_syn_backlog影响。SYN Flood 打满的是这个队列。 - 全连接队列(accept queue):握手已完成、处于
ESTABLISHED,等应用accept()取走。大小为min(listen 的 backlog, net.core.somaxconn)。
队列满的后果:
- 半连接满 → 丢弃新 SYN(开了
tcp_syncookies则不受限); - 全连接满 → Linux 默认可能忽略最后 ACK 并重传 SYN+ACK,客户端一侧却可能已进入 ESTABLISHED,随后数据重传并超时;开启
tcp_abort_on_overflow才会更积极地回 RST。服务端应用日志通常看不到,因为连接还没有被accept()。
排查:ss -lnt 看 Recv-Q 是否顶到 Send-Q,netstat -s | grep listen 看溢出计数。详见 网络-10。
RST 是什么?什么情况下会出现?
RST 表示立即终止连接,不走正常的四次挥手。常见成因:
- 向未被监听的端口发送数据(这就是
Connection refused的来源); - 对方已
close()关闭连接,你还继续发数据; - 接收缓冲区仍有未读数据时调用
close(),会发 RST 强制关闭(而非 FIN); - 请求超时;
- 中间设备(防火墙、LB)主动掐断。
排查线上 Connection reset by peer 的关键是判断 RST 是谁发的,方法见 网络-21。
SYN Flood 的原理和防御?tcp_syncookies 是怎么工作的?
原理:攻击者用伪造的源 IP 发大量 SYN,服务端回 SYN+ACK 后收不到第三次握手(真实 IP 不会响应),于是半连接队列被占满,并反复重传 SYN+ACK 消耗资源。因为源 IP 是伪造的,很难按 IP 封禁。
防御参数:
tcp_max_syn_backlog调大半连接队列;tcp_synack_retries调小(0 或 1),减少无用重传;tcp_syncookies = 1(最有效)。
syncookies 原理:不再把半连接放入队列(不保存任何状态),而是把连接关键信息(源/目的 IP 端口、时间戳、MSS)哈希编码进初始序列号本身。客户端第三次握手带回 ack 号时,内核从中取出并校验 cookie,通过则就地重建连接。
代价:序列号空间有限,装不下全部 TCP 选项,开启后窗口扩大因子、SACK 等可能协商不上——属于高负载下的降级手段。
xingliuhua