一次 TCP 连接的一生:从三次握手到四次挥手
平时我们发请求,都是调库:requests 一个 .get(),或者 Go 里 http.Get 一下。底层那条 TCP 连接怎么建立、怎么传数据、怎么断开,全被封装掉了。
封起来是好事,省心。但真出问题的时候------连接莫名被重置、端口耗尽、TIME_WAIT 堆积------不懂这层,排查就会特别吃力。
这篇把一条 TCP 连接从生到死走一遍,顺带把几个高频坑说清楚。
一、连接之前:三次握手
想通信,双方得先确认彼此都在、都准备好了。这个过程就是三次握手。
第一次,客户端发一个 SYN,意思是"我想连你,我的初始序号是 x"。
第二次,服务端回一个 SYN + ACK,意思是"我收到了,我也准备好了,我的初始序号是 y,期待你下一次从 x+1 开始发"。
第三次,客户端再回一个 ACK,"好,那我从 x+1 开始发了"。
握手完成,连接建立,双方开始传数据。
二、为什么必须是三次
很多人问,两次不行吗?理论上"你发我收"两次就够了,但有个关键问题:服务端没法确定客户端真的收到了自己的响应。
如果只握手两次,服务端发完 SYN+ACK 就认为连接建立了,可万一是网络里一个延迟了很久的旧请求包飘过来,服务端白白建立了一条没人用的连接,资源就这么被占着。
第三次握手,正好让客户端亲口确认"我收到了、我准备好收数据了"。服务端这时候才敢把连接标记为已建立。
多这一次,本质是确认双方的收发能力都对等可用。
三、连接建立之后:数据是怎么走的
握手完,就该传数据了。这一层最常被提到的两个词是窗口和拥塞控制。
窗口,管的是"我一次能收多少"。发送方要按对方的接收能力来发,发太快对方处理不过来,只会丢包重传,反而更慢。
拥塞控制,管的是"网络这条路上能塞多少"。发送方不能一上来就猛发,得慢慢加速,看着拥塞窗口一点点涨,遇到丢包就退回来。
这两个机制的存在,说明一件事:TCP 追求的是可靠和公平,不是单纯的快。 想要更低延迟,得上别的方案,比如后面的 QUIC。
四、断开的时候:四次挥手
用完要断开了。断开比建立多一次,是四次挥手。
第一次,主动方发一个 FIN,"我没有数据要发了"。
第二次,对方回一个 ACK,"知道了"。注意,这时对方可能还有数据没发完,所以它不会立刻关。
第三次,对方数据发完了,也发一个 FIN,"我也没数据要发了"。
第四次,主动方回一个 ACK,"收到"。连接正式关闭。
五、为什么挥手是四次,握手只要三次
因为 TCP 是全双工的,两个方向独立。
握手的时候,服务端把"收到你的请求"和"我也要连你"这两件事,合并在一个包里发回去了,所以省了一次。
而挥手时,对方的"我知道你要关了"和"我也要关了",中间夹着"我还有数据没发完"的等待。这两件事没法合并,只能分开各发一个包。于是就成了四次。
六、TIME_WAIT:那个让人又爱又恨的状态
主动关闭连接的一方,最后不会立刻消失,而是进入 TIME_WAIT,默认等上两倍报文最大生存时间(通常是 1~4 分钟)。
它的存在有两个理由。
一是保证最后的 ACK 能到达。如果第四次挥手那个 ACK 丢了,对方会重发 FIN,这时候主动方还在 TIME_WAIT 里,就能再回一个 ACK。要是早早就关了,对方等不到确认,会一直重发。
二是让旧连接的迟到报文自然消亡。同一个四元组如果再被新连接用上,旧的残留报文可能干扰新连接。等一会儿,旧报文就过期了。
代价是:频繁短连接会让 TIME_WAIT 堆积,占着端口不放。所以高并发短连接场景要留意这个状态,别等到端口耗尽才发现。
七、几个高频的坑
Connection reset by peer。 对方直接发 RST 把连接掐了,常见原因是对面进程崩了、或者连接在中间设备上被清掉了。看到这个,先确认对面还活着。
连接被"悄悄"断掉。 有些中间设备会在一段时间没流量后,单方面清掉它记录的连接状态。可双方都以为连接还在,直到下次发数据才发现通不了。这就是为什么要配心跳,或者开启 TCP keepalive。
队头阻塞。 同一个连接上,前面的包没到,后面的就得干等。这也是后来要上多路复用、要上 QUIC 的一个动因。
小结
把这条链路串起来看:三次握手建连,窗口和拥塞控制决定怎么传,四次挥手断开,TIME_WAIT 负责收尾。
平时用不着关心,但真遇到连接层面的怪问题,脑子里有这张图,定位会快很多。至少你会知道,该往"连接"这个方向去看,而不是一直盯着业务代码。
如果这篇对你有帮助,点个赞再走。评论区聊聊你踩过最深的连接层坑,我挑典型的再写一篇。