目录
一、三次握手
1. 过程描述
2. 为什么是三次握手?不是两次、四次?
(1)三次握手才可以阻止重复历史连接的初始化
(2)三次握手才可以同步双方的初始序列号
(3)三次握手才可以避免资源浪费
3. 如何唯一确定一个 TCP 连接?
4. TCP 头
(1)序列号
(2)确认应答号
(3)控制位
二、相关问题
1. 第一次握手丢失了,会发生什么?
2. 第二次握手丢失了,会发生什么?
3. 第三次握手丢失了,会发生什么?
一、三次握手
1. 过程描述
- 一开始,客户端和服务端都处于 CLOSE 状态。先是 服务端主动 监听某个端口,处于 LISTEN 状态。
- 客户端 会随机 初始化序号(client_isn),将此序号 置于 TCP 首部的「序号」字段中,同时把 SYN 标志位置为 1,表示 SYN 报文。接着把第一个 SYN 报文 发送给 服务端,表示向 服务端 发起连接,该报文 不包含应用层 数据,之后客户端处于 SYN-SENT 状态。
- 服务端 收到 客户端的 SYN 报文 后,首先 服务端 也 随机初始化 自己的序号(server_isn),将此 序号 填入 TCP 首部的「序号」字段中,其次 把 TCP 首部的「确认应答号」字段 填入 client_isn+1,接着把 SYN 和 ACK 标志 位置为 1。最后 把该报文 发给客户端,该报文 也不包含 应用层数据,之后 服务端 处于 SYN-RCVD 状态。
- 客户端 收到服务端 报文 后,还要 向服务端回 应最后一个 应答报文,首先该 应答 报文 TCP 首部 ACK 标志 位置 1,其次「确认应答号」字段 填入 server_isn +1,最后 把报文 发送给 服务端,这次 报文 可以携带客户 到服务端的 数据,之后 客户端 处于 ESTABLISHED 状态。
- 服务端 收到 客户端的应答 报文后,也进入 ESTABLISHED 状态。
2. 为什么是三次握手?不是两次、四次?
(1)三次握手才可以阻止重复历史连接的初始化(主要原因)
现在 考虑一个 场景,客户端先 发送了 SYN (seq=90)报文,然后 客户端 宕机了,而且 这个 SYN 报文 还被网络阻塞 了,服务端 并没有 收到,接着 客户端重启 后,又重新 向 服务端 建立连接,发送了 SYN (seq=100)报文(这里不是重传 SYN,重传的 SYN 的序列号是一样的)。
客户端 连续 发送多次 SYN(都是同一个四元组)建立 连接的 报文,在网络拥堵 情况下:
- 一个「SYN 报文」比「最新的 SYN」报文早到达了 服务端,那么 此时服务端 就会 回一个 SYN+ACK 报文给 客户端,此报文中的 确认号是 91(90+1)。
- 客户端 收到后,发现 自己 期望收到的 确认号 应该是 100+1,而不是 90+1,于是就会回 RST 报文。
- 服务端 收到 RST 报文后,就会 释放连接。
- 后续 最新的 SYN 抵达了 服务端后,客户端 与 服务端 就可以正常的 完成三次握手了。
在两次握手的情况下,服务端在收到 SYN 报文后,就进入 ESTABLISHED 状态,意味着这时 可以给 对方发送数据,但是 客户端 此时还 没有进入 ESTABLISHED 状态,假设 这次是 历史连接,客户端 判断到此次 连接为 历史连接,那么就会 回 RST 报文来 断开连接,而 服务端 在第一次握手 的时候 就进入ESTABLISHED 状态,所以 它可以 发送数据的,但是 它并不知道 这个是历史连接,它只有 在收到 RST 报文后,才会 断开连接。
可以看到,如果 采用 两次握手建立 TCP 连接的 场景下,服务端 在向客户端 发送数据 前,并 没有阻止掉 历史连接,导致 服务端 建立了一个 历史连接,又白白 发送了 数据,妥妥地 浪费了 服务端的 资源。
因此,要 解决这种 现象,最好 就是在 服务端 发送数据前,也就是 建立 连接之前,要阻止掉 历史连接,这样 就 不会造成 资源浪费,而 要 实现这个功能,就需要 三次握手。
所以,TCP 使用三次握手 建立连接的 最主要 原因是防止「历史连接」初始化了连接。
(2)三次握手才可以同步双方的初始序列号
TCP 协议的 通信双方,都 必须 维护一个「序列号」,序列号 是可靠传输的 一个 关键因素,它的 作用:
- 接收方可以去除重复的数据。
- 接收方可以根据数据包的序列号按序接收。
- 可以标识发送出去的数据包中,哪些是已经被对方收到的(通过 ACK 报文中的序列号知道)。
可见,序列号在 TCP 连接中 占据着 非常 重要的 作用,所以 当客户端 发送携带「初始序列号」的 SYN 报文的 时候,需要 服务端 回一个 ACK 应答报文,表示 客户端的 SYN 报文 已被 服务端 成功接收,那当 服务端 发送「初始序列号」给 客户端的 时候,依然 也要得到 客户端 的 应答回应,这样 一来一回,才能 确保双方的 初始序列号 能被 可靠的 同步。
四次握手 其实也 能够 可靠的 同步双方的 初始化序号,但 由于 第二步 和 第三步 可以 优化成一步,所以 就成了「三次握手」。
而 两次握手只 保证了一方的 初始序列号 能被对方 成功接收,没办法保证 双方的 初始序列号都能 被 确认接收。
(3)三次握手才可以避免资源浪费
如果只有「两次握手」,当 客户端 发生的 SYN 报文在 网络中 阻塞,客户端 没有 接收到 ACK 报文,就会 重新发送 SYN,由于 没有第三次握手,服务端 不清楚 客户端 是否收到了 自己回复的 ACK 报文,所以 服务端 每收到一个 SYN 就 只能先 主动建立 一个连接,这会 造成什么 情况呢?
如果 客户端 发送的 SYN 报文在 网络中 阻塞了,重复 发送 多次 SYN 报文,那么 服务端在 收到请求后 就会 建立 多个冗余 的 无效链接,造成 不必要的 资源浪费。
即 两次握手会 造成消息 滞留情况下,服务端 重复接受 无用的 连接请求 SYN 报文,而 造成重复 分配资源。
3. 如何唯一确定一个 TCP 连接?
TCP 四元组 可以 唯一的 确定一个 连接,四元组 包括如下:
源地址 和 目的地址 的 字段(32位)是在 IP 头部中,作用是 通过 IP 协议 发送报文给 对方主机。
源端口 和 目的端口的字段(16位)是在 TCP 头部 中,作用是 告诉 TCP 协议 应该 把报文发给 哪个进程。
4. TCP 头
(1)序列号
在 建立连接 时 由计算机 生成的 随机数作为 其初始值,通过 SYN 包 传给接收 端主机,每发送一次 数据,就「累加」一次该「数据字节数」的大小。用来 解决网络包 乱序问题。
(2)确认应答号
指下一次「期望」收到的 数据的 序列号,发送端 收到这个 确认应答 以后 可以认为 在 这个序号 以前的数据 都已经被 正常接收。用来 解决丢包的 问题。
(3)控制位
- ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初 建立连接时 的 SYN 包之外 该位必须 设置为 1。
- RST:该位为 1 时,表示 TCP 连接中 出现异常 必须 强制 断开连接。
- SYN:该位为 1 时,表示 希望 建立连接,并在其「序列号」的字段 进行序列号 初始值的 设定。
- FIN:该位为 1 时,表示 今后 不会再有数据 发送,希望 断开连接。当 通信结束 希望 断开连接时,通信 双方的 主机之间就 可以相互 交换 FIN 位为 1 的 TCP 段。
二、相关问题
1. 第一次握手丢失了,会发生什么?
当 客户端想 和 服务端 建立 TCP 连接的时候,首先 第一个 发的 就是 SYN 报文,然后 进入到 SYN_SENT 状态。在这之后,如果 客户端 迟迟 收不到服务端的 SYN-ACK 报文(第二次握手),就会触发「超时重传」机制,重传 SYN 报文,而且 重传的 SYN 报文的 序列号 都是一样的。
在 Linux 里,客户端的 SYN 报文 最大重传次数 由 tcp_syn_retries 内核参数 控制,这个参数是 可以 自定义的,默认值一般是 5。
通常,每次 超时的 时间 是上一次的 2 倍。当 第五次 超时重传 后,会 继续等待 32 秒,如果服务端 仍然 没有回应 ACK,客户端就 不再发送 SYN 包,然后断开 TCP 连接。
举个例子,假设 tcp_syn_retries 参数值为3,那么当客户端的 SYN 报文一直在 网络中 丢失时,会发生 下图的过程:
当客户端 超时 重传 3 次 SYN 报文后,由于 tcp_syn_retries 为 3,已达 到 最大重 传次数,于是 再 等待一段时间(时间为 上一次 超时时间的 2 倍),如果 还是 没能 收到服务端的 第二次握手(SYN-ACK 报文),那么 客户端就会 断开连接。
2. 第二次握手丢失了,会发生什么?
当 服务端 收到 客户端的 第一次握手 后,就会回 SYN-ACK 报文 给 客户端,这个 就是 第二次 握手,此时 服务端会 进入 SYN_RCVD 状态。
第二次握手的 SYN-ACK 报文有 两个目的:
- 第二次握手里的 ACK,是对第一次握手的确认报文。
- 第二次握手里的 SYN,是服务端发起建立 TCP 连接的报文。
因为 第二次握手 报文里 是 包含对 客户端的 第一次握手 的 ACK 确认报文,所以,如果 客户端 迟迟 没有 收到第二次握手,那么 客户端就 觉得 可能自己的 SYN 报文(第一次握手)丢失了,于是 客户端就会 触发 超时重传机制,重传 SYN 报文。
然后,因为 第二次握手 中 包含 服务端的 SYN 报文,所以当 客户端收到 后,需要 给 服务端发送 ACK 确认报文(第三次握手),服务端 才会 认为该 SYN 报文被 客户端收到了。那么,如果第二次握手 丢失了,服务端就 收不到第三次握手,于是 服务端 这边会 触发超时重传机制,重传SYN-ACK 报文。
在 Linux 下,SYN-ACK 报文 的最大 重传次数 由 tcp_synack_retries 内核参数决定,默认值是 5。
因此,当 第二次握手 丢失了,客户端 和 服务端 都会 重传:
- 客户端会重传 SYN 报文,也就是 第一次握手,最大 重传 次数 由 tcp_syn_retries 内核参数决定。
- 服务端会 重传 SYN-ACK 报文,也就是 第二次握手,最大重传 次数 由 tcp_synack_retries 内核 参数 决定。
举个例子,假设 tcp_syn_retries 参数值为1,tcp_synack_retries 参数值为 2,那么当第二次握手 一直 丢失时,发生的 过程如下图:
- 当客户端超时重传1次 SYN报文后,由于 tcp_syn_retries 为1,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的2倍),如果还是没能收到服务端的第二次握手(SYN-ACK 报文),那么客户端就会断开连接。
- 当服务端超时重传 2次 SYN-ACK 报文后,由于 tcp_synack_retries 2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的2倍),如果还是没能收到客户端的第三次握手(ACK报文),那么服务端就会断开连接。
3. 第三次握手丢失了,会发生什么?
客户端 收到 服务端的 SYN-ACK 报文后,就会 给服务端 回一个 ACK 报文,也就是 第三次握手,此时 客户端 状态 进入到 ESTABLISH 状态。
因为 这个 第三次握手 的 ACK 是对 第二次握手 的 SYN 的 确认报文,所以 当 第三次握手 丢失了,如果 服务端 那一方迟迟 收不到这个 确认报文,就会 触发超时重传机制,重传 SYN-ACK 报文,直到 收到 第三次握手,或者 达到 最大重传次数。
注意,ACK 报文 是不会 有重传的,当 ACK 丢失了,就由对方 重传对应的 报文。
举个例子,假设 tcp_synack_retries 参数值为 2,那么当 第三次握手 一直丢失 时,发生的 过程如下图:
当 服务端 超时重传 2 次 SYN-ACK 报文后,由于 tcp_synack_retries 为 2,已达到最大重传次数,于是 再等待一段时间(时间为 上一次 超时时间 的 2 倍),如果 还是没能 收到客户端的 第三次握手(ACK 报文),那么 服务端 就会 断开连接。