一文说透connection reset by peer异常

1、前置知识

1.1、TCP首部格式

一文说透connection reset by peer异常_第1张图片
从这张图可以看出IP报文由首部+数据组成,而IP数据又是TCP首部+数据的组成,首部就是对数据的描述,可以称之为元数据。

实际上发送方是一层层封包,也即在每一层加上自己的元数据。而接收方是一层层解包,就是在每一层根据元数据将其拆分,在运输层将其合并,在等待缓冲区填满时一并交给应用层

  • 源端口和目的端口:端口指定唯一的进程,双方通讯时可以通过端口找到进程
  • 序号(seq):占4个字节,范围是[0,2^32-1]。TCP面向字节流传输的,seq序号就是对字节的编号
  • 确认号(ack):占4个字节。期待收到对方下一个报文段的第一个数据字节的序号。比如接收到的报文中的seq为500,那么发送确认报文时ack为501,表示可以从501开始传给我。
  • 数据偏移:占4位。TCP首部占用字节数,也即报文中的数据离报文首字节偏移的长度

下面有6个控制位,说明报文段的性质

  • URG:当URG=1时,表明紧急指针字段有效。告诉系统此报文段有紧急数据,应该尽快传输,具有高优先级,不是按照队列顺序传输。比如给远程的主机发送很长的数据,等待很长的时间,这时可以通过Control+C中断传输,而这个命令就是紧急数据,排在要传输的数据的前面

  • ACK:只有大写的ACK=1时,小写的ack才生效。也即ACK=0时,ack无效。当然TCP规定,在连接建立后必须将ACK置为1

  • PSH:发送方将PSH置为1,立即创建报文段将其发送出去。接收方收到PSH=1的报文段时,会尽快将接收缓冲区的数据推给应用程序,不需要等到缓冲区填满再交给上层

  • RST:当RST=1时,表明TCP连接中出现严重差错,必须释放连接,然后再重新建立连接。RST置为1还用来拒绝一个非法的报文段或拒绝打开一个连接

  • SYN:在连接建立的过程中使用,表示想与对方建立连接。当SYN=1,ACK=0时,表明是一个请求连接的报文。当SYN=1,ACK=1时,表明这是响应报文段,同意与之建立连接。

  • FIN:用来释放一个连接。当FIN=1时,表明数据已经发送完毕,要求关闭连接。

  • 窗口:占用2字节。这是接收方的窗口,是接收方当前可以接收的字节数,而发送方根据此窗口控制发送数据的速度。这个窗口是动态变化的,最终也是接收方缓存的剩余空间决定的。

以上是基础知识,从本文的知识点来说,RST是重中之重。

1.2、三次握手

一文说透connection reset by peer异常_第2张图片

在客户端请求与服务端连接时,要经过三次握手

  • 服务器肯定要先处于监听状态,不然无法监听客户端的连接
  • 客户端发送请求报文:SYN=1,seq=x。表明客户端想要与服务端建立连接。
  • 服务端发送确认报文:SYN=1,ACK=1,seq=y,ack=x+1。表明服务端同意与客户端建立连接。注意:只有ACK=1时,ack才生效
  • 客户端也发送确认报文:ACK=1,seq=x+1,ack=y+1。表明客户端收到服务端的确认报文。
  • 双方建立连接,可以进行数据传输

问题1:seq是否不必要?重要的是SYN和ACK的标识符?肯定不是

  • seq作用在于同步双方的初始序号,Linux一般是提供随机数,而Windows从0开始。
  • 区分新旧连接,重建连接时初始序号的不同可以做区分。

问题2:如果只有两次握手可以吗?不可以,会出现以下问题:

  • 连接状态不确定。服务端处于连接状态,但客户端可能未收到响应报文,处于SYN-SENT状态,导致服务端一直未收到客户端的数据
  • 冗余连接请求。如果服务端发送确认报文,而客户端未响应,等待超时后会重发确认报文给客户端,直到最大的重试次数
  • 可靠性问题。如果客户端未收到确认报文,seq会不一致,加上网络影响,可能会出现丢包

1.3、四次挥手

一文说透connection reset by peer异常_第3张图片

当客户端已经传输完数据后,会主动关闭连接,然后经历四次挥手的过程

  • 客户端主动关闭连接时,发送终止报文给服务端:FIN=1,seq=u。FIN=1是告诉服务器要关闭连接。seq=u表明客户端最后发送的字节号为u
  • 服务端发送确认报文:ACK=1,seq=v,ack=u+1。ACK=1表明服务端知晓你要关闭连接,我会将缓冲区的数据发给你。seq=v表明给你发送的最大的字节序号为v。ack=u+1表明收到客户端的序列号为u
  • 服务端数据发送完成的报文:FIN=1,ACK=1,seq=w,ack=u+1。FIN=1表明数据传输完毕,可以关闭连接了。ACK=1表明是确认报文
  • 客户端收到确认报文后回一个确认报文:ACK=1,seq=u+1,ack=w+1。ack=w+1表明客户端收到了序号到w的字节。
  • 客户端处于TIME_WAIT,等待报文往返时间。然后客户端关闭连接

问题1:为何存在TIME_WAIT状态?

  • 2MSL是报文最大的往返时间,超过这个时间,报文会被丢弃,超时等待是保证服务器收到最后的ACK报文。

问题2:为何要挥手四次?

  • 我们就看下哪次可以省略。第一次挥手是由客户端发送终止报文,无法省略,不然服务器怎么知晓你要关闭连接。
  • 第二次挥手是用于确认收到客户端终止报文的,无法省略。那么如果没有数据要发送,直接发送FIN=1,ACK=1的确认报文给客户端可以吗?当然不行,第二次挥手是给客户端的确认报文,如果加上FIN=1,说明这是服务器的终止报文,就不是确认报文了
  • 第三次挥手是在无数据发送时的服务器终止报文,无法省略。如果省略会导致客户端无法知晓服务器是否将数据传输完毕,然后终止连接
  • 第四次挥手是客户端确认收到服务器的终止报文,无法省略。如果省略,服务器不知道客户端是否收到终止报文,客户端独自关闭连接,然后服务器一直挂着连接,浪费资源。TIME_WAIT也会进一步保证服务器收到最后的ACK报文。

2、异常解析

2.1、抓包工具

winshark是在Windows中的抓包工具,开启捕获操作时,通过过滤可以得到我们想要的抓包数据
winshark

tcp.port==8080,表明抓包TCP连接中端口号为8080的报文

2.2、RST报文

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class ServerDemo {
    public static void main(String[] args) throws Exception {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(8080));
        SocketChannel socketChannel = serverSocketChannel.accept();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        for(;;) {
            Thread.sleep(1000);
            socketChannel.read(byteBuffer);
            socketChannel.write(ByteBuffer.wrap("hello client".getBytes()));
            System.out.println("read success");
        }
    }
}

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class ClientDemo {
    public static void main(String[] args) throws Exception {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress(8080));
        for (int i = 0; i < 2; i++) {
            Thread.sleep(1000);
            socketChannel.write(ByteBuffer.wrap("hello server".getBytes()));
            System.out.println("write success");
        }
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        socketChannel.read(byteBuffer);
        socketChannel.close();
        Thread.sleep(10000);
    }
}

服务器出现异常:
一文说透connection reset by peer异常_第4张图片

通过winshark抓包:有个小疑问:为何所有write方法都会将PSH置1,Linux也是如此。

56590端口是客户端的,8080是服务端的。在这里只需关注控制位(FIN、ACK、RST)
一文说透connection reset by peer异常_第5张图片

为啥客户端会发送RST报文?我们猜一下是由于服务器还有数据发送导致客户端发送RST强制中断连接。那么简化一下代码,看看是否有RST报文出现

下面的代码服务端只是读取一次数据,而客户端只发送一次数据,双方都没有异常抛出。
一文说透connection reset by peer异常_第6张图片
一文说透connection reset by peer异常_第7张图片

客户端调用close:由于客户端要经历四次挥手,但服务端进程已经结束,第三次挥手无法响应,内核会发送RST报文给客户端,强制关闭连接
在这里插入图片描述
客户端未调用close:客户端结束进程,给服务端发送RST报文表明强制关闭连接
在这里插入图片描述

注意:如果一方已经发送RST报文,而另一方还是调用read/write操作,会有异常抛出。否则就没有异常

未调用close的情况下,是不是也有可能是服务端给客户端发送RST报文。给客户端加个睡眠,看看结果如何
由服务端发送RST报文给客户端。
一文说透connection reset by peer异常_第8张图片
在这里插入图片描述

以上代码服务端均未调用close关闭与客户端的连接,如果调用close会不会有RST报文?
一文说透connection reset by peer异常_第9张图片
一文说透connection reset by peer异常_第10张图片
在这里插入图片描述
报文显示有完整地四次挥手动作

小结:从当前的分析来看,如果没有正常调用close方法(服务端和客户端将socketChannel关闭),就结束进程,先结束的一方会发送RST报文给另一方,强制关闭连接

2.3、异常信息来源

一文说透connection reset by peer异常_第11张图片
我们来追踪源码,找到来源

1、异常堆栈中显示最近一次调用是FileDispatcherImpl.read0,这是JNI方法,就从FileDispatcherImpl.c找到read0方法。
read(fd, buf, len)是系统调用,没有任何异常信息输出。可能性在于convertReturnVal函数中。一文说透connection reset by peer异常_第12张图片

2、在IOUtil.c中找到convertReturnVal函数。前面几个判断都是特殊情况,而像我们这种异常会在最后一个else中,找一下JNU_ThrowIOExceptionWithLastError函数
一文说透connection reset by peer异常_第13张图片

3、获取最近一次错误信息,如果有就转换成java的String类型,调用Throw抛出异常

jin_util.c
一文说透connection reset by peer异常_第14张图片
4、errno是C函数库的全局变量,存储最近一次的错误号。strerror是C函数库中的全局函数,根据错误号获取对应的错误信息

jvm.cpp

在这里插入图片描述

os_linux.cpp

一文说透connection reset by peer异常_第15张图片

5、涉及到C函数库,我们从glibc中寻找。当接收到RST报文时,C函数库会转换成ECONNRESET错误信息

errlist.c
一文说透connection reset by peer异常_第16张图片

6、全局变量errno存放最近一次的错误号,如果多线程情况下,errno是不是会被覆盖。

extern:全局标识符

__thread:声明为线程局部变量

attribute_tls_model_ie:指定TLS模型

所以errno是TLS变量,并不会被其他线程覆盖

include/errno.h
在这里插入图片描述

小结:异常关闭连接时会由先结束的一方发送RST报文,如果另一方还继续操作,jvm会从glibc获取最近的错误号,从而得到异常信息,进而抛出Connection reset by peer

你可能感兴趣的:(TCP,connection,reset,by,peer异常,网络,tcp/ip,网络协议,nio)