📗 三次握手 & 四次挥手
2022年6月20日
- 计算机网络
📗 三次握手 & 四次挥手
1. 三次握手
1) 流程
2) 为什么是三次握手?
- 避免历史连接
- 防止旧的重复连接初始化造成混乱
- 假设只有2次握手,如果服务器端建立的是历史连接,那么主动方无法回应服务器,告知这是一次历史连接,因为前面的2次连接过程已经用掉了,此时客户端无法发送 RST 的机会
- 而3次握手,客户端重新建立了新连接后,服务端若回复的是历史连接的响应,那么序列号就不正确,客户端可以通过 RST 终止历史连接
- 同步双方初始序列号
- 双方均需要知道对方的序列号,且要保证自己的序列号被对方知道
- 避免服务器端资源浪费
- 若客户端重复发送多次 SYN 报文,由于时间改变,seq 不同,那么服务器接收一个就会建立对应的连接,造成连接的冗余,出现资源浪费
- 通过三次握手可以发现无效链接后,客户端可以通过 RST 及时终止
3) 握手阶段携带数据?
- 前 2 次握手阶段不可以携带数据
- 第 3 次握手,可以携带数据
4) Linux 中 TCP 状态查询
- 查看网络相关信息
netstat -napt
-n
拒绝显示别名,能显示数字的全部转换为数字-a
显示所有连线中的Socket-p
显示建立相关连接的程序名-t
显示TCP传输协议的连线状况
5) 初始化序列号 [ISN]
- 每次建立 TCP 连接时,初始化序列号发生改变,原因:
- 尽量降低历史报文的影响
- 重新建立连接后,历史报文被当初此次新连接的报文,被接收,数据发生错乱
- 防止黑客伪造相同序列号的 TCP 报文被接收方接收
- 尽量降低历史报文的影响
- 初始化序列号的生成
- 初始序列号 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 连接
- Linux 中客户端 SYN 报文重传次数由参数
② 第二次握手数据丢失
- 当 服务端发送的 SYN & ACK & seq & ack 丢失后
- 客户端收不到服务端的回应,会触发超时重传机制,重发 SYN 报文
- 服务端收不到客户端的第三次握手信息,也会触发超时重传机制,重发 SYN-ACK 报文
- Linux 中 SYN-ACK 报文重传次数由
tcp_synack_retries
控制,default = 5
- Linux 中 SYN-ACK 报文重传次数由
③ 第三次握手数据丢失
- 当 客户端发送的 ACK 报文丢失后
- 服务端收不到客户端的第三次握手信息,触发超时重传机制,重发 SYN-ACK 报文
8) SYN 攻击
① 是什么?
- 服务端接收到客户端第一次握手发送的 SYN 报文后,会将其存入 半连接队列 中,若半连接队列被占满,则无法继续提供服务
② 如何避免?
- 增大半连接队列大小
- 需要同时增大
tcp_max_syn_backlog
&somaxconn
&backlog
三个参数的值
- 减少 SYN + ACK 重传次数
tcp_synack_retries
- 开启
tcp_syncookies
net.ipv4.tcp_syncookies = 1
当且仅当半连接队列被占满时,才启用- == 0 关闭该功能
- == 2 无条件开启该功能
- 当 半连接队列占满后,后续 SYN 包不进入 半连接队列,生成一个 cookie 值随第二次握手返回给客户端
- 客户端回应 ACK 报文后,检验其合法性,如果通过验证,则直接放入全连接队列
③ 半连接队列 VS 全连接队列
- 当服务器收到第一次握手的 SYN 报文后,会存放到 半连接队列中
- 当服务器收到第三次握手的 ACK 报文后,会将该连接从 半连接队列 转移到 全连接队列中
- 应用程序可以通过 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 要小
- 双方均开启 TCP 时间戳
- SYN 合法
7) 处于 time_wait 状态的连接,收到 RST 后,会发生什么?
- 取决于内核参数
net.ipv4.tcp_rfc1337
- 若 == 0 [default]
- 提前结束 time_wait 状态,释放连接
- 若 == 1
- 丢弃该 RST 报文
- 若 == 0 [default]
8) tcp_tw_reuse
- 是什么?
- 如果开启该选项的话,客户端(连接发起方) 在调用
connect()
函数时,内核会随机找一个 TIME_WAIT 状态超过 1 秒的连接给新的连接复用 - 而服务器一般都是连接被动接收方,所以基本不可能减少服务端的压力
- 如果开启该选项的话,客户端(连接发起方) 在调用
- 前置条件?
- 开启
tcp_timestamps
参数,给 TCP 头部添加时间戳
- 开启
- 为什么设置为
default = off
?- 如果正好被复用连接对应的接收方没有收到 ACK 报文,会导致接收方不能正常关闭
- 这个问题有点像问,为什么不把
time_wait
的时间设置短一点,比如说从原来的 60 秒设置为 1 秒
3. 中间过程
1) 客户端突然主机崩溃
- TCP 有保活机制
- 启动前提:
- socket 接口设置了
so_keepalive
选项才能生效
- socket 接口设置了
- 在一个时间段内,如果没有任何连接相关活动,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) 有数据传输,一方异常,会发生什么?
- 客户端主机宕机,又迅速重启
- 客户端主机宕机后,服务端在有限时间内没有收到 响应,会触发超时重传机制
- 如果此时客户端重启,接收到了重传的报文
- 若客户端主机没有进程监听该 TCP 报文的端口号
- 客户端内核回复 RST 报文,要求重置该连接
- 若客户端主机有进程监听该 TCP 报文的端口号
- 由于重启,因此之前的 socket 数据已经不存在,所以也无法回复该 TCP 想要的信息,也会回复 RST 报文,要求重置该连接
- 若客户端主机没有进程监听该 TCP 报文的端口号
- 客户端主机宕机,且一直没有重启
- 客户端主机宕机后,服务端在有限时间内没有收到 响应,会触发超时重传机制
- 若达到了最大超时时间 | 重传次数达到,客户端也没有响应
- 服务端会停止重传,内核会判定该 TCP 连接有问题,通过 socket 接口告知应用程序,然后服务端该 TCP 连接断开
3) 拔掉网线后,TCP 连接会发生什么?
- 拔掉网线这个动作发生的这瞬间,并不会直接影响内核保存的 TCP 连接状态,TCP 状态保存在 内核的
struct_socket
结构体中
- 拔掉网线后,有数据传输
- 加上客户端拔掉网线,客户端理应向服务端发送数据,结果服务端一直收不到客户端的数据
- 因此服务端触发超时重传
- 若在重传过程中,客户端网线又连接回去,客户端的 socket 结构数据也还在,因此
- 客户端响应重传报文,继续建立通信
- 这种情况类似于该数据包收到网络影响,发生了短期的丢包现象
- 若在重传过程中,客户端一直没有将网线连接回去
- 服务端达到超时重传阈值后,就不会继续发送数据了,内核会判定该 TCP 连接有问题,通过 socket 接口告知应用程序,然后服务端该 TCP 连接断开
- 此时若客户端又重新连接网线,向服务端发送数据,会收到 服务端的 RST 报文,重置该连接
- 拔掉网线后,无数据传输
- 未开启 TCP 保活机制
- 客户端 & 服务端的 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