为什么我背了很多年 TCP 三次握手,还是总觉得差一点?

为什么我背了很多年 TCP 三次握手,还是总觉得差一点?

写在前面

TCP 三次握手这个东西,我学过很多次。

  • 上学的时候学过
  • 找工作的时候背过
  • 面试的时候讲过
  • 抓包的时候也看过

但很多年里,我一直有一种感觉:

好像懂了,又好像没真正懂。

因为网上大多数解释,虽然没错,但总是在说这些话:

  • 确认双方收发能力
  • 同步序列号
  • 防止历史连接

这些结论都对。

但它们没有让我真的"想通"。

后来我才发现,问题不在答案不对,而在于我一直站在的角度看 TCP。

我看到的是:

text 复制代码
SYN
SYN + ACK
ACK

看到的是:

text 复制代码
FIN
ACK
FIN
ACK

但我一直没先回答一个更底层的问题:

连接到底是什么?


一、连接不是一根线,而是内核里的一个状态对象

很多人脑子里对"连接"的想象,其实是一根线:

text 复制代码
客户端 ---------------- 服务器

好像线通了,连接就建立了。

但 TCP 在内核里不是这么工作的。

从代码视角看,连接不是一根线,而是一个对象

你可以把它理解成:

  • 一块内存
  • 一个变量
  • 一个有状态的内核对象

这个对象里要记录很多东西:

  • 当前状态是什么
  • 自己的序列号走到哪里
  • 对方的序列号确认到哪里
  • 我下一步能不能继续

所以 TCP 真正管理的,不是"线通没通",而是:

这个对象现在能不能切到下一个状态。

这时候很多事情就变了。

你不再关心:

一共发了几个包?

而开始关心:

这个对象凭什么有资格切状态?


二、TCP 最关键的一点,不是我发了什么,而是我收到了什么

我后来觉得,TCP 里最容易被忽略的一点就是:

状态切换,盯的不是"我发了什么",而是"我收到了什么"。

为什么?

因为"发"只能说明你的意图。

你发了一个 SYN,只能说明:

我想建立连接。

它不能说明:

  • 对方收到了
  • 对方同意了
  • 对方已经为这个连接建好了自己的状态

所以 TCP 真正决定状态能不能往下走的,不是"我已经发出去了",而是:

我有没有收到对方的 ACK。

这就是我后来真正想通的地方。

连接对象的状态切换,本质上是在等对方的确认把状态闭上。


三、这样再看三次握手,突然就顺了

如果只背包,三次握手是:

text 复制代码
SYN
SYN + ACK
ACK

但如果换成"连接对象状态切换"的视角,它其实简单很多。

第一次:客户端发 SYN

这只能说明一件事:

我想连你。

客户端这边可以把自己的状态推进一点,但推进不到最终状态。

因为它还没收到任何确认。

也就是说,这时候连接对象还不能说"连接建立好了"。

第二次:服务器回 SYN + ACK

这时候服务器做了两件事:

  • 告诉你:你的 SYN 我收到了
  • 告诉你:我这边也为这个连接建好状态了

所以客户端这边一看,才知道:

  • 对方收到我了
  • 对方也能回我

客户端这边的对象,状态就可以继续推进。

但服务器那边还没完全闭上。

为什么?

因为服务器虽然把 SYN + ACK 发出去了,但它还不知道:

客户端到底有没有收到我这个回应。

第三次:客户端回 ACK

这一发很多人以前总觉得像废话。

其实不是。

这一发的意义在于:

它是服务器那边状态闭合所必须的一步。

服务器收到这个 ACK,才能确认:

我前面发出去的 SYN + ACK,对方确实收到了。

到这里,两边的连接对象状态才真正都闭上了。

所以三次握手为什么是三次?

不是因为"三"这个数字神圣。

而是因为:

要让两边的状态对象都闭合,三次刚好够。


四、为什么不是两次?因为两次只能让一边安心,不能让两边都安心

如果只有两次:

text 复制代码
客户端 -> SYN
服务器 -> SYN + ACK

客户端大概还能觉得差不多了。

但服务器不行。

服务器会卡在一个很尴尬的状态里:

我知道你来找我了,我也回你了,但我不知道你到底有没有收到我。

也就是说,服务器那边的状态对象还没闭上。

所以两次不够。

它不够的根本原因,不是什么口诀,而是:

服务器没拿到那个能让自己状态切换的 ACK。


五、四次挥手也是同一个逻辑,只不过 FIN 不是 SYN

很多人握手背完以后,接着又会卡在挥手:

  • 为什么通常是四次?
  • 为什么有时候又是三次?

如果继续只盯包看,就会继续乱。

但如果你已经接受了前面那个视角,其实这里很好理解。

关键在于:

FIN 不是"连接立即关闭",而是"我这个方向没数据了"。

也就是说,TCP 是双向的。

你这个方向关了,不代表对方那个方向也马上关。

所以关闭连接,天然就比建立连接更容易拆成两段。


六、为什么通常是四次?因为 ACK 不能等,FIN 可以等

假设客户端先发一个 FIN。

服务器收到以后,首先必须立刻回一个 ACK。

因为客户端在等确认。

但服务器不一定能立刻发自己的 FIN。

因为它可能:

  • 还有数据没发完
  • 还有业务没处理完
  • 还没准备好关闭自己这个方向

所以这里自然就会拆开:

  1. 先 ACK:我知道你这边发完了
  2. 再 FIN:我这边现在也发完了

所以四次挥手的本质不是"四步模板",而是:

ACK 不能等,FIN 可以等。


七、为什么有时候又会变成三次?因为 ACK 和 FIN 正好可以合并

如果服务器收到对方 FIN 的时候,自己也正好没数据要发了,那么它完全可以直接回:

text 复制代码
ACK + FIN

这样四次就压成三次了。

所以这里没有什么神秘现象。

就是一句话:

能合并就合并,不能合并就分开。

决定因素不是模板,而是时机。


八、到这里,三次握手和四次挥手其实都被一条逻辑打通了

我后来重新看 TCP,真正补上的不是某个知识点,而是一种视角。

这套视角其实很简单:

1. 连接不是线,是内核里的状态对象

你甚至可以把它粗暴理解成:

一个变量,一块内存,一个有状态的对象。

2. 这个对象能不能切状态,不看我发了什么,看我收到了什么

更准确地说:

看我有没有收到对方的 ACK。

3. 所谓三次握手、四次挥手,本质上都在做一件事

闭状态。

这时候你再去看那些经典问题:

  • 为什么是三次
  • 为什么不是两次
  • 为什么挥手通常是四次
  • 为什么有时候又是三次

就会发现它们其实不是四个问题。

它们本质上都是一个问题:

连接对象的状态,到底什么时候才算闭合?


九、回到抓包和排障,真正该看的不是"发了几个包",而是谁还没等到 ACK

我后来再看抓包,思路就变了。

我不再先看:

  • 这是第几次握手
  • 这是第几个 FIN
  • 这个 ACK 是不是多余

而是先看:

现在是谁的状态还没闭上?

比如:

只看到 SYN,没有 SYN + ACK

那意思不是抽象地说"握手没完成"。

而是:

客户端已经想建连接了,但客户端还没收到能让状态继续推进的 ACK。

看到 SYN、SYN + ACK,但看不到最后 ACK

那更具体地说就是:

服务器那边的状态还没闭上。

挥手时只看到 ACK,看不到 FIN

那不是"少了一步"。

而是:

对方先确认了你的关闭请求,但它自己那个方向还没结束。

一旦这么看,TCP 就不会再只是"背模板"。

它会变成一个很清楚的状态流转问题。


相关推荐
一个做软件开发的牛马2 小时前
Spring Boot 自动配置原理揭秘:从 @SpringBootApplication 到手写自定义 Starter
java·后端
周杰伦fans2 小时前
续集:工作空间一切换,我的插件菜单就消失?——MenuBar与Ribbon的自动重载方案
后端·ribbon·c#
可乐ea3 小时前
【Spring Boot + MyBatis|第7篇】JWT 登录认证与拦截器实现
java·spring boot·后端·mybatis·状态模式
西安邮电大学3 小时前
有关栈的经典算法题
java·后端·其他·算法·面试
摇滚侠3 小时前
SpringMVC 入门到实战 配置类替换 XML 配置文件 86-91
xml·java·后端·spring·maven·intellij-idea
我登哥MVP3 小时前
SpringCloud Alibaba 核心组件解析:服务注册与发现(Nacos)
java·spring boot·后端·spring·spring cloud·java-ee·maven
摇滚侠4 小时前
SpringMVC 入门到实战 处理静态资源的过程 64
java·后端·spring·maven·intellij-idea
Liquad Li4 小时前
ABP vNext 标准分层解决方案项目结构完整解析
后端
布朗克1684 小时前
39 Spring Boot Web实战
前端·spring boot·后端·实战