tcp三次握手
TCP(传输控制协议)的三次握手,它的核心目的是在正式传输数据前,同步双方的初始序列号,并确认彼此的发送和接收能力都正常,从而建立一个可靠的、全双工的通信连接。
第一次握手:客户端发起连接请求
客户端首先主动发起连接,它会向服务端发送一个特殊的TCP报文段。
报文内容:
SYN=1:这个标志位表示"希望建立连接"。Seq=x:客户端会随机生成一个初始序列号(ISN),记为x。通俗理解 :客户端说:"你好,我想和你建立连接。我的初始序列号是
x,你能收到吗?"状态变化:
- 客户端 :从初始的
CLOSED状态进入SYN-SENT状态,表示连接请求已发送,正在等待服务端的响应。- 服务端 :在收到请求前,服务端会先启动并进入
LISTEN状态,持续监听指定端口,等待客户端的连接请求。
第二次握手:服务端同意并发起连接
服务端收到客户端的
SYN报文后,会给予确认,同时也向客户端发起自己的连接请求。
报文内容:
SYN=1和ACK=1:表示这个报文既是同步请求,也是对客户端请求的确认。Seq=y:服务端也会随机生成一个自己的初始序列号,记为y。Ack=x+1:确认号为x+1,表示"我已经收到了你序列号为x的报文,期待你下一个报文的序列号是x+1"。通俗理解 :服务端说:"我收到你的请求了(
Ack=x+1),我同意建立连接。同时,我也想和你建立连接,我的初始序列号是y,你能收到吗?"状态变化:
- 客户端 :保持在
SYN-SENT状态,等待最终的确认。- 服务端 :从
LISTEN状态进入SYN-RCVD状态,表示已经收到了同步请求,正在等待客户端的最终确认。
第三次握手:客户端最终确认
客户端收到服务端的
SYN+ACK报文后,会发送最后一个确认报文,完成连接的建立。
报文内容:
ACK=1:表示确认收到服务端的报文。Seq=x+1:客户端的序列号变为x+1。Ack=y+1:确认号为y+1,表示"我已经收到了你序列号为y的报文,期待你下一个报文的序列号是y+1"。- 注意:这次握手可以开始携带应用层数据了。
通俗理解 :客户端说:"我收到你的确认和请求了(
Ack=y+1),我们正式开始通信吧!"状态变化:
- 客户端 :从
SYN-SENT状态进入ESTABLISHED状态,表示连接已建立。- 服务端 :收到确认后,从
SYN-RCVD状态也进入ESTABLISHED状态。
至此,三次握手完成,客户端和服务端都进入了 ESTABLISHED 状态,可以开始可靠地双向传输数据了。
为什么是三次握手?不是两次、四次?
简单来说,三次握手是在"可靠性"和"效率"之间找到的最佳平衡点。两次握手无法保证连接的绝对可靠,而四次握手则显得多余且降低效率。
为了更直观地理解,我们可以把建立连接的过程想象成打电话:
- 第一次握手(你拨通电话):
- 你对朋友说:"喂,听得到吗?"
- 目的: 确认你的发送 没问题,朋友的接收没问题。
- 第二次握手(朋友接听):
- 朋友回答:"听得到!那你听得到我说话吗?"
- 目的: 朋友确认了你的接收 没问题(因为他听到了你的声音),同时也测试了自己的发送没问题。
- 此时的问题: 虽然朋友知道你听得到,但你还不知道朋友是否听到了你的回答。如果朋友那边的麦克风坏了,他以为你在听,其实你什么也没听到,这就没法沟通了。
- 第三次握手(你再次确认):
- 你说:"我也听得到你!"
- 目的: 告诉朋友,你的发送 和接收都正常,连接彻底建立。
为什么不能是两次握手?(不可靠)
如果只有两次握手(客户端发SYN -> 服务端回SYN+ACK -> 开始传数据),会存在两个致命缺陷:
1. 防止"已失效的连接请求"突然到达(最核心原因)
在网络中,数据包可能会因为拥堵而"迷路"或延迟。
场景: 客户端A向服务器B发送了一个连接请求(SYN),但因为网络卡顿,这个包卡在了半路。A以为包丢了,超时重发了一个新的连接请求,并成功建立连接、传输数据、关闭了连接。
如果是两次握手: 此时,那个迟到的旧SYN包终于到达了服务器B。B收到后,以为A要建新连接,于是回复SYN+ACK,并认为连接已建立,开始分配资源(内存、端口等)等待A发数据。
后果: A根本不理B(因为A没请求),但B一直傻等,白白浪费服务器资源(这就是"半开连接")。
三次握手的作用: 如果是三次握手,B回复SYN+ACK后,A收到这个包会发现:"这不是我刚才请求的序列号",于是A会直接忽略或发送重置包(RST)。B收不到第三次ACK,就知道连接无效,从而释放资源。
2. 确认双方的"收发能力"都正常
TCP是全双工通信(双方都能同时收发)。
两次握手只能确认:A能发,B能收;B能发,A能收。
但B无法确认:A是否真的收到了B的确认?
只有第三次握手(A回复ACK),才能向B证明:"我确实收到了你的响应,且我的发送功能正常"。这样双方才算真正"握手成功"。
为什么不需要四次握手?(效率低)
其实四次握手也是可以的,只是没必要。
- 原因: 在第二次握手时,服务端发送的
SYN(我想建立连接)和ACK(我确认收到你的请求)是可以合并在一起发送的。- 如果不合并(四次):
- 客户端 -> 服务端:SYN
- 服务端 -> 客户端:ACK(只确认收到)
- 服务端 -> 客户端:SYN(我也要连你)
- 客户端 -> 服务端:ACK
- 后果: 这样多了一次网络往返(RTT),增加了延迟,浪费了带宽,但并没有带来额外的可靠性收益。既然两步并作一步走就能解决问题,何必多此一举呢?
不使用「两次握手」和「四次握手」的原因:
「两次握手」:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
「四次握手」:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
为什么每次建立 TCP 连接时,初始化的序列号都要求不一样呢?
简单来说,TCP 要求每次建立连接时初始序列号(ISN, Initial Sequence Number)都不一样,主要是为了解决两个核心问题:防止历史数据干扰 和 保障连接安全。
如果每次 ISN 都固定(比如都从 0 开始),网络世界将会陷入混乱。
1. 防止"历史报文"干扰新连接(核心原因)
这是最本质的技术原因。在网络通信中,数据包并不总是按顺序到达,有时会因为网络拥堵、路由绕路等原因"迟到"。
假设场景:如果 ISN 每次都固定为 0
- 旧连接(连接 A): 客户端和服务端建立连接,ISN=0。客户端发送了一个数据包(Seq=100),但因为网络卡顿,这个包卡在了路由器的队列里,迟迟未到。随后,连接 A 因为超时或异常断开了。
- 新连接(连接 B): 客户端和服务端再次建立连接(四元组相同),因为规则固定,ISN 依然是 0。
- 灾难发生: 此时,那个在连接 A 中迟到的数据包(Seq=100)终于晃晃悠悠到达了服务端。
- 后果: 服务端一看,新连接 B 的 ISN 是 0,正好收到一个 Seq=100 的包,完全符合预期!于是服务端把这个属于旧连接的垃圾数据当成了新连接的有效数据接收了。这会导致数据错乱,甚至导致应用程序崩溃。
解决方案:ISN 动态变化
通过让 ISN 每次都不一样(通常是随时间递增或随机生成),我们可以极大降低这种风险。
- 新连接(连接 B): 假设这次 ISN 变成了 5000。
- 迟到包到达: 那个旧数据包(Seq=100)终于到了。
- 服务端反应: 服务端检查新连接 B 的期望序列号(应该是 5000 左右),发现收到的包 Seq=100 不在接收窗口内。
- 结果: 服务端直接丢弃这个包,认为是非法数据。新连接 B 因此得到了保护。
RFC 793 的设计哲学: 协议设计者假设一个数据包在网络中的最大存活时间(MSL)是有限的(通常是 2 分钟)。只要 ISN 的增长速度足够快(比如每 4 微秒加 1),就能保证新连接的 ISN 远远大于旧连接可能滞留的包,从而天然隔离历史数据。
2. 防止安全攻击(序列号预测)
这是为了防御黑客。如果 ISN 是固定的或者容易被预测的,TCP 连接将毫无安全性可言。
- 攻击原理(会话劫持): 黑客如果想伪装成客户端向服务器发送恶意数据(比如"删除数据库"指令),他必须伪造一个 TCP 包。而这个包必须包含正确的序列号,服务器才会接受。
- 如果 ISN 可预测: 假设黑客知道你的 ISN 生成规则(比如每次加 1,或者固定为 0),他就可以轻易算出你当前的序列号,伪造出合法的数据包注入到连接中。
- 后果: 黑客可以劫持会话、注入恶意代码或强行切断连接(RST 攻击)。