由InputStream.available()引发的偶现bug

案情经过

需求是通过Feign下载一个文件,然后将下载接口得到的InputStream文件转成MultipartFile类型然后再调另外一个接口。从Feign返回的InputStream中读取文件流转换成MultipartFile类型过程中会涉及到将InputStream转成OutputStream的操作。由于懒得找所以直接使用了前辈写的工具类,也懒得看实现细节,先把功能实现其他再说。
代码大概是这样的

Response response = xxxFeign.getFile(fileName);
MultipartFile mulFile =MultipartFileUtils.getMulFile(response.body().asInputStream());
Response response1 = xxxFeign.xxx(mulFile);

但是当调试的时候发现这个功能时而好时而报错,有时候什么都不返回,有时候只返回一个损坏的文件,还是随机的,没有规律。
整个过程就做了三件事情,所以要么是下载的文件有问题,要么是转成MultipartFile 有问题,接受MultipartFile 参数的接口有问题。直接调用两个Feign接口都没问题,那肯定是转成MultipartFile 有问题。深入看一下工具类,看到一个代码和之前自己使用的不一样的写法,那当然要研究一下是自己的方法好还是别人的方法好啦,如下。

byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);
outputStream.write(bytes);

平时我读取inputStream都是有循环的,这里竟然没看见循环,这么神奇?inputStream.available()这个方法貌似没用过查一下,一翻源码就发现了一段备注。
返回此输入流下一个方法调用可以不受阻塞地从此输入流读取(或跳过)的估计字节数。拿来当分配长度是不正确的。由InputStream.available()引发的偶现bug_第1张图片
通过咨询知道,原来该工具类之前使用读取本地文件的InputStream的,是没问题的。。。,经过查阅资料可知读取本地文件的InputStream,使用available()方法总是返回总长度的。这可能和本地文件流的实现有关。但是当InputStream是通过网络获取的就有问题了

案情原因

inputStream.available()有时候不会返回InputStream的总长度。例如网络请求的InputStream会因为网络延迟等原因导致读取流时会出现读取阻塞,这时候available()返回的就不是总长度。

为什么网络延迟会导致InputStream阻塞

在之前的认知里面,网络通讯时进行一次系统调用,参数是一个需要通讯数据指针,例如linux的 int write(int sockfd, char *buf, int len);(参考Linux网络编程),那么一个HTTP请求的数据报文直接全部放到一个指针,然后调用该方法,操作系统会将整个HTTP数据报文分包发送,并且接收方操作系统接收完所有TCP包后会自动再重组完成才返回给应用层的,阻塞不是操作系统的事情吗?操作系统阻塞等待TCP包全部到达的时候,因为数据没有完整所以不会返回给应用层的,此时应用层系统调用读取是什么都没读到的,相当于这个HTTP请求还没到达应用层,所以这时候还没有轮到available()方法的执行呢,接收完成后(即数据准备好了),应用才能系统调用读取数据,读取到的数据就是整个发送方发送的所有数据了即整个HTTP数据包,这时候应用为啥还会阻塞呢??不都拿到所有数据了吗?
不懂就去学习,原来TCP的头部长度是是固定的也就是能分的包也是有数量限制的,所以 int write(int sockfd, char *buf, int len);传入的指针指向的数据长度是有限制的,所以一个HTTP请求的全部内容不会只有一次系统调用。草率了肤浅了。
由InputStream.available()引发的偶现bug_第2张图片
网络延迟会导致InputStream阻塞是因为一个HTTP请求对应多个TCP请求,分割后的HTTP数据报文会依次到达应用层,应用层的HTTP协议处理程序会等待数据报文到达完成才会进一步封装成request,然后再往下处理即到我们写的代码处理。但是HTTP协议处理程序接受到请求头所有数据后就将请求体body封装成流,这个流对应的请求体的TCP数据报文可能没有全部到达,之后就直接进入下一步处理即我们写的代码,所以我们读取流的时候可能数据没到就会阻塞等待。

如何判断流全部到达

TCP基于字节流传输的,不管应用层传什么内容都当作无差别的字节流传输到目的地,而且为会做缓存后发送,如果应用层发过来的长度没到长度缓存要求会超时等待下一个数据一起发送,所以会出现出现粘包问题。HTTP通过一个请求头Content-Length参数设置HTTP的长度,做TCP传输的界限的,确保不会粘包的。比如TCP发生粘包即通过Content-Length参数进行切割区别是哪个HTTP请求的数据。所以是通过Content-Length的值判断当前HTTP请求数据是否全部达到的,即流是否结束,是否需要继续阻塞等待数据
源码看不懂只能用过实验验证了
实验如下
通过springboot编写一个post请求接口,方法里面循环读取请求体的流,流结束即返回。用postman设置请求头参数Content-Length设置超大,请求体随便写发送请求。因为Content-Length设置比实际发送的数据多了,所以InputStream会一直阻塞,请求不会返回,因为客户端不会发送Content-Length长度的数据。点取消请求后报Unexpected EOF read on the socket错误。达到预期。
在这里插入图片描述

   @PostMapping("/post")
    public void post(HttpServletRequest httpServletRequest){
        
        InputStream inputStream = null;
        try {
            inputStream = httpServletRequest.getInputStream();
            for (int i = inputStream.read(); i != -1; i = inputStream.read()) {
                
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

扩展阅读@RequestBody为什么不阻塞

根据上面实验,心想如果手动设置Content-Length长度,设置长度比实际发送数据的长度大岂不是会出现请求超时bug。去测试公司的接口,通过postman调接口把Content-Length设置超级长,预期会出现接口超时的情况,然后拿去问该接口开发同事让他一脸懵逼,找不到bug,岂不是很……。
邪恶的去操作发现没超时,惊讶,难道我的分析是错的???当我第一次请求后没超时,再次请求时超时了,惊讶,同事没有一脸懵逼,我自己倒是一脸懵逼。
猜想:可能是spring boot做反序列化的时候不读等读完流的数据就返回了,spring boot是基于jackson做反序列化的,去翻一下源码。翻了半天搞不懂,源码太复杂了,实在是看得头疼,通过实验的验证吧,以后看懂源码了再通过源码验证。
既然是反序列化的时候不读等读完流的数据,那么应该是根据一些符号或者什么字符做为读取结束的标记,即使流中数据没全部读取完成也结束读取,同事的接口是基于@RequestBody的,对应的请求体结构是JSON字符串,那么的结束标记应当就是括号}
那我就将请求体里面的JSON字符串的括号}去掉,将Content-Length设置比实际长度大,请求超时了,Content-Length设置正常就报jackson反序列化错误。果然不出我所料。使用MultipartFile接收文件也类似,文件在请求体里面会有符号标志开始和结束,spring mvc会读取完文件流完成后转成MultipartFile类型传递到用户编码部分,所以如果使用MultipartFile.getInputStream().available()也是会返回正确的结果,因为mvc等待文件流到达完成才进入用户编码的部分。
结论:@RequestBody不阻塞是因为jackson做反序列化的时候只读取第一个括号{}里面的数据,然后反序列化后返回,不读取完整的流,但是流没释放,如果Content-Length设置比实际长度大,因为上一个没读完数据就返回了,所以同一个客户端下一次请求数据会当作是这个流的数据,所以这次请求的流不会有数据或者数据不完整所以要么超时要不报400错误。

你可能感兴趣的:(踩坑记录,java,面试,spring,boot)