背景
最近有接触到大文件下载,且正好看了内核内存映射文件的相关内容,在实际使用中也踩了一些坑,在这里简单做个记录总结。言归正传,开始今天的内容。
内容介绍
首先说下场景,在一般的请求中,比如返回html网页内容或者json数据,都放到请求返回的body中,这种也是字符数据,比较容易好处理,直接拿到结果就达到目的了。但是,如果要下载一个超级大的文件,比如一个系统镜像,一部电影。如果是java处理,直接等全部返回内容放到内存堆是不合理,也不一定能实现的,所以正常的流程是分批的处理请求到的数据。
常见方法
1. 堆内存拷贝
首先,第一种方式,获取到请求返回的输入流,然后构建一个内存buffer数组,每次从流中将数据写入数组,然后再让文件流从数组中读取,写入到文件中。比如如下常见的操作。
//in http response
//out file out
byte[] buffer = new byte[1024];
int l;
int off = 0;
while ((l = in.read(buffer)) > 0){
out.write(buffer, off+=l, l);
}
2. NIO
上面的方式当然也可以,但是每次操作多了堆内存拷贝,其实效率上可以更优化一些。有些同学这里会想到用直接内存directBuffer
,要使用直接内存就得用java NIO channel
对应的API,如此修改后的代码就少了从堆内存中继拷贝的过程。
代码展示
public static void download(String url, String file, Map<String,String> headers) {
ReadableByteChannel byteChannel = null;
RandomAccessFile accessFile = null;
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024);
long start = 0;
try {
File f = new File(file);
HttpGet get = new HttpGet(new URI(url));
RequestConfig config = RequestConfig.custom().setConnectTimeout(5000).setConnectionRequestTimeout(600000).build();
get.setConfig(config);
for (Map.Entry<String, String> e : headers.entrySet()) {
get.setHeader(e.getKey(), e.getValue());
}
CloseableHttpResponse response = getHttpClient().execute(get);
if (response.getStatusLine().getStatusCode() >= HttpStatus.SC_MULTIPLE_CHOICES) {
System.out.println("下载失败,返回:" + response.getStatusLine());
return;
}
start = System.currentTimeMillis();
System.out.println("start:" + start);
HttpEntity entity = response.getEntity();
InputStream in = entity.getContent();
byteChannel = Channels.newChannel(in);
accessFile = new RandomAccessFile(f, "rw");
FileChannel fileChannel = accessFile.getChannel();
// while ((byteChannel.read(buffer)) != -1) {
// buffer.flip();
// fileChannel.write(buffer);
// buffer.clear();
// }
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, entity.getContentLength());
while ((byteChannel.read(buffer) != -1)){
buffer.flip();
mappedByteBuffer.put(buffer);
buffer.clear();
}
// fileChannel.transferFrom(byteChannel,0, entity.getContentLength());
response.close();
} catch (Exception e) {
log.error("download file error.", e);
} finally {
try {
if (null != byteChannel) {
byteChannel.close();
}
} catch (Exception ignore) {
}
try {
if (null != accessFile) {
accessFile.close();
}
} catch (Exception ignore) {
}
System.out.println("end:" + (System.currentTimeMillis() - start));
}
}
方法中提供了三种写入方式,都大同小异,其中最后一种用FileChannel
的transferFrom()
方法,内部也是用添加缓存的方式来处理(注意不同版本可能不一致)。还有一点是这里的buffer大小最好1MB就足够,因为一般读取到buffer的内容,大小也限制在8192字节。
特别说明:
提到的FileChannel
的transferFrom()
版本不同不一致的问题, 亲测在jdk1.8.0_144 版本内部实现buffer是DirectBuffer,而在jdk1.8.0_201,则是heapBuffer。
总结
篇幅有限,对于一些HTTP请求返回的细节,没有全部写出来,感兴趣的同学可以深入了解下相关内容,对于理解内容非常有帮助。感谢阅读。
网友评论