原创:打码日记,欢送分享,转载请保留出处。

简介

在目前微服务的背景下,网络异样越来越常见了,而有一些网络异样十分含糊,了解什么状况下会导致什么异样,还是有肯定难度的,为此我做了大量试验,来复现各种异样场景。

socket状态变迁图

先疾速回顾下失常状况下TCP的交互过程与socket状态变迁,如下:

三次握手

  1. 客户端调用connect函数,会发SYN包给服务端,客户端状态变为SYN_SENT,服务端收到后变为SYN_RECV,同时回复SYN+ACK包给客户端。
  2. 客户端收到SYN+ACK包后,变成ESTABLISHED状态,同时回复ACK包给服务端,并且客户端的connect函数执行实现。
  3. 服务端收到ACK包后,也变成ESTABLISHED状态,至此连贯建设实现。

思考:如果第一个SYN包服务端没收到,会怎么样?
客户端会重发SYN包给服务端,服务端收到后会再次发SYN+ACK给客户端。

思考:如果最初一个ACK包没收到,会怎么样?
服务端会重发SYN+ACK包给客户端,客户端收到后会再次发ACK给服务端。

这里能够发现,TCP协定外面,重发都产生在没有收到ACK的场景,纯ACK确认包不会重发。

数据传输

  1. 客户端调用write函数,发送申请数据,服务端调用read函数,接管申请数据。
  2. 服务端申请解决完结,服务端调用write函数,返回响应数据,客户端调用read函数,接管响应数据。

思考:如果之前三次握手时ACK失落了,但客户端曾经是ESTABLISHED状态了,调用write发数据了,会怎么样?
write发的数据包,也是带有ACK标记的,不论与之前的ACK包哪个先到,服务端都会变成ESTABLISHED状态。

而如果ACK与数据包都到不了服务端,一段时间后,服务端SYN_RECV状态的Socket会主动敞开,且不回复任何包给客户端,能够发现这种场景下,客户端认为连贯胜利,而服务端基本就没有连贯。

四次挥手

  1. 客户端调用close函数,会发送FIN包给服务端,状态变为FIN_WAIT_1,服务端收到后,回复ACK,且状态变为CLOSE_WAIT。
  2. 客户端收到ACK后,状态变为FIN_WAIT_2状态。
  3. 服务端调用close函数,也会发送FIN包给客户端,状态变为LAST_ACK,客户端收到后,回复ACK,且状态变为TIME_WAIT。
  4. 服务端收到ACK后,Socket被操作系统回收,客户端的TIME_WAIT状态Socket在期待2MSL后,也被操作系统回收。

思考:如果一个连贯始终没有被应用(如连接池),而超过服务端最大闲暇工夫,服务端被动敞开了连贯,会怎么样?
这时服务端会变成FIN_WAIT_2,这个状态也是有超时工夫的,如果对方始终不发FIN过去,操作系统就会回收掉这个Socket,而客户端会始终是CLOSE_WAIT状态。

所以如果CLOSE_WAIT状态很多,个别是程序漏写了敞开Socket的代码。

从下面的状态变迁图,也能够推断出,绝大多数状况下,SYN_SENTSYN_RECVFIN_WAIT_1LAST_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读取超时异样。

  1. 服务端解决太慢
  2. 网络卡了,数据包始终传输不过去

大多数状况下,这种异样都是服务端解决太慢导致的,可通过socket.setSoTimeout()来批改这个超时工夫,留神了解这个超时工夫,它不是整个读取过程工夫,而是无任何数据通信的闲暇工夫。

write重传超时

一般来说,因为socket有写缓冲(send buffer),write办法是不阻塞立刻返回的,但如果write大量数据(如文件上传),当send buffer用完时write办法还是会阻塞的。
不论write办法是否阻塞,数据多次重传失败,会导致异样,区别是阻塞write被异样打断,而没有阻塞write时,会在下一次write时抛异样。

对于这种状况的异样信息,不同的操作系统体现不一样,如下:

  1. 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)
  2. 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时,在不同操作系统上会产生不一样的异样序列,如下:

  1. 在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)
  2. 在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)
  3. 在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调用,不同操作系统上体现不同,如下:

  1. 在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
  2. 在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 pipeSoftware caused connection abort异样。

注:如果间接Ctrl+ckill -9杀死程序,因为只是过程死亡,Linux内核还在,内核会给对端发送FIN包以敞开连贯。

其它RST场景

下面曾经看到了,绝大多数异样都是因为收到了RST包,除了端口未监听或连贯不存在这两种状况会产生RST包外,还有一些非凡状况,也会导致RST包产生,如下:

  1. TCP连贯队列backlog满了
    如果连贯队列满了,在Linux上是抛弃SYN包,而Windows上是响应RST包。
  2. NAT环境中连贯长时间闲暇
    目前的公网环境,除有公网IP的服务器外,基本上都是通过NAT转发技术连网的,如果程序中tcp连贯长时间未通信,NAT设施会断开数据链路,而当连贯被再次应用而发送数据时,NAT设施回复RST包。
  3. GFW国家防火墙
    GFW国家防火墙如果发现数据包中有敏感信息,回复RST中断TCP连贯。
  4. dns净化
    dns净化会导致dns会被解析来谬误的ip地址上,而如果对应ip地址上没有监听相干端口,就会回复RST包。
  5. socket的recv buffer中还有未读取的数据时敞开连贯
    如果socket的recv buffer中还有未读取走的数据,间接调用close(),会给对方发RST包。
  6. socket的send buffer中还有未发送的数据时敞开连贯
    默认状况下,socket的send buffer中还有未发送的数据时,间接调用close()会阻塞,直到数据发送结束,但如果设置了TCP的SO_LINGER选项,则close会立马实现,并给对方发RST包。
  7. NAT环境下,启用了TCP疾速回收
    Linux在启用了tcp_recycle的状况下,若收到SYN包的timestamp比之前包的timestamp小,则会回复RST包,参考:https://mp.weixin.qq.com/s/uw... 。
  8. 应用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