关于tcp:常见的Socket网络异常场景分析

94次阅读

共计 9283 个字符,预计需要花费 24 分钟才能阅读完成。

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

简介

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

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

正文完
 0