原创:打码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处。

简介

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

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)
  1. 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)
  1. 在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)
  1. 在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
  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 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/uwykopNnkcRL5JXTVufyBw

  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/phcaowQWFQf9dzFCqxSCJA

相关命令

如果你也想复现这些网络异常,可以了解下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

往期内容

神秘的backlog参数与TCP连接队列

Linux命令拾遗-入门篇

原来awk真是神器啊

Linux文本命令技巧(上)

Linux文本命令技巧(下)

字符编码解惑

常见的Socket网络异常场景分析的更多相关文章

  1. 异常测试之Socket网络异常

    本文由作者张雨授权网易云社区发布. 前言 不知道大家在测试的过程中有没有发现关于异常测试这样一个特点: 无论是分散在功能测试中的异常用例还是规模相对较大的专项异常测试中,异常测试的用例占比虽然不大但是 ...

  2. .NET面试题系列(十九)Socket网络异常类型

    序言 资料 异常测试之Socket网络异常

  3. 4种Kafka网络中断和网络分区场景分析

    摘要:本文主要带来4种Kafka网络中断和网络分区场景分析. 本文分享自华为云社区<Kafka网络中断和网络分区场景分析>,作者: 中间件小哥. 以Kafka 2.7.1版本为例,依赖zk ...

  4. 在c#中利用keep-alive处理socket网络异常断开的方法

    本文摘自 http://www.z6688.com/info/57987-1.htm 最近我负责一个IM项目的开发,服务端和客户端采用TCP协议连接.服务端采用C#开发,客户端采用Delphi开发.在 ...

  5. 常见的NoSql系统使用场景分析--转载

    •Cassandra •特性:分布式与复制的权衡\根据列和键范围进行查询\BigTable类似的功能:列,列族\写比读快很多 •最佳适用:写操作较多,读比较少的时候.如果你的系统都是基于Java的时候 ...

  6. java socket 网络编程常见异常

    1.java.net.SocketTimeoutException 这个异常比较常见,socket超时.一般有2个地方会抛出这个,一个是connect的时候,这个超时参数由connect(Socket ...

  7. Java Socket网络编程常见异常(转)

    1.java.net.SocketTimeoutException 这个异常比较常见,socket超时.一般有2个地方会抛出这个,一个是connect的时候,这个超时参数由connect(Socket ...

  8. JVM之调优及常见场景分析

    JVM调优 GC调优是最后要做的工作,GC调优的目的可以总结为下面两点: 减少对象晋升到老年代的数量 减少FullGC的执行时间 通过监控排查问题及验证优化结果,可以分为: 命令监控:jps.jinf ...

  9. MySQL死锁系列-常见加锁场景分析

    在上一篇文章<锁的类型以及加锁原理>主要总结了 MySQL 锁的类型和模式以及基本的加锁原理,今天我们就从原理走向实战,分析常见 SQL 语句的加锁场景.了解了这几种场景,相信小伙伴们也能 ...

随机推荐

  1. 【SCOI2007】组队(单调性)

    题目链接 大意 给定\(N\)个人与三个常量\(A,B,C\),每个人有两个属性:\(Hi\),\(Vi\). 现要让你选些人出来,定义\(Hmin\)为选出来的这些人中最小的\(Hi\)值,\(Vm ...

  2. Java中的多线程你只要看这一篇就够了(引用)

    引 如果对什么是线程.什么是进程仍存有疑惑,请先Google之,因为这两个概念不在本文的范围之内. 用多线程只有一个目的,那就是更好的利用cpu的资源,因为所有的多线程代码都可以用单线程来实现.说这个 ...

  3. Keycloak 团队宣布他们正在弃用大多数 Keycloak 适配器,包括Spring Security和Spring Boot

    2月14日,Keycloak 团队宣布他们正在弃用大多数 Keycloak 适配器. 其中包括Spring Security和Spring Boot的适配器,这意味着今后Keycloak团队将不再提供 ...

  4. 学习Spring5必知必会(1)~未使用spring前的麻烦

    一.未使用spring前的麻烦 开闭原则:扩展是开放的,但是对于修改是"封闭的". 1.代码耦合度比较高[不符合开闭原则]: public class EmployeeServic ...

  5. 从 MMU 看内存管理

    在计算机早期的时候,计算机是无法将大于内存大小的应用装入内存的,因为计算机读写应用数据是直接通过总线来对内存进行直接操作的,对于写操作来说,计算机会直接将地址写入内存:对于读操作来说,计算机会直接读取 ...

  6. 《PHP程序员面试笔试宝典》——签约和违约需要注意哪些事情?

    本文摘自<PHP程序员面试笔试宝典>. PHP面试技巧分享,PHP面试题,PHP宝典尽在"琉忆编程库". 经过了紧张激烈的笔试面试后,最后过五关斩六将,终于得到了用人单 ...

  7. GCC 使用库文件名进行链接

    使用 GCC 进行 C/C++ 代码编译时,如果代码中使用到了库函数,需要使用 -l 选项指定该库函数所在的库.如:-lm.-lrt.-lpthread等.这种方式使用的是库的缩写.一个库的文件名如果 ...

  8. CentOS 7 编译部署LAMP环境

    文章目录 1.需求以及环境准备 1.1.版本需求 1.2.环境准备 1.3.安装包准备 2.编译升级Openssl 2.1.查看当前Openssl版本 2.2.备份当前版本Openssl文件 2.3. ...

  9. python中随机生成整数

    1 #可以多运行几次,看看结果是不是随机生成的~ 2 3 import random 4 #调用random模块,与 5 a = random.randint(1,100) 6 # 随机生成1-100 ...

  10. CMake 交叉编译

    CMake 交叉编译 交叉编译就是说在平台 A (宿主机)上编译出可以在平台 B (目标机) 上运行的程序,比如在 x86 上编译 ARM 程序 要交叉编译首先要去下载目标平台的工具链,比如要编译 A ...