深入理解 TCP 连接的握手和挥手
我们都知道的是 TCP 连接的建立存在 "三次握手, 四次挥手", 但是这中间到底发生了什么? 为什么挥手的次数要更多? 如果只用两次握手会怎么样? 如果握手和挥手的过程中存在网络延迟或者包的丢失会发生什么? 这篇文章将会给你答案.
TCP 头部格式
为了更好地讲解 "三次握手, 四次挥手" 的过程, 我们先来看一下 TCP 报文的头部格式.
序列号
是一个在建立连接的时候由计算机生成的随机数, 存在在 SYN
包中. 每发送一次数据, 该序列号就会累加一次该 数据字节数 的大小.
用来解决网络包乱序的问题.
确认应答号
指下一次 期望 收到的数据的序列号. 发送端收到这个确认应答后会认为该序列号之前的数据都已经被正确接收.
用来解决丢包的问题.
控制位
ACK
: 为 1 时, 表示 确认应答 字段变为有效.RST
: 为 1 时, 表示 TCP 连接中出现异常必须断开连接.SYN
: 为 1 时, 表示希望建立连接, 并将 序列号 字段作为序列号的初始值FIN
: 为 1 时, 表示希望断开连接, 今后不会有数据再发送.
三次握手
我们先来看一下三次握手的示意图:
握手过程
第一次握手
在没有建立连接的时候, 客户端和服务端都会处于
CLOSE
的状态. 当客户端想要发起连接的时候, 就会向服务端发送第一个报文: SYN报文. 格式如下:- 客户端会随机初始化序号:
client_isn
并将其作为序列号 SYN
位会被置 1
发送完该报文后, 客户端会进入 SYN-SENT 状态.
- 客户端会随机初始化序号:
第二次握手
服务端在收到客户端的 SYN 报文后会进行应答, 发送: SYN + ACK 报文. 格式如下:
- 服务端也会随机初始化序号:
server_isn
并将其作为序列号 - 确认应答号填写:
client_isn
+ 1 SYN
和ACK
位置 1
发送完该报文后, 服务端会进入 SYN-RCVD 状态.
- 服务端也会随机初始化序号:
第三次握手
在客户端收到服务端的应答后, 还需要向服务端回应最后一个报文: ACK报文. 格式如下:
- 确认应答号填写:
server_isn
+ 1 ACK
位置 1- 本报文会携带客户端需要发送的数据
发送完该报文后, 客户端会进入 ESTABLISHED 状态. 服务端在收到该报文后也会进入 ESTABLISHED 状态.
- 确认应答号填写:
为什么需要三次?
- 只有三次握手能阻止历史重复连接的初始化 (主要原因)
- 三次握手才能同步双方的初始序列号
- 三次握手才能避免资源浪费
1. 避免重复历史连接
RFC 793 对 TCP 连接使用三次握手的解释:
The principle reason for the three-way handshacke is to prevent old duplicate connection initiations from causing confusion.
三次握手的主要原因时避免历史重复连接导致的初始化混乱.
光看这句话可能会感觉非常抽象. 但是没关系, 我们看下面的这个例子就能很好地理解什么叫做历史连接, 然后三次握手是如何阻止历史连接所造成的混乱的.
我们考虑下面这样一个场景: 客户端首先发送报文 SYN(seq = 90), 然后在没有收到回应的时候 宕机, 并且该 SYN 报文出现了 网络阻塞 未被服务端收到. 后续客户端重启, 又一次尝试建立连接, 发送 SYN(seq = 100).
注意
此处的场景不属于 SYN 报文重传. 如果仅仅是因为网络阻塞导致的报文重传, 两次 SYN 的序列号是一致的.
可以看到网络阻塞导致的主要问题在于 旧的 SYN 报文比新的 SYN 报文更早抵达了服务端. 如果仅仅使用两次握手, 那么当服务端响应了旧报文(seq = 90)的那一刻, 服务器与客户端就已经在 seq = 90 的同步序列基础上建立了连接, 双方同时进入 ESTABLISHED 状态.
但是三次握手中我们可以看到, 服务端在收到旧 SYN 报文之后回应了 SYN + ACK 报文. 客户端在接收到 ACK = 90 + 1 的回应报文时可以发现与自己的预期不符, 从而在连接还未建立的时候就发送 RST
报文来中止连接的建立.
2. 同步双方的序列号
TCP 协议通信双方必须维护一个序列号用于保证消息的可靠传输. 主要作用为:
- 接收方可以去除重复的数据
- 接收方可以通过序列号将消息按顺序重组
- 发送方可以通过 ACK 报文中的序列号知道哪些消息已经被接收
3. 避免资源浪费
其实这个原因与第一个"避免重复历史连接"是一样的. 如果只有两次握手, 那么当旧 SYN 报文抵达服务端时连接就已经被建立. 那么当新的 SYN 报文抵达之后, 服务端又要建立一次连接. 这导致第一次建立了错误的连接浪费了资源, 而且错误连接可能已经产生了数据的传输从而浪费了网络资源.
为什么握手不是四次?
其实握手是四次, 只不过中间的第二次和第三次可以合并, 所以变成了三次握手.
四次挥手
挥手过程
第一次挥手
客户端主动发送 FIN 报文, 表示自己不会再发送数据, 并进入 FIN_WAIT_1 状态.
第二次挥手
服务端收到 FIN 报文后会回复 ACK 报文 进行确认, 然后服务端进入 CLOSE_WAIT 状态.
第三次挥手 如果服务端的应用程序已经发送完所有数据, 则服务端会发送 FIN 报文 表示不再发送数据并进入 LAST_ACK 状态.
第四次挥手
客户端收到服务端的 FIN 报文后, 会回复 ACK 确认报文. 之后客户端会进入 TIME_WAIT 状态. 服务端在接收到这个 ACK 报文后会进入 CLOSE 状态, 而客户端则会在发送报文之后 再等 2MSL 时间 之后再进入 CLOSE状态.
为什么四次挥手中间两次不能合并变成三次挥手?
因为当客户端发起关闭的时候, 应用程序很有可能还有数据没有发送完成.
第二次挥手中服务端发送的 ACK 报文只是为了表示收到了客户端发起的 FIN 报文. 第三次挥手中的 FIN 报文是否发送的决定权并不在服务端, 而是在 服务端的应用程序, 只有当应用程序的数据全部发送完毕后, 应用程序调用关闭函数, 服务端才会发送 FIN报文.
总结
TCP 协议通过三次握手, 四次挥手建立保证传输正确性的链接.
三次挥手包括: 客户端发起的 SYN 报文, 服务端回应的 SYN + ACK 报文和客户端最后确认的 ACK 报文. 其中服务器回应的 SYN + ACK 报文是对第一次握手的 ACK 回应以及自己发起连接的 SYN 报文的合并, 所以四次握手变成了三次握手. 三次握手可以很有效地避免重复历史连接导致的资源浪费和错误.
四次挥手包括: 客户端发起的 FIN 报文, 服务端回应的 ACK 报文, 服务端发起的 FIN 报文和最后客户端确认的 ACK 报文. 四次挥手无法跟握手一样压缩为三次挥手的原因是客户端请求关闭连接时服务端可能还有数据没有传输完, 需要等待服务端应用发送完数据后, 再发送服务端的 FIN 报文.
本文参考资料