需求是通过Feign下载一个文件,然后将文件转成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的,是没问题的。。。读取本地文件的InputStream的时候available总是返回总长度的。这可能和本地文件流的实现有关。当时通过网络下载的文件流就有问题了。
inputStream.available()有时候不会返回InputStream的总长度。例如网络请求的InputStream会因为网络延迟等原因导致读取流时会出现读取阻塞,这时候available()返回的就不是总长度了。
为什么网络延迟会导致InputStream阻塞在之前的认知里面,网络通讯时进行系统调用,参数是一个数据的指针例如linux的 int write(int sockfd, char *buf, int len);(参考Linux网络编程),那么一个HTTP请求直接全部丢给操作系统,操作系统会自动分包发送,并且接收方接收完成再排序好了再返回给应用的,阻塞不是操作系统的事情吗?阻塞的时候应用是什么都没读取到的,这时候还没有轮到available()方法的执行呢,接收完成后(即数据准备好了)应用才能系统调用读取数据,读取到的数据就是整个发送方发送的所有数据了,这时候应用为啥还会阻塞呢??不都拿到所有数据了吗?
不懂就去学习,原来TCP的头部长度是是固定的也就是能分的包也是有数量限制的,所以 int write(int sockfd, char *buf, int len);传入的数据长度是有限制的,所有一个HTTP请求的全部内容不会只有一次系统调用。草率了肤浅了。
网络延迟会导致InputStream阻塞是因为一个HTTP请求对应多个TCP请求,当HTTP请求头的所有数据达到后就会将HTTP请求体封装成一个流,这个流对应的请求体可能没有全部到达,所以当请求头接受完成后会就会进入程序处理,就是进入编写代码的地方,这时候请求体对应的流有可能没有完全到达,所以读取流的时候就会阻塞。
TCP基于字节流传输的,不管应用层传什么内容都当作无差别的字节流传输到目的地,而且为了传输效率会做缓存,如果应用层发过来的长度没到长度缓存要求会超时等待下一个包一起发送,所以会出现出现粘包问题。HTTP通过一个请求头Content-Length参数设置HTTP的长度做TCP传输的界限的确保不会粘包的。比如TCP发生粘包即通过Content-Length参数进行切割区别是哪个请求的数据。所以是通过Content-Length的值判断当前HTTP请求数据是否全部达到的,即流是否结束。
源码看不懂只能用过实验验证了
实验如下
通过springboot编写一个post请求请求体随便写,然后循环读取请求体的流。之后用postman请求头Content-Length设置大点,预期结果是改请求会一直卡在for 循环读取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长度的小心了,设置过多了就会出现请求超时咯。邪恶的去测试公司的接口,通过postman调接口把Content-Length设置得超级长,预期会出现接口超时的情况,拿去恶搞同事让他一脸懵逼找不到bug,岂不是很……。得意的去点发现没超时,惊讶,难道我的分析是错的?然后再次请求即超时了??搞得我一脸懵逼。难道是spring boot做反序列化的时候不读等读完流的数据再返回?spring boot是基于jackson做反序列化的,去翻一下源码。翻了半天搞不懂,源码太复杂了,实在是看得头疼,通过猜想——>实验验证的方法探究吧,以后看懂源码了再通过源码验证。猜想了很多不写历程了写猜想合理的结果,
猜想反序列化的时候是不用读取流的所有内容的,可能读取某一个符号作为结束的标记,JSON的结束标记当然是括号}
那我就将请求体里面的JSON字符串的括号}去掉,如果Content-Length长度设置超级长请求超时了,如果Content-Length正常就报jackson反序列化错误。果然不出我所料。
结论:@RequestBody不阻塞是因为jackson做反序列化的时候只读取括号{}里面的数据,然后反序列化后返回,不读取完整的流,但是流还是没关闭的,之后同一个客户端下一次请求当作是这个流的数据所以就超时了。



