一、机器准备
本次实验使用两台机器,一台作为服务端,一台作为客户端,运行linux系统。
服务端 192.168.12.36
客户端 192.168.12.37
两台机器各自运行某模块服务,两模块为上下游关系,可以进行通信。
TCP三次握手和四次挥手过程如下图:
二、模拟TCP 第⼀次握⼿ SYN 丢包
为了模拟 TCP 第⼀次握⼿ SYN 丢包的情况,可以在拔掉服务器的⽹线后,⽴刻在客户端给服务端发送请求。
也可以粗暴地在服务端设置iptables规则,使其丢掉来自客户端的所有请求
iptables -A INPUT -s 192.168.12.36 -j DROP
然后在客户端执行tcmdump,抓取访问服务端的数据包
tcpdump tcp port xxx and host 192.168.12.37 -w tcp_sys_timeout.pcap
接着,把 tcp_sys_timeout.pcap ⽂件⽤ Wireshark 打开并进行分析。
总结:通过实验结果,我们可以得知,当客户端发起的 TCP 第⼀次握⼿ SYN 包,在超时时间内没收到服务端的
ACK,就会在超时重传 SYN 数据包,每次超时重传的 RTO 是翻倍上涨的,直到 SYN 包的重传次数到达
tcp_syn_retries (/proc/sys/net/ipv4/tcp_syn_retries)值后,客户端不再发送 SYN 包。
三、模拟TCP 第⼆次握⼿ SYN、ACK 丢包
为了模拟客户端收不到服务端第⼆次握⼿ SYN、ACK 包,我的做法是在客户端加上防⽕墙限制,直接粗暴的把来
⾃服务端的数据都丢弃,防⽕墙的配置如下:
iptables -I INPUT -s 192.168.12.36 -j DROP
接着在客户端发送请求给服务端,并在客户端使用tcpdump命令抓取数据包,然后使用 Wireshark 进行分析。
总结:通过实验结果,我们可以得知,当 TCP 第⼆次握⼿ SYN、ACK 包丢了后,客户端 SYN 包会发⽣超时重
传,服务端 SYN、ACK 也会发⽣超时重传。
客户端 SYN 包超时重传的最⼤次数,是由 tcp_syn_retries (/proc/sys/net/ipv4/tcp_syn_retries)决定的,默认值是 5 次;服务端 SYN、ACK 包时重传的最⼤次数,是由 tcp_synack_retries(/proc/sys/net/ipv4/tcp_synack_retries) 决定的,默认值是 5 次。
四、模拟TCP 第三次握⼿ ACK 丢包
为了模拟 TCP 第三次握⼿ ACK 包丢,我的实验⽅法是在服务端配置防⽕墙,屏蔽客户端 TCP 报⽂中标志位是
ACK 的包,也就是当服务端收到客户端的 TCP ACK 的报⽂时就会丢弃,iptables 配置命令如下:
iptables -I INPUT -s 192.168.12.37 -p tcp --tcp-flag ACK ACK -j DROP
然后在客户端执行tcpdump抓取数据包,在客户端给服务端发送请求。
此时,由于服务端收不到第三次握⼿的 ACK 包,所以⼀直处于 SYN_RECV 状态;
⽽客户端是已完成 TCP 连接建⽴,处于 ESTABLISHED 状态;
过了 1 分钟后,观察发现服务端的 TCP 连接不⻅了;
持续「好⻓」⼀段时间,客户端才断开连接。
总结:在建⽴ TCP 连接时,如果第三次握⼿的 ACK,服务端⽆法收到,则服务端就会短暂处于 SYN_RECV 状态,⽽
客户端会处于 ESTABLISHED 状态。
由于服务端⼀直收不到 TCP 第三次握⼿的 ACK,则会⼀直重传 SYN、ACK 包,直到重传次数超过
tcp_synack_retries 值(默认值 5 次)后,服务端就会断开 TCP 连接。
⽽客户端则会有两种情况:
如果客户端没发送数据包,⼀直处于 ESTABLISHED 状态,然后经过 2 ⼩时 11 分 15 秒(触发了保活机制)才可以发现⼀个
【死亡】连接,于是客户端连接就会断开连接。
如果客户端发送了数据包,⼀直没有收到服务端对该数据包的确认报⽂,则会⼀直重传该数据包,直到重传次数超过 tcp_retries2 值(/proc/sys/net/ipv4/tcp_retries2,默认值 15 次)后,客户端就会断开 TCP 连接。
五、模拟TCP 三次握手后丢包
这种情况就比较简单了,如果需要服务端丢包,可以在服务端使用gdb -p xxx(进程号)阻塞住服务端进行即可。客户端同理。
六、关闭连接的两种方式
关闭连接的⽅式通常有两种,分别是 RST 报⽂关闭和 FIN 报⽂关闭。
如果进程异常退出了,内核就会发送 RST 报⽂来关闭,它可以不⾛四次挥⼿流程,是⼀个暴⼒关闭连接的⽅式。
安全关闭连接的⽅式必须通过四次挥⼿,它由进程调⽤ close 和 shutdown 函数发起 FIN 报⽂(shutdown 参数须传⼊ SHUT_WR 或者 SHUT_RDWR 才会发送 FIN)。
调⽤ close 函数和 shutdown 函数有什么区别?
调⽤了 close 函数意味着完全断开连接,完全断开不仅指⽆法传输数据,⽽且也不能发送数据。 此时,调⽤了
close 函数的⼀⽅的连接叫做「孤⼉连接」,如果你⽤ netstat -p 命令,会发现连接对应的进程名为空。
使⽤ close 函数关闭连接是不优雅的。于是,就出现了⼀种优雅关闭连接的 shutdown 函数,它可以控制只关闭
⼀个⽅向的连接:int shutdown(int sock, int howto);
第⼆个参数决定断开连接的⽅式,主要有以下三种⽅式:
SHUT_RD(0):关闭连接的「读」这个⽅向,如果接收缓冲区有已接收的数据,则将会被丢弃,并且后续再收到新的数据,会对数据进⾏ ACK,然后悄悄地丢弃。也就是说,对端还是会接收到 ACK,在这种情况下根本不知道数据已经被丢弃了。
SHUT_WR(1):关闭连接的「写」这个⽅向,这就是常被称为「半关闭」的连接。如果发送缓冲区还有未发送的数据,将被⽴即发送出去,并发送⼀个 FIN 报⽂给对端。
SHUT_RDWR(2):相当于 SHUT_RD 和 SHUT_WR 操作各⼀次,关闭套接字的读和写两个⽅向。
close 和 shutdown 函数都可以关闭连接,但这两种⽅式关闭的连接,不只功能上有差异,控制它们的 Linux 参数也不相同。
七、第一次挥手丢失了,会发生什么?
当客户端(主动关闭方)调用 close 函数后,就会向服务端发送 FIN 报文,试图与服务端断开连接,此时客户端的连接进入到 FIN_WAIT_1
状态。正常情况下,如果能及时收到服务端(被动关闭方)的 ACK,则会很快变为 FIN_WAIT2
状态。
如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发次数由 tcp_orphan_retries
参数控制。
当客户端重传 FIN 报文的次数超过 tcp_orphan_retries
后,就不再发送 FIN 报文,直接进入到 CLOSE状态。
八、第二次挥手丢失了,会发生什么?
当服务端收到客户端的第一次挥手后,就会先回一个 ACK 确认报文,此时服务端的连接进入到 CLOSE_WAIT
状态。
由于ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。
这里提一下,当客户端收到第二次挥手,也就是收到服务端发送的 ACK 报文后,客户端就会处于 FIN_WAIT2
状态,在这个状态需要等服务端发送第三次挥手,也就是服务端的 FIN 报文。
对于 close 函数关闭的连接,由于无法再发送和接收数据,所以FIN_WAIT2
状态不可以持续太久,而 tcp_fin_timeout
控制了这个状态下连接的持续时长,默认值是 60 秒。这意味着对于调用 close 关闭的连接,如果在 60 秒后还没有收到 FIN 报文,客户端(主动关闭方)的连接就会直接关闭。
九、第三次挥手丢失了,会发生什么?
当服务端(被动关闭方)收到客户端(主动关闭方)的 FIN 报文后,内核会自动回复 ACK,同时连接处于 CLOSE_WAIT
状态,顾名思义,它表示等待应用进程调用 close 函数关闭连接。此时,内核是没有权利替代进程关闭连接,必须由进程主动调用 close 函数来触发服务端发送 FIN 报文。
服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。
如果迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retrie
s 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的。
十、第四次挥手丢失了,会发生什么?
当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT
状态。在 Linux 系统,TIME_WAIT 状态会持续 60 秒(也就是2MSL)后才会进入关闭状态。
然后,服务端(被动关闭方)没有收到 ACK 报文前,还是处于 LAST_ACK 状态。
如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries
参数控制。