美文网首页
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