原创:代码日记,欢迎分享,转载请保留出处。引言在当前微服务的背景下,网络异常越来越普遍,而有些网络异常是非常模糊的。在什么情况下会导致什么异常,目前还很难理解。为此,我做了很多实验。各种不寻常的场景。socket状态转换图先快速回顾一下正常情况下的TCP交互过程和socket状态转换,如下:三向握手客户端调用connect函数,向服务端发送SYN包。客户端状态变为SYN_SENT,服务端收到。改为SYN_RECV,同时回复SYN+ACK包给客户端。client收到SYN+ACK包后,变为ESTABLISHED,同时回复ACK包给server,client的connect函数执行完毕。服务器收到ACK包后,也变为ESTABLISHED状态,至此连接建立。思考:如果服务器没有收到第一个SYN包会怎样?客户端会重新发送SYN包给服务器,服务器收到后会再次发送SYN+ACK给客户端。思考:最后一个ACK包没有收到怎么办?服务器会重新发送SYN+ACK包给客户端,客户端收到后会再次向服务器发送ACK。这里可以发现,在TCP协议中,重传是在没有收到ACK的情况下发生的,纯ACK确认包是不会重传的。数据传输客户端调用写函数发送请求数据,服务端调用读函数接收请求数据。服务端请求处理完成后,服务端调用写函数返回响应数据,客户端调用读函数接收响应数据。思考:如果前面3次握手都丢了ACK,但是client已经是ESTABLISHED状态,调用write发送数据怎么办?write发送的数据包也被标记为ACK,无论哪个先到前面的ACK包,服务器都会变为ESTABLISHED状态。而如果ACK和数据包都没有到达服务器,一段时间后,服务器处于SYN_RECV状态的Socket会自动关闭,不会回复任何数据包给客户端。可以发现,在这个场景下,client认为连接成功了,而server根本就没有连接。当client四次挥手调用close函数时,会向server发送一个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!好了,以上就是TCP的正常流程,下面以Java网络异常为例,说说各种异常情况!常见网络异常场景出现连接超时异常:java.net.SocketTimeoutException:connecttimedoutatjava.net.PlainSocketImpl.socketConnect(NativeMethod)atjava.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)atjava.net。java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)在java.net.SocksSocketImpl.connect(SocksSocketImpl.java.java:392)在java.net.Socket(.java:589)出现这个异常的原因是客户端连接建立连接时,服务端还没有收到SYN包,超过连接超时时间就会报这个异常。也有可能服务端收到了一个SYN包,但是SYN+ACK还没有发送给客户端,也会报这个异常。连接被拒绝异常:java.net.ConnectException:连接被拒绝(连接被拒绝)在java.net.PlainSocketImpl.socketConnect(NativeMethod)在java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350)在java.net.AbstractPlainSocketImpl。connectToAddress(AbstractPlainSocketImpl.java:206)在java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188)在java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)在java.net.Socket.connect(Socket.java:589)出现这个异常的原因是当服务端没有监听某个端口的程序时,当客户端试图连接到这个端口时就会出现这个异常。其实质就是服务器回复一个RST包。注意:RST报文在TCP协议中用于处理异常情况。一般情况下,接收方收到RST包后,会直接回收Socket资源,不需要经过四次挥手的过程。read读取超时时出现异常:java.net.SocketTimeoutException:Readtimedoutatjava.net.SocketInputStream.socketRead0(NativeMethod)atjava.net.SocketInputStream.socketRead(SocketInputStream.java:116)atjava.net。套接字输入流。read(SocketInputStream.java:171)atjava.net.SocketInputStream.read(SocketInputStream.java:141)socket.read()从对端读取数据时,如果等待数据超时,会报Readtimedout。.服务器处理太慢和网卡太慢,导致数据包无法传输。大多数情况下,这种异常是由于服务器处理速度太慢造成的。可以通过socket.setSoTimeout()修改超时时间,注意理解这个超时时间,不是整个读取过程的时间,而是没有任何数据通信的空闲时间。写重传超时一般情况下,因为socket有写缓冲区(发送缓冲区),write方法不会阻塞,立即返回,但是如果写入大量数据(比如文件上传),write方法仍然会阻塞,当发送缓冲区已耗尽。无论write方法是否被阻塞,多次数据重传失败都会引发异常。不同的是,阻塞的写是被异常打断的,而写不阻塞的时候,下次写的时候会抛出异常。对于这种情况下的异常信息,不同的操作系统表现不同,如下:在Linux上,会抛出如下异常,同时会关闭本地Socket,不会向对端发送数据包。发生异常:java.net.SocketException:连接超时(写入失败)在java.net.SocketOutputStream.socketWrite0(NativeMethod)在java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111)在java.net.SocketOutputStream.write(SocketOutputStream.java:143)在Windows上,抛出如下异常,同时关闭本地Socket,不会向对端发送数据包。发生异常:java.net.SocketException:Connectionresetbypeer:socketwriteerroratjava.net.SocketOutputStream.socketWrite0(NativeMethod)atjava.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111)atjava.net。SocketOutputStream.write(SocketOutputStream.java:143)重传次数,linux默认15次,可以通过内核参数net.ipv4.tcp_retries2配置,windows默认5次,可以配置通过注册表项TcpMaxDataRetransmissions。总而言之,写超时可能会导致Connectiontimedout(Writefailed)异常或者Connectionresetbypeer异常(在Windows上)。读写时接收对方的RST包。一般来说,如果对端机器上的连接不存在,调用write向它发送数据包,对端回复一个RST包终止连接。注意:什么时候连接不存在?比如机器直接断电重启,或者网络包被路由到错误的机器等,机器上不会有对应的TCP连接。而在write/read阻塞时,收到对方的RST包,或者先收到对方的RST包,再write/read,就会报Connectionresetexception。如果对端机器上的连接不存在,当本端不断调用write/read时,在不同的操作系统上会产生不同的异常序列,如下:在Linux或Windows上先写,然后一直读,表现如下:#第一次写入,调用正常,对端返回RST包#第二次读取,抛出连接重置异常:发生异常:java.net.SocketException:Connectionresetatjava.net。SocketInputStream.read(SocketInputStream.java:210)atjava.net.SocketInputStream.read(SocketInputStream.java:141)#第三次读取,抛出connectionresetexception:exceptionoccurred:java.net.SocketException:Connectionresetatjava.net.SocketInputStream.read(SocketInputStream.java:210)atjava.net.SocketInputStream.read(SocketInputStream.java:141)一直在linux上写,表现如下:#第一次写,调用正常,并且thepeerreturnstheRSTpacket#Thesecondwrite,throwingaconnectionresetexception:发生异常:java.net.SocketException:Connectionresetatjava.net.SocketOutputStream.socketWrite(SocketOutputStream.java:115)atjava.net.SocketOutputStream.write(SocketOutputStream.java:143)#第三次写入,抛出brokenpipe异常:发生异常:java.net.SocketException:Brokenpipe(Writefailed)atjava.net.SocketOutputStream.socketWrite0(NativeMethod)atjava.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111)atjava.net.SocketOutputStream.write(SocketOutputStream.java:143)一直在Windows上写,表现如下:#第一次写,调用是normal,thepeerReturnRSTpackage#第二次写入,抛出Connectionresetbypeerexception:anexceptionoccurred:java.net.SocketException:Connectionresetbypeer:socketwriteerroratjava.net.SocketOutputStream.socketWrite0(NativeMethod)atjava.net。SocketOutputStream.socketWrite(SocketOutputStream.java:111)atjava.net.SocketOutputStream.write(SocketOutputStream.java:143)#第三次写入,抛出Connectionresetbypeer异常:exceptionoccurred:java.net.SocketException:Connectionresetbypeerpeer:socketwriteerroratjava.net.SocketOutputStream.socketWrite0(NativeMethod)atjava.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111)atjava.net.SocketOutputStream.write(SocketOutputStream.java:143)包会导致Connection重置异常,也可能导致Brokenpipe异常对方关闭连接后仍然在读写。如果对方调用close关闭连接,那么如果本端调用read或者write方法读写数据会怎样呢?如果是read第一次调用,linux和windows都会返回-1,表示EOF,如下:如果是write第一次调用,peer会回复一个RST包,如下:如果是连续的write/readcall,在不同的操作系统上表现不同,如下:在Linux上一直写/读,表现如下:#第一次写,调用正常,对端返回RST包#第二次写,一个坏掉管道异常:发生异常:java.net.SocketException:java.net.SocketOutputStream.socketWrite0(NativeMethod)上的java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111)上的java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111).SocketOutputStream.write(SocketOutputStream.java:143)#第三次写入,抛出brokenpipe异常:发生异常:java.net.SocketException:Brokenpipe(Writefailed)atjava.net.SocketOutputStream.socketWrite0(NativeMethod)atjava.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111)atjava.net.SocketOutputStream.write(SocketOutputStream.java:143)#第四次读取,返回-1。在Windows上,write/read已经执行如下:#第一次写,调用正常,对端返回RST包#第一次第二次写,throwSoftwarecausedconnectionabort:socketwriteerrorexception:exceptionoccurred:java.net.SocketExceptionn:软件导致连接中止:java.net.SocketOutputStream.socketWrite0(NativeMethod)上的套接字写入错误143)#第三次write,抛Softwarecausedconnectionabort:socketwriteerror异常:发生异常:java.net.SocketException:Softwarecausedconnectionabort:socketwriteerroratjava.net.SocketOutputStream.socketWrite0(NativeMethod)atjava.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111)atjava.net.SocketOutputStream.write(SocketOutputStream.java:143)#第四次读取,抛软件导致连接中止:recvfailed异常:发生异常:java.net.SocketException:软件导致连接中止:在java.net.SocketInputStream.socketRead0(本机方法)在java.net.SocketInputStream.socketRead(SocketInputStream.java:116)在java.net.SocketInputStream.read(SocketInputStream.java:171)在java.net.SocketInputStream.read(SocketInputStream.java:141)总之,如果对方关闭连接,本端还在写数据,会报BrokenpipeorSoftwarecausedconnectionabort异常注意:如果你直接按Ctrl+c或kill-9杀掉程序,因为只有进程死了,Linux内核还在,内核会发一个FIN包给对端关闭连接。上面已经看到了其他RST场景。大多数异常是由于收到RST包引起的。除了端口没有被监听或者连接不存在这两种情况,都会产生RST包。还有一些特殊情况也会造成RST包,如下:TCP连接队列积压已满。如果连接队列已满,在Linux上会丢弃SYN包,在Windows上会响应RST包。NAT环境下,连接长时间空闲。在目前的公网环境下,除了拥有公网IP的服务器外,基本都是通过NAT转发技术接入网络。如果程序中的tcp连接长时间不通信,NAT设备会断开数据。链接,当连接被重新用于发送数据时,NAT设备会回复一个RST数据包。GFW国家防火墙如果GFW国家防火墙发现数据包中有敏感信息,就会回复RST中断TCP连接。dns污染dns污染会导致dns解析到错误的ip地址,如果对应的ip地址没有监听相关端口,就会回复一个RST包。当socket的recvbuffer中有未读数据时关闭连接如果socket的recvbuffer中有未读数据,则直接调用close(),发送一个RST包给对方。当socket发送缓冲区有未发送数据时关闭连接默认情况下,当socket发送缓冲区有未发送数据时,直接调用close()会阻塞直到数据发送完毕,但如果TCP设置了SO_LINGER选项,关闭将立即完成,并向对方发送一个RST数据包。在NAT环境下,开启TCP快速回收Linux开启tcp_recycle,如果收到的SYN包的时间戳小于上一个包的时间戳,会回复RST包,参考:https://mp.weixin。qq。com/s/你...在使用Linux的NAT功能时,Linux的NAT是通过使用netfilter机制接收outoftcpwindow数据包来实现的。对于outoftcpwindow的包,在通过netfilter时,会被标记为无效,而无效的包是不在ConnectionTrack模块中的,即不处理也不丢弃,直接交给用于进一步处理的协议栈。因此,数据包的源IP地址不会被替换。对端收到数据包后,会发现没有对应的socket连接,会回复一个RST数据包,导致连接断开。参考:https://mp.weixin.qq.com/s/ph...。相关命令如果你也想重现这些网络异常,可以学习iptables和hping3命令实现丢包或者发送指定包(比如RST包),如下:#观察22333端口数据包sudotcpdump-nianyport22333#添加iptables规则,丢弃22333端口上的数据包sudoiptables-tfilter-IINPUT-ptcp-mtcp--dport22333-jDROP#添加iptables规则,丢弃22333端口上除SYN+ACK之外的所有ACK包sudoiptables-tfilter-IINPUT-ptcp-mtcp--dport22333--tcp-flagsSYN,ACKACK-jDROP#删除iptables规则sudoiptables-tfilter-DINPUT-ptcp-mtcp--dport22333--tcp-flagsSYN,ACKACK-jDROP#手动发送RST数据包#-a:源IP地址#-s:源端口号#-p:目标端口号#--rst:启用RST标志#--win:设置tcp窗口大小#--setseq:设置包序列号sudohping3-a10.243.72.157-s22333-p53824--rst--win0--setseq654041264-c110.243.211.45
