美文网首页
Java大文件下载拷贝过程优化

Java大文件下载拷贝过程优化

作者: 西5d | 来源:发表于2020-07-26 17:00 被阅读0次

背景

最近有接触到大文件下载,且正好看了内核内存映射文件的相关内容,在实际使用中也踩了一些坑,在这里简单做个记录总结。言归正传,开始今天的内容。

内容介绍

首先说下场景,在一般的请求中,比如返回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));
        }
    }

方法中提供了三种写入方式,都大同小异,其中最后一种用FileChanneltransferFrom()方法,内部也是用添加缓存的方式来处理(注意不同版本可能不一致)。还有一点是这里的buffer大小最好1MB就足够,因为一般读取到buffer的内容,大小也限制在8192字节。

特别说明:
提到的FileChanneltransferFrom()版本不同不一致的问题, 亲测在jdk1.8.0_144 版本内部实现buffer是DirectBuffer,而在jdk1.8.0_201,则是heapBuffer。

总结

篇幅有限,对于一些HTTP请求返回的细节,没有全部写出来,感兴趣的同学可以深入了解下相关内容,对于理解内容非常有帮助。感谢阅读。

相关文章

网友评论

      本文标题:Java大文件下载拷贝过程优化

      本文链接:https://www.haomeiwen.com/subject/iogwlktx.html