📗 三次握手 & 四次挥手

吞佛童子2022年6月20日
  • 计算机网络
  • 传输层
  • TCP
大约 15 分钟

📗 三次握手 & 四次挥手

1. 三次握手

1) 流程

2) 为什么是三次握手?

  1. 避免历史连接
    • 防止旧的重复连接初始化造成混乱
    • 假设只有2次握手,如果服务器端建立的是历史连接,那么主动方无法回应服务器,告知这是一次历史连接,因为前面的2次连接过程已经用掉了,此时客户端无法发送 RST 的机会
    • 而3次握手,客户端重新建立了新连接后,服务端若回复的是历史连接的响应,那么序列号就不正确,客户端可以通过 RST 终止历史连接
  2. 同步双方初始序列号
    • 双方均需要知道对方的序列号,且要保证自己的序列号被对方知道
  3. 避免服务器端资源浪费
    • 若客户端重复发送多次 SYN 报文,由于时间改变,seq 不同,那么服务器接收一个就会建立对应的连接,造成连接的冗余,出现资源浪费
    • 通过三次握手可以发现无效链接后,客户端可以通过 RST 及时终止

3) 握手阶段携带数据?

  • 前 2 次握手阶段不可以携带数据
  • 第 3 次握手,可以携带数据

4) Linux 中 TCP 状态查询

  • 查看网络相关信息 netstat -napt
  • -n 拒绝显示别名,能显示数字的全部转换为数字
  • -a 显示所有连线中的Socket
  • -p 显示建立相关连接的程序名
  • -t 显示TCP传输协议的连线状况

img_1.png

5) 初始化序列号 [ISN]

  1. 每次建立 TCP 连接时,初始化序列号发生改变,原因:
    • 尽量降低历史报文的影响
      • 重新建立连接后,历史报文被当初此次新连接的报文,被接收,数据发生错乱
    • 防止黑客伪造相同序列号的 TCP 报文被接收方接收
  2. 初始化序列号的生成
    • 初始序列号 ISN 基于时钟 M, 4 ms ++,转一圈 ≈ 4.55 小时
    • ISN = M + hash(源 ip,源 port,目的 ip,目的 port)

6) TCP 分片

  • IP 层会进行数据的分片,为什么还需要 TCP 根据 MSS 进行分片?
    • IP 层根据 MTU = IP 头部 + TCP 头部 + 实际数据长度 = 1500 字节,进行分片
    • TCP 根据 MSS = 建立连接时双方协商的 MSS 值,进行分片
    • 若只有 IP 层进行分片,当一个分片数据丢失时,整个 IP 报文的所有分片均需要重传
    • 若 TCP 参与分片,当一个分片数据丢失后,触发 TCP 丢包重传,只需要传丢失的这个分片

7) 握手数据丢失问题

① 第一次握手数据丢失

  • 当 客户端发送的 SYN & seq 丢失后
  • 若客户端一直收不到服务端的回应,那么会触发 [超时重传] 机制,重新发送 SYN & seq
  • 超时时间受操作系统 & 版本的问题,现假设为 1 秒
    • Linux 中客户端 SYN 报文重传次数由参数 tcp_syn_retries 控制,default = 5
    • 第一次超时重传在 1 秒后,第二次在 2 秒,第三次隔 4 秒,第四次隔 8 秒,第五次隔 16 秒,第五次之后,会等待 32秒
    • 若服务端还没有回应,客户端不再继续发送 SYN 报文,断开 TCP 连接

② 第二次握手数据丢失

  • 当 服务端发送的 SYN & ACK & seq & ack 丢失后
  • 客户端收不到服务端的回应,会触发超时重传机制,重发 SYN 报文
  • 服务端收不到客户端的第三次握手信息,也会触发超时重传机制,重发 SYN-ACK 报文
    • Linux 中 SYN-ACK 报文重传次数由 tcp_synack_retries 控制,default = 5

③ 第三次握手数据丢失

  • 当 客户端发送的 ACK 报文丢失后
  • 服务端收不到客户端的第三次握手信息,触发超时重传机制,重发 SYN-ACK 报文

8) SYN 攻击

① 是什么?

  • 服务端接收到客户端第一次握手发送的 SYN 报文后,会将其存入 半连接队列 中,若半连接队列被占满,则无法继续提供服务

② 如何避免?

  1. 增大半连接队列大小
  • 需要同时增大 tcp_max_syn_backlog & somaxconn & backlog 三个参数的值
  1. 减少 SYN + ACK 重传次数
  • tcp_synack_retries
  1. 开启 tcp_syncookies
  • net.ipv4.tcp_syncookies = 1 当且仅当半连接队列被占满时,才启用
    • == 0 关闭该功能
    • == 2 无条件开启该功能
  • 当 半连接队列占满后,后续 SYN 包不进入 半连接队列,生成一个 cookie 值随第二次握手返回给客户端
  • 客户端回应 ACK 报文后,检验其合法性,如果通过验证,则直接放入全连接队列

③ 半连接队列 VS 全连接队列

  1. 当服务器收到第一次握手的 SYN 报文后,会存放到 半连接队列中
  2. 当服务器收到第三次握手的 ACK 报文后,会将该连接从 半连接队列 转移到 全连接队列中
  3. 应用程序可以通过 accept() 函数调用 socket 接口,从 全连接队列中取出该连接

9) SYN 报文什么情况下会被丢弃?

  • 半连接队列被占满
  • 全连接队列被占满

2. 四次挥手

1) 流程

2) 为什么是四次挥手?

  • 需要确保双方均受到对方发送完成的通知

3) 挥手数据丢失问题

① 第一次挥手数据丢失

  • 客户端发送给服务端的 FIN 报文丢失,客户端正常进入 fin_wait_1 状态
  • 一直收不到服务端的 ACK 报文时,则 客户端 会触发超时重传机制,重传 FIN 报文
  • 参数 tcp_orphan_retries
  • 当达到最大次数后,不再发送 FIN 报文,客户端直接进入 Close 状态

② 第二次挥手数据丢失

  • 服务端发送的 ACK 报文丢失,服务端正常进入 closed_wait 状态
  • 客户端 收不到服务端的 ACK 报文,触发超时重传机制,重发 FIN 报文
  • 由于 ACK 报文从来不会重传,因此服务端不会有超时重传机制

③ 第三次挥手数据丢失

  • 服务端发送的 FIN 报文丢失,服务端正常进入 lack_ack 状态
  • 服务端 一种收不到客户端的 ACK 报文,会触发超时重传机制,重发 FIN 报文
  • 参数 tcp_orphan_retries

④ 第四次挥手数据丢失

  • 客户端发送的 ACK 报文丢失,客户端正常进入 time_wait 状态
  • 服务端 收不到客户端的 ACK 报文,会触发超时重传机制,重发 FIN 报文
  • 参数 tcp_orphan_retries

4) time_wait

① MSL [Maximum Segment Lifetime]

  • 报文最大生存时间
  • 网络传输的报文在网络上存在的最长时间,超过这个时间报文将被丢弃
  • 数据之所以可以被抛弃是因为 TCP 层的下面的 IP 层有个 TTL
    • TTL 记录报文传输过程中经过的最大路由次数,每经过一个路由 TTL--,当 TTL == 0 时被丢弃
  • MSL >= TTL 消耗为 0 的时间
  • Linux 中,TTL == 64,MSL == 30 s

② time_wait 定义

  • time_wait == 2 * MSL
  • 从客户端接收到 服务端的 FIN 报文开始计时
  • 若在 time_wait 时间内,客户端又接收到了 服务端的 FIN 报文,time_wait 会重新计时

③ time_wait 存在的作用

  • 防止旧连接的数据包被重新消费
    • 2MSL 可以使短时间内重新建立的新连接不会受到历史连接的影响,历史连接的数据包均被丢弃,不可能误认为是本次连接的数据包
  • 确保服务端连接被正常关闭
    • 尽可能保证客户端发送的 ACK 报文能被服务端接收到

④ time_wait 过长存在的问题

  • 对内存资源的占用,每个连接均需要消耗内存资源
  • 对端口资源的占用,端口有限

5) 四次挥手中,收到乱序的 FIN 包,会发生什么?

  • 即,FIN 包到达之后,因网络延迟导致本来应该先到达的数据包在 FIN 包之后到达,会发生什么?
  • 收到乱序 FIN 包后,由于 不是想要的 FIN 包,此时客户端不会从 fin_wait_2 状态进入 time_wait 状态
  • 客户端会将乱序的 FIN 包放入乱序队列中,
  • 等之后收到延迟到达的数据包后,会判断乱序队列中是否有与当前数据包的理应下一个数据包
  • 如果找到,取出,这时就会发现这个数据包还带有 FIN 标志,意味着服务端发送完毕
  • 此时,客户端可以进入 time_wait 状态

6) 处于 time_wait 状态的连接,收到相同四元组的 SYN 包,会发生什么?

  • 在客户端收到服务端发送的 FIN 包之后,会进入 time_wait 状态,同时回复服务端一个 ACK 包
  • 此时,收到相同四元组的 SYN 包,会出现以下 2 种可能性:
    • SYN 合法
      • 客户端重用此四元组连接,跳过 2MSL 直接进入 SYN_RECV 状态,可以继续建立连接过程
    • SYN 不合法
      • 客户端再次回复一个第四次握手的 ACK 报文
      • 服务端收到后,发现不是想要的 SYN-ACK 报文,服务端发 RST 报文给客户端
      • 服务端一段时间后还没有收到 SYN-ACK 报文,触发超时重传,重新发送 SYN 报文
    • SYN 是否合法?
      • 双方均开启 TCP 时间戳
        • SYN 合法 服务端发给客户端的 SYN 中的 seq 比客户端期望收到的 ack 要大 && 服务端的 SYN 时间戳比客户端最后收到报文的时间戳 大
        • SYN 不合法 SYN 的 seq 比客户端最后收到的 seq 要小 | SYN 时间戳比客户端最后收到的报文 时间戳 小
      • 双方没有开启 TCP 时间戳
        • SYN 合法 SYN 的 seq 比客户端最后收到的 seq 要大
        • SYN 不合法 SYN 的 seq 比客户端最后收到的 seq 要小

7) 处于 time_wait 状态的连接,收到 RST 后,会发生什么?

  • 取决于内核参数 net.ipv4.tcp_rfc1337
    • 若 == 0 [default]
      • 提前结束 time_wait 状态,释放连接
    • 若 == 1
      • 丢弃该 RST 报文

8) tcp_tw_reuse

  1. 是什么?
    • 如果开启该选项的话,客户端(连接发起方) 在调用 connect() 函数时,内核会随机找一个 TIME_WAIT 状态超过 1 秒的连接给新的连接复用
    • 而服务器一般都是连接被动接收方,所以基本不可能减少服务端的压力
  2. 前置条件?
    • 开启 tcp_timestamps 参数,给 TCP 头部添加时间戳
  3. 为什么设置为 default = off
    • 如果正好被复用连接对应的接收方没有收到 ACK 报文,会导致接收方不能正常关闭
    • 这个问题有点像问,为什么不把 time_wait 的时间设置短一点,比如说从原来的 60 秒设置为 1 秒

3. 中间过程

1) 客户端突然主机崩溃

  • TCP 有保活机制
  • 启动前提:
    • socket 接口设置了 so_keepalive 选项才能生效
  • 在一个时间段内,如果没有任何连接相关活动,TCP 保活机制发挥作用,
  • 每隔一个时间间隔,发送一个探测报文,报文包含的数据非常少
  • 若连续几个探测报文都没有响应,服务器会认为当前 TCP 连接已经死亡
  • 若其中有响应,那么 TCP 保活时间重置,等待下一次的 保活机制 发挥作用的情况
  • 参数:
net.ipv4.tcp_keepalive_time=7200 # 2小时后触发保活机制
net.ipv4.tcp_keepalive_intvl=75  # 每隔 75 秒发送一次探测报文
net.ipv4.tcp_keepalive_probes=9 # 发送 9 次都无响应,则认为对方不可达
  • 若服务端没有开启 keepalive 机制 && 双方一直无数据交互,此时若客户端发生主机崩溃
    • 那么服务端是感知不到的,因为不会发送探测报文,无法得知客户端是否正常
    • 此时服务端对该客户端的 TCP 连接会处于 Established 状态,直到服务端重启进程
  • 若服务端没有开启 keepalive 机制 && 双方一直无数据交互,此时若客户端发生进程崩溃
    • 服务端会发送 FIN 报文,与客户端进行四次挥手,说明进程崩溃,操作系统是可以感知到的

2) 有数据传输,一方异常,会发生什么?

  1. 客户端主机宕机,又迅速重启
    • 客户端主机宕机后,服务端在有限时间内没有收到 响应,会触发超时重传机制
    • 如果此时客户端重启,接收到了重传的报文
      • 若客户端主机没有进程监听该 TCP 报文的端口号
        • 客户端内核回复 RST 报文,要求重置该连接
      • 若客户端主机有进程监听该 TCP 报文的端口号
        • 由于重启,因此之前的 socket 数据已经不存在,所以也无法回复该 TCP 想要的信息,也会回复 RST 报文,要求重置该连接
  2. 客户端主机宕机,且一直没有重启
    • 客户端主机宕机后,服务端在有限时间内没有收到 响应,会触发超时重传机制
    • 若达到了最大超时时间 | 重传次数达到,客户端也没有响应
    • 服务端会停止重传,内核会判定该 TCP 连接有问题,通过 socket 接口告知应用程序,然后服务端该 TCP 连接断开

3) 拔掉网线后,TCP 连接会发生什么?

  • 拔掉网线这个动作发生的这瞬间,并不会直接影响内核保存的 TCP 连接状态,TCP 状态保存在 内核的 struct_socket 结构体中
  1. 拔掉网线后,有数据传输
    • 加上客户端拔掉网线,客户端理应向服务端发送数据,结果服务端一直收不到客户端的数据
    • 因此服务端触发超时重传
    • 若在重传过程中,客户端网线又连接回去,客户端的 socket 结构数据也还在,因此
      • 客户端响应重传报文,继续建立通信
      • 这种情况类似于该数据包收到网络影响,发生了短期的丢包现象
    • 若在重传过程中,客户端一直没有将网线连接回去
      • 服务端达到超时重传阈值后,就不会继续发送数据了,内核会判定该 TCP 连接有问题,通过 socket 接口告知应用程序,然后服务端该 TCP 连接断开
      • 此时若客户端又重新连接网线,向服务端发送数据,会收到 服务端的 RST 报文,重置该连接
  2. 拔掉网线后,无数据传输
    • 未开启 TCP 保活机制
      • 客户端 & 服务端的 TCP 连接一直存在,直到进程重启
    • 开启 TCP 保活机制
      • 达到保活计时器时间后,服务端会发送探测报文
      • 如果响应了探测报文,保活计时器重置,等待下次触发时机
      • 若一直没有响应探测报文,达到阈值后,服务端会认为客户端无法到达,会通知应用程序,随后断开该连接

4. 参数配置

  • 修改 TCP 相关参数,可优化 TCP 高并发通信,
  • 编辑 /etc/sysctl.conf 添加 | 修改以下相关参数
# 服务器对外连接的端口范围,影响该服务器与其他服务器的连接数
net.ipv4.ip_local_port_range =102465535

# TCP最大连接数
net.core.somaxconn = 65535

# SYN 队列的长度,默认为1024 
net.ipv4.tcp_max_syn_backlog = 65535

# 当半连接队列满时,无法接收新连接
net.ipv4.tcp_syncookies = 0

# 修改连接处于 FIN-WAIT-2 状态的时间为 ... 单位:秒
net.ipv4.tcp_fin_timeout = 30

# 开启TCP连接重用,允许处理 TIME-WAIT 状态的连接重新用于新的TCP连接
net.ipv4.tcp_tw_reuse = 1

# 开启快速回收TCP连接中处于 TIME-WAIT 状态的连接
net.ipv4.tcp_tw_recycle = 1

# 保持 TIME_WAIT 状态连接的最大数量,如果超过此值,TIME_WAIT 将立刻被清除并打印警告信息,默认为180000
net.ipv4.tcp_max_tw_buckets =5000

# 当 keepalive(长连接)启用的时候,TCP发送 keepalive 消息(探测包)的时间间隔(秒),默认为 2 小时
net.ipv4.tcp_keepalive_time =1200

# 每个网络接口接收数据包的速率比内核处理这些包的速率快时,允许送到队列的数据包的最大数目
net.core.netdev_max_backlog =65535

# 预留用于接收缓冲的内存默认值(字节) 
net.core.rmem_default = 8388608

# 预留用于接收缓冲的内存最大值(字节) 
net.core.rmem_max = 16777216

# 预留用于发送缓冲的内存默认值(字节) 
net.core.wmem_default = 8388608

# 预留用于发送缓冲的内存最大值(字节) 
net.core.wmem_maX = 16777216

# 避免时间戳异常
net.ipv4.tcp_timestamps = 0

# 系统中最多有多少个 TCP 套接字不被关联到任何一个用户文件句柄上,如果超过这个数字,连接将即刻被复位并打印警告信息,这个限制仅仅是为了防止简单的DoS 攻击
net.ipv4.tcp_max_orphans =3276800
上次编辑于: 2022/10/10 下午8:43:48
贡献者: liuxianzhishou