写这篇TCP连接管理的深度解析,源于最近在排查线上服务时又碰到一堆连接问题。后台日志里飘着“bind: only one usage of each socket address”、“connect timeout”、“connection reset by peer”,网上搜一圈全是零散的口诀,真正把三次握手、四次挥手和保活机制串起来讲透的文章不多。今天这篇就把这些题目拆开揉碎,从协议设计原理讲到实际排障,把抓包、参数调优、常见报错一次说清楚。无论是准备面试,还是在生产环境里被TCP折磨,这篇都能直接用上。
1. 三次握手:为什么非得是三次,而不是两次或四次
1.1 建立连接的核心目标:同步序列号,而不是“打个招呼”
很多教程把三次握手讲成“你好——你好——好的”,这个类比帮助理解流程,但完全掩盖了协议设计的真正目的。TCP是全双工、面向字节流的可靠传输协议,双方要收发数据,就必须各自维护一套序列号(Sequence Number)和确认号(Acknowledgment Number),用于标记数据顺序、去重和重传。
三次握手的本质是:让通信双方各自确认“对方的接收能力”和“自己的发送能力”都正常,同时完成初始序列号(ISN)的同步。
拆开看第一次握手:客户端发送SYN包,里面带一个随机初始序列号x。这个包发出后,客户端进入SYN_SENT状态。它隐含的信息是:我的序列号从x开始。
第二次握手:服务端收到SYN,如果同意建立连接,会回复SYN+ACK包,其中ACK号是x+1,表示“我收到了你的SYN,期待你下一个字节的序列号是x+1”;同时携带自己的初始序列号y,表示“我的序列号从y开始”。服务端进入SYN_RCVD状态。
第三次握手:客户端收到SYN+ACK后,回复一个ACK包,确认号是y+1,表示“我收到了你的SYN,期待你下一个字节的序列号是y+1”。客户端进入ESTABLISHED状态。服务端收到这个ACK后,也进入ESTABLISHED状态。
这里有个关键点:前两次握手只能让服务端确认客户端的发送能力和自己的接收能力正常,但客户端还不知道服务端的发送能力是否正常。只有等第三次握手完成,客户端确认收到SYN+ACK,双方才算都验证了“你发我收、我发你收”这条通路。所以连接建立的最小次数就是三次,少了无法保证双方能力对等,多了又平白增加延迟。
如果只有两次握手,会带来一个经典的僵尸连接问题:客户端因为网络拥堵发出的SYN包超时重传,第一个迟到的SYN到达服务端,服务端直接分配资源并回复SYN+ACK,但客户端根本不知道这个连接存在,不会回复。服务端会一直等待,资源白白占用。三次握手能避免这个场景,因为客户端不会为不存在的连接发送第三次ACK,服务端可以通过超时回收资源。
1.2 初始序列号为什么要随机
另一个容易被忽略的细节是初始序列号。老旧的实现用固定的递增值,存在序列号预测攻击的风险:攻击者猜出ISN后,可以伪造RST包切断正常连接。现代内核采用随机化ISN,配合时间戳选项(TCP Timestamps)还能进一步防序列号回绕。
实际操作中我们可以用 tcpdump抓包验证,三次握手里每个包的标志位和序列号都能看得一清二楚:
bash复制sudo tcpdump -i eth0 -nn 'tcp port 8080 and (tcp[tcpflags] & (tcp-syn|tcp-fin|tcp-rst) != 0)'
抓包输出里,第一次握手是Flags [S],第二次是Flags [S.],第三次是Flags [.]。注意第二个包里SYN和ACK标志同时置位,确认号等于第一个包的序列号加1,这就是“捎带确认”的经典做法。
1.3 握手阶段的“隐藏状态”与防攻击机制
三次握手的SYN包本身也是DDoS攻击的目标,经典攻击方式叫SYN Flood。攻击者伪造海量源IP发送SYN包,服务端回复SYN+ACK后等不到第三次ACK,半连接队列(SYN Queue)被塞满,正常用户无法建立连接。
Linux内核为此提供了一系列防护参数,这也是面试官喜欢深入问的点:
bash复制sysctl net.ipv4.tcp_syncookies
sysctl net.ipv4.tcp_syn_retries
sysctl net.ipv4.tcp_max_syn_backlog
tcp_syncookies置1时,服务端在SYN队列满的情况下不再保留半连接状态,而是通过一种特殊编码把连接信息放在SYN+ACK的序列号里,等客户端回复ACK时再还原。这样既不占用队列资源,也能抵御简单的SYN Flood。tcp_syn_retries控制服务端重发SYN+ACK的次数,默认5次,也就是约63秒后才会放弃半连接。生产环境里如果要调高并发连接能力,tcp_max_syn_backlog和tcp_abort_on_overflow也要一起看。
1.4 握手延迟带来的真实问题:TCP connect超时
最近帮朋友排查一个Java服务调用外部接口特别慢的问题,现象是偶发性的几十秒延迟。最后抓包发现服务端在某些时刻SYN队列溢出,导致客户端发出的SYN被内核直接丢弃,客户端只能等重传。重传间隔从1秒、2秒、4秒指数递增,最长的一次等了将近30秒才连上。
这种问题单看应用层日志几乎无从下手,因为服务端HTTP层根本没有收到请求。排查手段就是抓包,看到大量SYN重传且服务端无响应,再去看ss -lnt里SYN_RCVD状态堆积情况,基本就能锁定是半连接队列溢出。调整tcp_max_syn_backlog、tcp_syncookies以及应用层backlog参数(如Java的acceptCount)可以缓解。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 四次挥手:断开连接为什么是四次,以及TIME_WAIT的那点事
2.1 挥手流程与关闭方向的不对称性
连接关闭比建立更复杂,因为TCP连接是双向的,每一方向必须单独关闭。四次挥手的本质是:通信双方各自发送FIN来关闭自己的数据发送方向,并用ACK确认对方的FIN。
正常流程是:
- 主动关闭方发送FIN,进入FIN_WAIT_1状态,表示“我的数据发完了,不再发送新数据”。
- 被动关闭方收到FIN,回复ACK,进入CLOSE_WAIT状态。主动方收到ACK后进入FIN_WAIT_2。
- 被动关闭方处理完剩余数据后,发送自己的FIN,进入LAST_ACK状态。
- 主动方收到FIN,回复ACK,进入TIME_WAIT状态。被动方收到ACK后进入CLOSED。
注意第2步和第3步之间可能有时间间隔,因为被动关闭方可能还有数据没发完,不能立刻关闭。这就是为什么关闭需要四次而不是三次的原因——ACK和FIN在被动方往往是分开发送的,除非它收到F
