原创:打码日记,欢送分享,转载请保留出处。
简介
在目前微服务的背景下,网络异样越来越常见了,而有一些网络异样十分含糊,了解什么状况下会导致什么异样,还是有肯定难度的,为此我做了大量试验,来复现各种异样场景。
socket 状态变迁图
先疾速回顾下失常状况下 TCP 的交互过程与 socket 状态变迁,如下:
三次握手
- 客户端调用 connect 函数,会发 SYN 包给服务端,客户端状态变为 SYN_SENT,服务端收到后变为 SYN_RECV,同时回复 SYN+ACK 包给客户端。
- 客户端收到 SYN+ACK 包后,变成 ESTABLISHED 状态,同时回复 ACK 包给服务端,并且客户端的 connect 函数执行实现。
- 服务端收到 ACK 包后,也变成 ESTABLISHED 状态,至此连贯建设实现。
思考:如果第一个 SYN 包服务端没收到,会怎么样?
客户端会重发 SYN 包给服务端,服务端收到后会再次发 SYN+ACK 给客户端。
思考:如果最初一个 ACK 包没收到,会怎么样?
服务端会重发 SYN+ACK 包给客户端,客户端收到后会再次发 ACK 给服务端。
这里能够发现,TCP 协定外面,重发都产生在没有收到 ACK 的场景,纯 ACK 确认包不会重发。
数据传输
- 客户端调用 write 函数,发送申请数据,服务端调用 read 函数,接管申请数据。
- 服务端申请解决完结,服务端调用 write 函数,返回响应数据,客户端调用 read 函数,接管响应数据。
思考:如果之前三次握手时 ACK 失落了,但客户端曾经是 ESTABLISHED 状态了,调用 write 发数据了,会怎么样?
write 发的数据包,也是带有 ACK 标记的,不论与之前的 ACK 包哪个先到,服务端都会变成 ESTABLISHED 状态。
而如果 ACK 与数据包都到不了服务端,一段时间后,服务端 SYN_RECV 状态的 Socket 会主动敞开,且不回复任何包给客户端,能够发现这种场景下,客户端认为连贯胜利,而服务端基本就没有连贯。
四次挥手
- 客户端调用 close 函数,会发送 FIN 包给服务端,状态变为 FIN_WAIT_1,服务端收到后,回复 ACK,且状态变为 CLOSE_WAIT。
- 客户端收到 ACK 后,状态变为 FIN_WAIT_2 状态。
- 服务端调用 close 函数,也会发送 FIN 包给客户端,状态变为 LAST_ACK,客户端收到后,回复 ACK,且状态变为 TIME_WAIT。
- 服务端收到 ACK 后,Socket 被操作系统回收,客户端的 TIME_WAIT 状态 Socket 在期待 2MSL 后,也被操作系统回收。
思考:如果一个连贯始终没有被应用 (如连接池),而超过服务端最大闲暇工夫,服务端被动敞开了连贯,会怎么样?
这时服务端会变成 FIN_WAIT_2,这个状态也是有超时工夫的,如果对方始终不发 FIN 过去,操作系统就会回收掉这个 Socket,而客户端会始终是 CLOSE_WAIT 状态。
所以如果 CLOSE_WAIT 状态很多,个别是程序漏写了敞开 Socket 的代码。
从下面的状态变迁图,也能够推断出,绝大多数状况下,SYN_SENT
、SYN_RECV
、FIN_WAIT_1
、LAST_ACK
状态应该很少,除非网络很卡,因为这些状态只有一收到了 ACK 就转变成其它状态了!
ok,下面是 TCP 失常流程,上面以 Java 网络异样为例,讲讲各种异常情况!
常见网络异样场景
连贯超时
产生异样:java.net.SocketTimeoutException: connect timed out
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
这个异样起因是,客户端 connect 建设连贯时,服务端始终没收到 SYN 包,超过了设置的连贯超时工夫后,就会报此异样。
还可能是,服务端收到了 SYN 包,但 SYN+ACK 始终发不到客户端,也会报此异样。
连贯回绝
产生异样:java.net.ConnectException: Connection refused (Connection refused)
at java.net.PlainSocketImpl.socketConnect(Native Method)
at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)
at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206)
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
这个异样起因是,当服务端没有程序监听某个端口时,客户端却又试图 connect 连贯这个端口就会呈现此异样,其本质是服务端回复了一个 RST 包。
注:RST 包就是 TCP 协定中用来解决异常情况的,个别接管方收到 RST 包后,会间接回收 Socket 资源而不通过四次挥手过程。
read 读取超时
产生异样:java.net.SocketTimeoutException: Read timed out
at java.net.SocketInputStream.socketRead0(Native Method)
at java.net.SocketInputStream.socketRead(SocketInputStream.java:116)
at java.net.SocketInputStream.read(SocketInputStream.java:171)
at java.net.SocketInputStream.read(SocketInputStream.java:141)
当 socket.read()读对端数据时,期待数据超时了,则会报 Read timed out
读取超时异样。
- 服务端解决太慢
- 网络卡了,数据包始终传输不过去
大多数状况下,这种异样都是服务端解决太慢导致的,可通过 socket.setSoTimeout()
来批改这个超时工夫,留神了解这个超时工夫,它不是整个读取过程工夫,而是无任何数据通信的闲暇工夫。
write 重传超时
一般来说,因为 socket 有写缓冲 (send buffer),write 办法是不阻塞立刻返回的,但如果 write 大量数据(如文件上传),当 send buffer 用完时 write 办法还是会阻塞的。
不论 write 办法是否阻塞,数据多次重传失败,会导致异样,区别是阻塞 write 被异样打断,而没有阻塞 write 时,会在下一次 write 时抛异样。
对于这种状况的异样信息,不同的操作系统体现不一样,如下:
-
Linux 上,抛如下异样,且会同时会敞开本端 Socket,不给对端发任何包。
产生异样:java.net.SocketException: Connection timed out (Write failed) at java.net.SocketOutputStream.socketWrite0(Native Method) at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111) at java.net.SocketOutputStream.write(SocketOutputStream.java:143)
-
Windows 上,抛如下异样,且会同时会敞开本端 Socket,不给对端发任何包。
产生异样:java.net.SocketException: Connection reset by peer: socket write error at java.net.SocketOutputStream.socketWrite0(Native Method) at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111) at java.net.SocketOutputStream.write(SocketOutputStream.java:143)
对于重传的次数,Linux 上默认 15 次,可通过内核参数
net.ipv4.tcp_retries2
配置,而 Windows 上默认 5 次,可通过注册表项TcpMaxDataRetransmissions
配置。
总而言之,write 超时可能导致 Connection timed out (Write failed)
异样或 Connection reset by peer
异样(Windows 上)。
读写时收到对方 RST 包
一般来说,如果对端机器上连贯不存在了,还调用 write 往其发数据包,对方会回复 RST 包来终止连贯。
注:那什么时候会呈现连贯不存在呢?如机器间接断电后重启,或网络包被路由到了谬误的机器上等,都会使得机器上没有相应的 TCP 连贯。
而当阻塞在 write/read 时,收到了对方的 RST 包,或先收到对方的 RST 包,再 write/read 时,就会报 Connection reset
异样。
如果对端机器上连贯不存在了,本端间断调用 write/read 时,在不同操作系统上会产生不一样的异样序列,如下:
-
在 Linux 或 Windows 上先 write,而后始终 read,体现如下:
# 第一次 write,调用失常,对端返回 RST 包 # 第二次 read,抛 connection reset 异样:产生异样:java.net.SocketException: Connection reset at java.net.SocketInputStream.read(SocketInputStream.java:210) at java.net.SocketInputStream.read(SocketInputStream.java:141) # 第三次 read,抛 connection reset 异样:产生异样:java.net.SocketException: Connection reset at java.net.SocketInputStream.read(SocketInputStream.java:210) at java.net.SocketInputStream.read(SocketInputStream.java:141)
-
在 Linux 上始终 write,体现如下:
# 第一次 write,调用失常,对端返回 RST 包 # 第二次 write,抛 connection reset 异样:产生异样:java.net.SocketException: Connection reset at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:115) at java.net.SocketOutputStream.write(SocketOutputStream.java:143) # 第三次 write,抛 broken pipe 异样:产生异样:java.net.SocketException: Broken pipe (Write failed) at java.net.SocketOutputStream.socketWrite0(Native Method) at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111) at java.net.SocketOutputStream.write(SocketOutputStream.java:143)
-
在 Windows 上始终 write,体现如下:
# 第一次 write,调用失常,对端返回 RST 包 # 第二次 write,抛 Connection reset by peer 异样:产生异样:java.net.SocketException: Connection reset by peer: socket write error at java.net.SocketOutputStream.socketWrite0(Native Method) at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111) at java.net.SocketOutputStream.write(SocketOutputStream.java:143) # 第三次 write,抛 Connection reset by peer 异样:产生异样:java.net.SocketException: Connection reset by peer: socket write error at java.net.SocketOutputStream.socketWrite0(Native Method) at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111) at java.net.SocketOutputStream.write(SocketOutputStream.java:143)
总而言之,RST 包会导致 Connection reset
异样,同时也可能导致 Broken pipe
异样。
对方敞开连贯后仍然读写
如果对方调用 close 敞开了连贯,本端再调用 read 或 write 办法读写数据会怎么样呢?
如果首次是 read 调用,Linux 和 Windows 都会返回-1
,示意 EOF,如下:
如果首次是 write 调用,对端会回复 RST 包,如下:
而如果是间断的 write/read 调用,不同操作系统上体现不同,如下:
-
在 Linux 上始终 write/read,体现如下:
# 第一次 write,调用失常,对端返回 RST 包 # 第二次 write,抛 broken pipe 异样:产生异样:java.net.SocketException: Broken pipe (Write failed) at java.net.SocketOutputStream.socketWrite0(Native Method) at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111) at java.net.SocketOutputStream.write(SocketOutputStream.java:143) # 第三次 write,抛 broken pipe 异样:产生异样:java.net.SocketException: Broken pipe (Write failed) at java.net.SocketOutputStream.socketWrite0(Native Method) at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111) at java.net.SocketOutputStream.write(SocketOutputStream.java:143) # 第四次 read,返回 -1
-
在 Windows 上始终 write/read,体现如下:
# 第一次 write,调用失常,对端返回 RST 包 # 第二次 write,抛 Software caused connection abort: socket write error 异样:产生异样:java.net.SocketException: Software caused connection abort: socket write error at java.net.SocketOutputStream.socketWrite0(Native Method) at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111) at java.net.SocketOutputStream.write(SocketOutputStream.java:143) # 第三次 write,抛 Software caused connection abort: socket write error 异样:产生异样:java.net.SocketException: Software caused connection abort: socket write error at java.net.SocketOutputStream.socketWrite0(Native Method) at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111) at java.net.SocketOutputStream.write(SocketOutputStream.java:143) # 第四次 read,抛 Software caused connection abort: recv failed 异样:产生异样:java.net.SocketException: Software caused connection abort: recv failed at java.net.SocketInputStream.socketRead0(Native Method) at java.net.SocketInputStream.socketRead(SocketInputStream.java:116) at java.net.SocketInputStream.read(SocketInputStream.java:171) at java.net.SocketInputStream.read(SocketInputStream.java:141)
总而言之,如果对方敞开了连贯,本端还 write 数据,会报 Broken pipe
或Software caused connection abort
异样。
注:如果间接
Ctrl+c
或kill -9
杀死程序,因为只是过程死亡,Linux 内核还在,内核会给对端发送 FIN 包以敞开连贯。
其它 RST 场景
下面曾经看到了,绝大多数异样都是因为收到了 RST 包,除了端口未监听或连贯不存在这两种状况会产生 RST 包外,还有一些非凡状况,也会导致 RST 包产生,如下:
- TCP 连贯队列 backlog 满了
如果连贯队列满了,在 Linux 上是抛弃 SYN 包,而 Windows 上是响应 RST 包。 - NAT 环境中连贯长时间闲暇
目前的公网环境,除有公网 IP 的服务器外,基本上都是通过 NAT 转发技术连网的,如果程序中 tcp 连贯长时间未通信,NAT 设施会断开数据链路,而当连贯被再次应用而发送数据时,NAT 设施回复 RST 包。 - GFW 国家防火墙
GFW 国家防火墙如果发现数据包中有敏感信息,回复 RST 中断 TCP 连贯。 - dns 净化
dns 净化会导致 dns 会被解析来谬误的 ip 地址上,而如果对应 ip 地址上没有监听相干端口,就会回复 RST 包。 - socket 的 recv buffer 中还有未读取的数据时敞开连贯
如果 socket 的 recv buffer 中还有未读取走的数据,间接调用 close(),会给对方发 RST 包。 - socket 的 send buffer 中还有未发送的数据时敞开连贯
默认状况下,socket 的 send buffer 中还有未发送的数据时,间接调用 close()会阻塞,直到数据发送结束,但如果设置了 TCP 的 SO_LINGER 选项,则 close 会立马实现,并给对方发 RST 包。 - NAT 环境下,启用了 TCP 疾速回收
Linux 在启用了 tcp_recycle 的状况下,若收到 SYN 包的 timestamp 比之前包的 timestamp 小,则会回复 RST 包,参考:https://mp.weixin.qq.com/s/uw…。 - 应用 Linux 的 NAT 性能时,收到
out of tcp window
的数据包
Linux 的 NAT 是应用 netfilter 机制实现的,对于out of tcp window
的数据包,通过 netfilter 时,会被标记为有效,而 invalid 的报文不在 Connection Track 模块里,即不解决也不抛弃,间接交给协定栈持续解决。
所以包的源 ip 地址不会被替换,对端接管到这个包后,会发现没有对应 socket 连贯,就会回应 RST 数据包,进而导致连贯断开,参考:https://mp.weixin.qq.com/s/ph…。
相干命令
如果你也想复现这些网络异样,能够理解下 iptables 和 hping3 命令,实现包抛弃或发送指定包(如 RST 包),如下:
# 观测 22333 端口数据包
sudo tcpdump -ni any port 22333
# 增加 iptables 规定,抛弃 22333 端口的数据包
sudo iptables -t filter -I INPUT -p tcp -m tcp --dport 22333 -j DROP
# 增加 iptables 规定,抛弃 22333 端口除 SYN+ACK 的所有 ACK 包
sudo iptables -t filter -I INPUT -p tcp -m tcp --dport 22333 --tcp-flags SYN,ACK ACK -j DROP
# 删除 iptables 规定
sudo iptables -t filter -D INPUT -p tcp -m tcp --dport 22333 --tcp-flags SYN,ACK ACK -j DROP
# 手动发 RST 包
# -a:源 ip 地址
# -s:源端口号
# -p:指标端口号
# --rst:开启 RST 标记位
# --win:设置 tcp window 大小
# --setseq:设置包 seq 号
sudo hping3 -a 10.243.72.157 -s 22333 -p 53824 --rst --win 0 --setseq 654041264 -c 1 10.243.211.45