Su的技术博客

  • 首页
  • 原创
  • 视频
  • Java
  • MySQL
  • DDD
  • 事故复盘
  • 架构方案
  • AI
  • Other
  • 工具
    • AI工具集
    • 工具清单
    • JSON在线格式化
    • JSON在线比较
    • SQL在线格式化
  • 打赏
  • 关于
路很长,又很短
  1. 首页
  2. Other
  3. 正文
                           

【长连接】为什么 TCP 建立连接需要三次握手

2024-11-23 1103点热度 0人点赞 0条评论

前置知识

首先来看看 TCP 的控制位和状态机,这是理解 TCP 三次握手的基础。

TCP 报文控制位

TCP 报文头部中的控制位用于控制 TCP 连接的状态,可以指示各种控制信息,如连接建立、终止、重置等。

常见的控制位有 6 个:

  1. SYN (Synchronize Sequence numbers): 请求建立连接 (三次握手),在连接建立时的初始数据包中设置,表明发送端希望建立连接并同步序列号,TCP 是双向通信,所以建立连接时,双方都要发一个 SYN, 虽然 SYN 报文不能携带数据,但要消耗掉一个序号
  2. ACK (ACKnowledgment field significant): 确认接收到的数据,ACK 标识设置后,接收端会在确认字段中填入下一个期望接收的序列号,ACK 报文如果不携带数据,则不消耗序号
  3. FIN (No more data from sender): 请求终止连接 (四次挥手),在数据发送完毕后的数据包中设置,通知接收 (对) 方: 发送 (己) 方已发送完所有数据,TCP 是双向通信,所以关闭连接时,双方都要发一个 FIN, 虽然 FIN 报文不携带数据,但要消耗掉一个序号
  4. RST (Reset the connection): 重置连接,用于异常或错误的连接终止,当接收端收到 RST 标识时,会立即终止连接,不做任何数据确认,例如 之前的文章中提到的 TIME_WAIT 问题,注意: 生产环境中出现 RST 包往往意味着潜在的问题
  5. PSH (Push Function): 提示接收端将接收到的数据立即交付应用层,表明数据应当被快速处理,而不需要等待更多的后续数据到达
  6. URG (Urgent Pointer field significant): 指示数据具有高优先级,接收端应尽快处理
  7. ECE (ECN-Echo): 指示通信双方在三次握手时,协商两端是否都支持显示拥塞控制

Sequence Number (序号)

TCP 是双向通信,所以单个连接中的双方都可以向对方发送数据,所以需要各自维护自己的 Seq 字段。

Seq 是动态随机生成的,这样可以避免被伪造的报文重置连接 (RST 攻击)。

TCP 提供有序的传输,所以每个数据报文段都要加上一个 Seq 序号字段:

  • 当接收端收到乱序的包时,可以根据 Seq 重新排序
  • 当接收端收到重复的包时,可以根据 Seq 去重

如图所示,序号增长方式 (重要):

  • 数据段 1 起始 Seq 号为1,长度为 1448 (单位: 子节),那么数据段 2 的 Seq 号就等于 1 + 1448 = 1449
  • 数据段 2 的长度也是 1448,所以数据段 3 的 Seq 号为 1449 + 1448 = 2897

也就是说,一个 Seq 号的大小是根据上一个数据段的 Seq 号和长度相加而来的。

上一个数据段的长度Seq=上一个数据段的Seq+Len(长度)Seq=上一个数据段的Seq+Len(长度)

所以在 TCP 数据传输过程中,任意一方发出的数据段应该是连续的: 后一个包的 Seq 号等于前一个包的 Seq + Len (三次握手和四次挥手除外)。

Len (数据段长度)

为什么 TCP 建立连接需要三次握手

需要注意的是: Len 不包括 TCP 头部长度,所以不要认为 Len = 0 的数据包没有意义,TCP 头部本身携带的信息也很多。

ACK (确认号)

接收端告诉发送端自己已经收到了哪些数据段 (Seq 序号)。

为什么 TCP 建立连接需要三次握手

  • 发送端 发送了 Seq:1 Len:100 的数据到 接收端, 然后 接收端 回复的 ACK 就是 1 + 100 = 101, 表示自己收到了 101 之前的所有数据
  • 发送端 发送了 Seq:101 Len:50 的数据到 接收端, 然后 接收端 回复的 ACK 就是 101 + 50 = 151, 表示自己收到了 151 之前的所有数据

比如甲发送了“Seq:x Len:y”的数据段给乙,那乙回复的确认号就是x+y,这意味着它收到了x+y之前的所有字节。

结论 (重要): 接收端回复的 ACK 号正好等于发送端的下一个 Seq 号,所以我们可以看到 10377 号包的 ACK 正好等于 10378 号包的 Seq。

如果通信中任意一方没有发送任何数据,那么对方返回的 ACK 号也不会发生变化 (也就是三次握手时的初始值)。


TCP 状态机

下面是经典的 TCP 状态机,每个 TCP 连接从最初建立到最后断开,整个生命周期中的所有状态都囊括其中。

为什么 TCP 建立连接需要三次握手

下面是来自维基百科的彩色版本:

图片来源: https://en.wikipedia.org/wiki/File:Tcp_state_diagram.png


三次握手示例

下面是一个典型的三次握手示例图。

图片来源: https://coolshell.cn/articles/11564.html

  1. 第一次握手:客户端发起连接请求,设置请求报文控制位 SYN = 1, 同时初始化一个随机序列号 (ISN) Seq = x, 发送请求报文 SYN 后,客户端进入 SYN-SENT 状态
  2. 第二次握手:服务端收到客户端的连接请求后,初始化一个随机序列号 (ISN) Seq = y, 同时设置应答报文控制位 SYN = 1, 确认控制位 ACK = x + 1, 发送应答报文 SYN-ACK 后,服务端进入 SYN-RECEIVED 状态
  3. 第三次握手:客户端收到服务端的应答后,设置应答确认控制位 ACK = x + 1, 发送应答报文 ACK 后,客户端进入 ESTABLISHED 状态,服务端收到客户端的 ACK = y + 1 报文后,进入 ESTABLISHED 状态,TCP 连接建立完成

在整个连接过程中,客户端和服务端的状态变化如下图所示:

图片来源: tcpipguide.com

从图中可以看到,对于 ESTABLISHED (连接已建立) 这个状态,客户端和服务端的感知时间是不一样的:

  • 对于客户端来说,两次握手完成后,连接建立完成
  • 对于服务端来说,三次握手完成后,连接建立完成

Wireshark 抓包

为了更直观的感受 TCP 的三次握手过程,这里使用 Wireshark 进行抓包:

打开 WireShark 开始抓包,然后在终端执行下列命令:

$ curl -I -H "Connection: close"

切换到 Wireshark 操作界面,使用 tcp 进行包过滤,可以看到如下的 TCP 三次握手过程,其中 192.168.3.68 是本级的局域网 IP 地址。

为什么 TCP 建立连接需要三次握手

  1. 第一次握手:客户端发起连接请求时,使用的 Seq 字段为 3123802190
  2. 第二次握手:服务端发起应答报文时,使用的 Seq 字段为 1071295171, ACK = x + 1, 也就是 3123802191
  3. 第三次握手:客户端发起应答报文时,使用的 ACK = y + 1, 也就是 1071295172

Tips: Wireshark 默认情况下,显示的是 Seq 的相对值 (从 0 开始),如果想看到客户端和服务端的真实随机 Seq 值,可以在 Wireshark 操作界面的菜单进行如下设置:

Edit > Preferences > Protocols > TCP

取消勾选: Relative Sequence numbers

 

图片来源: Wireshark 网络分析就是这么简单


证明 (粗糙版本)

讲完了 TCP 三次握手的理论基础之后,接下来可以分析并证明如下命题:

TCP 建立连接时,至少需要三次握手。

这里我们使用一个非常基础的数学证明方法:反证法,既然至少需要三次握手?那么我们的反证命题如下:

TCP 建立连接时,不需要三次握手。

具体来说,我们假设 TCP 握手次数 N 少于三次就可以建立连接,这里又可以将 N 分为三种范围区间:

为什么 TCP 建立连接需要三次握手

下面对三种范围区间分别进行证明。

1. N < 1

当 N < 1 时,意味着双方都不发起第一次握手,此时双方甚至都不知道彼此的存在,更别谈建立连接进行通信了,所以 N < 1 不成立,此时进入下一个命题: N == 1。

2. N == 1

当 N == 1 时,意味着双方需要一次握手,也就是 发送端 向 接收方 发起连接建立请求 (SYN),但是请求发出后就没有下文了,所以连接还是无法建立。

因为没有收到接收端的应答报文,所以 发送端 此时无法确认两件事情:

发送 接收
发送端 ❌ ❌
接收端 ❌ ❌
  1. 发送端 (自己) 的发送功能是否正常 (否可以将数据正常发送到接收端)
  2. 接收端的接收功能是否正常 (包括是否监听了对应的端口、数据缓冲区是否正常等)

因为无法确认这两件事情,所以发送端认为连接还未建立,自然也就不会向接收方继续发送数据了。所以 N == 1 不成立,此时进入下一个命题: N == 2。

3. N == 2

当 N == 2 时,意味着双方需要两次握手,我们在 N == 1 的基础上继续证明。

在第一次握手后,接收端 收到 发送端 的连接建立请求报文后,会回复一个连接建立应答报文 (SYN-ACK),但是和 发送端 的报文一样,接收端 的应答发出后就没有下文了,所以连接还是无法建立。

1. 发送端 (自己) 的发送功能是否正常 (否可以将数据正常发送到接收端)

2. 接收端的接收功能是否正常 (包括是否监听了对应的端口、数据缓冲区是否正常等)

对于 N == 1 中存在的两个问题,此时已经得到了确认:

发送 接收
发送端 ✅ ❌
接收端 ❌ ✅
  1. 发送端的发送功能是正常的
  2. 接收端的接收功能是正常的

因为没有收到 发送端 的 (确认) 应答报文,所以 接收端 此时无法确认两件事情:

  1. 接收端 (自己) 的发送功能是否正常 (是否可以将数据正常发送到发送端)
  2. 发送端的接收功能是否正常

原命题证明

通过前文中的反证法,可以证明 TCP 建立连接时,至少需要三次握手。

我们沿着前文中的思路,来看看 N == 3 时,发送端 和 接收端 对应的状态变化。

在第二次握手后,发送端 收到 接收端 的连接建立应答报文后,会回复一个连接建立应答报文 (ACK)。

1. 接收端 (自己) 的发送功能是否正常 (是否可以将数据正常发送到发送端)

2. 发送端的接收功能是否正常

对于 N == 2 中存在的两个问题,此时已经得到了确认:

发送 接收
发送端 ✅ ✅
接收端 ✅ ✅
  1. 接收端的发送功能是正常的
  2. 发送端端接收功能是正常的

证明 (正确版本)

前文中通过 (粗糙地) 反证法 + TCP 报文状态 证明了 TCP 建立连接时,至少需要三次握手,除此之外,也可以利用 TCP 序列号 (严谨地) 证明。

为了实现可靠数据传输,TCP 协议的通信双方都必须维护一个序列号 Seq,用来标识发送出去的数据包中哪些是已经被对方收到的。

三次握手的过程等于: 通信双方相互发送初始序列号,并确认对方已经收到了初始序列号的必要过程。

  • 如果只有一次握手,接收端 没有发送初始序列号
  • 如果只有两次握手,只有 发送端 的初始序列号可以被确认,接收端 的初始序列号无法得到确认

除此之外,在只有两次握手的情况下,可能还会出现一种异常情况: 延迟包导致的无效连接。

为什么 TCP 建立连接需要三次握手

如图所示,某个网络有多条路径,客户端建立连接请求的第一个数据包,正好被传输到一条延迟严重的路径,所以迟迟没有到达服务器。

客户端发送超时后,认为第一个数据包丢失了,于是重新发起请求,第二个请求被传输到正常的路径,所以很快就完成了连接。

对于客户端来说,本次通信过程似乎已经结束了,但是此时它的第一个数据包,延迟到达了服务器。因为服务器并不知道这是一个旧的无效请求,所以按照正常情况回复应答。

如果 TCP 只有两次握手,服务器上此时就建立了一个无效的 (过期的重复) 连接。

但是在 TCP 三次握手的机制下,客户端收到服务器的回复后,发现这个 (已经超时的) 连接不是它想要的,所以就应答一个 RST 数据包,服务器收到 RST 数据后,同时关闭连接。

重点: 三次握手,到底握的是什么?发送方和接收方的初始化 Seq 值 (ISN, Initial Sequence Number), 通过这个值就可以区分当前的本次连接和历史旧连接。


结论

理论上讲 3 次以上,不论握手多少次,都无法确认一个 TCP 连接是 “完全可靠” 的。

但通过 3 次握手,至少可以确认连接是 “基本可用” 的,再增加握手次数,也只不过是提高 “连接可用” 这个结论的可信程度而已。

TCP 3 次握手后,发送端 和 接受端状态变化如下:

  • 发送端确认了: 自己发送、接收正常,对方发送、接收正常
  • 接收端确认了: 自己发送、接收正常,对方发送、接收正常
发送 接收
发送端 ✅ ✅
接收端 ✅ ✅

综上所述,TCP 三次握手也是软件工程中的一个经典的 “Trade-Off” 案例。


扩展阅读

  • Beneath the TCP Handshakes in Modern Networking Infrastructures

FQA

第一次握手时可以携带应用数据吗?

不行,因为此时连接还没有建立。

是否可以由接收方将应用数据缓存起来,等到三次握手完成,连接建立之后,再由接收方交给上层应用呢?

也不行,这样会放大 TCP SYN Flood 攻击,如果攻击者伪造了大量的携带数据报文,那么接收方就需要大量的内存来临时存储应用数据,最终导致内存耗尽。

第二次握手时可以携带应用数据吗?

第二次握手是接收方向发送方发送数据,虽然可以携带数据,但是没有任何实质意义。

第三次握手时可以携带应用数据吗?

可以。

发送第三次握手之前,发送方此时已经进入 ESTABLISHED 状态,所以只要第三次握手的报文到达接收方,那么接收方的状态也会进入 ESTABLISHED 状态,连接就算建立完成了。

此时接收方将发送方在第三次握手时携带的应用数据,转交给上层应用即可。

那这样就不会引发 TCP SYN Flood 攻击吗?

作为攻击者来说,也是需要考虑攻击成本的,如果在第三次握手携带应用数据,就会建立起正常的 TCP 连接,攻击者同样需要资源来存储建立的连接,对于攻击者来说,这是本末倒置的。如果攻击者是远程操纵 “肉鸡” 进行攻击的话,直接在连接建立完成后,让 “肉鸡” 发送海量应用请求就可以了,没有必要在第三次握手时携带应用数据。

TCP 连接失败抓包

TCP 握手失败一般分两种类型,要么被拒绝,要么丢包。因此在 Wireshark 中可以用两个过滤表达式定位出大多数失败的握手。

1. 表达式 1

(tcp.flags.reset == 1) && (tcp.Seq == 1)

 

从表面上看,它只是过滤出 Seq 号为1,且含有 Reset 标志的包,似乎与握手无关。

但在 (默认) 启用 Relative Sequence Numbers 的情况下,这往往表示握手请求被对方拒绝了,结果如下图所示。

图片来源: Wireshark 网络分析的艺术

接下来只需右键选中过滤出的包,再点击 Follow TCP Stream 就可以把失败的全过程显示出来,如下图所示。

图片来源: Wireshark 网络分析的艺术

此次握手失败的原因是服务器没有在监听 80 端口,所以拒绝了客户端的握手请求。

2. 表达式 2

(tcp.flags.syn == 1) && (tcp.analysis.retransmission)

 

这个表达式可以过滤出重传的握手请求。

一个握手请求之所以要重传,往往是因为对方没收到,或者对方回复的确认包丢失了。这个重传特征正好用来过滤,结果如下图所示。

图片来源: Wireshark 网络分析的艺术

接下来只需右键选中过滤出的包,再点击 Follow TCP Stream 就可以把失败的全过程显示出来,如下图所示。

图片来源: Wireshark 网络分析的艺术

此次握手失败的原因是丢包,所以服务器收不到握手请求。

 

转自: 为什么 TCP 建立连接需要三次握手 - 蛮荆 https://dbwu.tech/posts/network/why-tcp-does-needs-three-way-handshake/

 

 

 

 

 

更多文章:

  1. 笔记 | 面试又挂了,只因问了:TCP三次握手和四次挥手
  2. 记一次网络请求连接超时的事故
  3. 系统设计 | 业务编号生成
  4. 分布式唯一 ID 生成方案浅谈
  5. 浅析设计模式3 —— 装饰者模式
  6. Chrome插件(扩展)开发全攻略2.6w字,看这篇就够了!
  7. 系统设计 | 如何表达迭代技术方案?(战术篇)
  8. iOS请求访问文件网关服务图片接口异常问题的解决
  9. 系统设计 | 哪些技术标准可以帮助系统设计?
  10. 笔记 | 5种网络IO模型
标签: 长连接 三次握手 转载 面试题 网络 TCP
最后更新:2024-11-23

秋天0261

关注Java领域,后端开发、Netty、Zookeeper、Kafka、ES、分布式、微服务、架构等。分享技术干货,架构设计,实战经验等。

打赏 点赞
< 上一篇
下一篇 >
广告
文章目录
  • 前置知识
    • TCP 报文控制位
    • Sequence Number (序号)
    • Len (数据段长度)
    • ACK (确认号)
    • TCP 状态机
  • 三次握手示例
    • Wireshark 抓包
  • 证明 (粗糙版本)
    • 1. N < 1
    • 2. N == 1
    • 3. N == 2
    • 原命题证明
  • 证明 (正确版本)
  • 结论
  • 扩展阅读
  • FQA
    • 第一次握手时可以携带应用数据吗?
    • 第二次握手时可以携带应用数据吗?
    • 第三次握手时可以携带应用数据吗?
    • TCP 连接失败抓包
最新 热点 推荐
最新 热点 推荐
Anthropic Code with Claude 开发者大会:开启 AI Agent 新时代 视频笔记-微服务架构P4:必懂5种设计模式 视频笔记:微服务架构P4 设计模式:每服务数据库、API 网关和事件驱动架构 干货 | 论Elasticsearch数据建模的重要性 马蜂窝消息总线——面向业务的消息服务设计 基于 MySQL Binlog 实现可配置的异构数据同步 视频笔记:Google发布Agent2Agent协议 视频笔记:什么是微服务,为什么是微服务?
基于 MySQL Binlog 实现可配置的异构数据同步马蜂窝消息总线——面向业务的消息服务设计视频笔记:微服务架构P4 设计模式:每服务数据库、API 网关和事件驱动架构干货 | 论Elasticsearch数据建模的重要性视频笔记-微服务架构P4:必懂5种设计模式Anthropic Code with Claude 开发者大会:开启 AI Agent 新时代
干货!有些bug,跨年才有机会见 Lombok 同时使用 @Data 和 @Builder 的巨坑,千万别乱用! MySQL事务死锁问题排查 笔记08 | 搜狗面试题:IO多路复用之select、poll、epoll的区别 笔记 | 5种网络IO模型 解构领域驱动设计(三):领域驱动设计 MySQL性能优化浅析及线上案例讲解 全链路压测之影子库及ShardingSphere实现影子库源码剖析

CRUD (1) Event Sourcing (1) graphql (1) id (1) NoSQL (1) quarkus (1) rest (1) RocketMQ (2) Spring Boot (1) zk (1) zookeeper (1) 上下文 (1) 事务消息 (1) 二级缓存 (1) 值对象 (1) 关系数据库 (1) 分布式缓存 (1) 原子性 (1) 唯一ID (1) 商品 (1) 多对多 (1) 子域 (1) 字符集 (1) 客户端心跳 (1) 幂等 (2) 干货 (1) 并发 (1) 应用场景 (1) 应用架构图 (1) 康威定律 (2) 异步复制 (1) 微服务架构 (3) 总体方案 (1) 技术方案 (2) 技术架构 (2) 技术架构图 (1) 技能 (1) 持续集成 (1) 支撑域 (1) 故障恢复 (1) 数据架构图 (1) 方案选型 (1) 日记 (1) 服务发现 (1) 服务治理 (1) 服务注册 (2) 机房 (1) 核心域 (1) 泄漏 (1) 洋葱架构 (1) 消息队列 (5) 源码剖析 (1) 灰度发布 (1) 熔断 (1) 生态 (1) 画图工具 (1) 研发团队 (1) 线程 (2) 组织架构 (1) 缓存架构 (1) 编码 (1) 视频 (20) 读写分离 (1) 贵州 (1) 软件设计 (1) 迁移 (1) 通用域 (1) 集群化 (1) 雪花算法 (1) 顺序消息 (1)

推荐链接🔗
  • AI工具集
  • 工具箱🛠️

COPYRIGHT © 2014-2025 verysu.com . ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang

粤ICP备15033072号-2