首页 > 其他分享 >文件分段上传和下载

文件分段上传和下载

时间:2023-02-20 19:25:41浏览次数:53  
标签:文件 分段 bytes file 分片 new 上传 下载

1、RandomAccessFile简介

RandomAccessFile类是一个随机读取文件数据的java类,常用于分片上传和下载,使用方法和InputStream类似,不同之处在与其构造方法,需要传入mode,mode有四种,"r"、"rw"、"rwd"、"rws"

r 只读模式,进行写操作会报IO异常。
rw 读写模式,不过是写入到buffer,除非满了或者close、sync,才会写入到文件。
rws 同步读写模式,包括元数据,每次操作都会同步到文件中。
rwd 同步读写模式,不包含元数据,只是正文的更新,会每次同步到文件中。
rws和rwd模式相对安全(比如意外jvm退出,没有来得及close的时候,rw模式对文件的修改无效),但是性能上来说要比rw模式要差(每次同步性能肯定比较差)

注:文件的数据包含两部分,分别是实际数据(正文)和元数据,实际数据很好理解,就是文件实际记录的东西,元数据表示文件的一些其他信息,比如:文件名、文件类型、大小、节点号、权限、所有者、所属组、链接数、时间等等

2、文件分片下载

首先需要知道下载文件的总大小,然后逻辑处理需要分成几片子文件,将所有子文件下载完成后,再合并成一个完整的文件,中间部分文件下载失败时,可以重试下载。

2.1、优缺点分析

优点:a、并发从不同的后端服务获取不同的分片文件,在带宽未受限制的情况下,可以提升下载速率。

b、如果中间某一片下载失败,其他下载成功的不需要重新下载,只需要对失败的分片文件进行下载重试。

缺点:a、代码的复杂度上升。

b、若是单线程调用分片下载接口,由于调用接口次数增多,并且还需要合并,所以下载速率反而会下降。

2.2、请求头和响应头参数

从上面知道,分段下载需要知道文件总大小,才好分片,所以需要约定一些参数,通用做法如下:

  • Request Header参数说明:

range:值的格式为 bytes={start}-{end},{start}从0开始,0表示第一个byte,例子:bytes=10000-20000 ,表示从10000个byte读取到第20000个byte,{start}可以为空,例如:bytes=-20000,表示从第一个byte读取到20000个byte,{end}可以为空,例如bytes=10000-,表示读取10000个byte开始后面所有的数据。

  • Response Header参数说明

content-range:格式为 bytes {range}/{size} ,例子bytes 1000-2000/12000,其中{range}为request传入的参数{start-end},{size}为下载的文件总的大小

content-length:下载的分段文件的大小,例如1000,即为起始值和初始值的差值

2.3、代码实现

private void randomReadBytes(File file, Integer start, Integer length, OutputStream os) throws IOException {
 
        //以只读模式分段获取文件
        try (RandomAccessFile rf = new RandomAccessFile(file, "r")) {
 
            //定位到要读取的文件位置,包含该位置的字节
            rf.seek(start);
 
            //创建读取数据的缓冲区
            int len = 2 * 1024;
            byte[] bytes = new byte[len];
 
            //计算循环次数和最后一次读取的比特数
            int remainder = length % len;
            int times = remainder == 0 ? length / len : length / len + 1;
 
            //循环取数据
            for (int i = 1; i <= times; i++) {
 
                //最后一次循环需要特殊处理
                if (i == times && remainder > 0) {
                    rf.read(bytes, 0, remainder);
                    os.write(bytes, 0, remainder);
                } else {
                    rf.read(bytes, 0, len);
                    os.write(bytes, 0, len);
                }
            }
        }
    }

3、文件分片上传

文件被分割成多个子文件上传,后端接受到所有的分片文件后进行合并,可以实现断点续传的功能。

3.1、优缺点分析

优点:a、并发向不同的服务器上传不同的分片文件,在带宽未受限制的情况下,可以提升上传速率。

b、如果中间某一片上传失败,其他已经上传成功的不需要重新上传,只需要对失败的分片文件进行上传重试,其实就是断点续传的功能。

缺点:a、代码复杂度上升。

b、并且在分布式系统中,需要考虑并发的问题,多个分片文件上传到服务器后,需要再次读取合并,增加了服务器IO。

3.2、实现方案

1、创建分片上传事件,告知服务器要上传的文件名称、大小和总的分片数,返回一个用于此次分片上传的唯一Id和过期时间。

2、上传分片文件,携带第一步获取的唯一Id和当前分片的序号。

3、合并分片文件,携带第一步获取的唯一Id。

4、放弃分片上传,携带第一步获取的唯一Id.

时序图如下:

3.3、合并文件代码实现

  • 利用字节流按顺序合并文件
private static void mergeFileByFileStream(List<String> sourceFilePaths, File targetFile) throws IOException {
        try (OutputStream os = Files.newOutputStream(targetFile.toPath())) {
            for (String filePath : sourceFilePaths) {
                try (InputStream is = Files.newInputStream(new File(filePath).toPath(), StandardOpenOption.READ)) {
                    byte[] bytes = new byte[2048];
                    int readCount;
                    while ((readCount = is.read(bytes)) > 0) {
                        os.write(bytes, 0, readCount);
                    }
                }
            }
        }
    }
  • 如果是字符或者字符串文件,可以用buffer合并文件
private static void merFileByBuffer(List<String> sourceFilePaths, File targetFile) throws IOException {
        try (BufferedWriter writer = Files.newBufferedWriter(targetFile.toPath(), StandardOpenOption.WRITE)) {
            for (String filePath : sourceFilePaths) {
                File file = new File(filePath);
                if (file.exists() && file.isFile()) {
                    try (BufferedReader reader = Files.newBufferedReader(file.toPath())) {
                        String line;
                        while ((line = reader.readLine()) != null) {
                            writer.write(line);
                        }
                    }
                }
            }
        }
    }
  • 用NIO的Channel,transerFrom和transerTo本质没什么区别,但是transerTo,对于文件大小有2G限制,对于socketChannel有8M的限制。
private static void mergeFileByChannel(List<String> sourceFilePaths, File targetFile) throws IOException {
        try (FileChannel oc = new FileOutputStream(targetFile).getChannel()) {
            for (String filePath : sourceFilePaths) {
                File file = new File(filePath);
                if (file.exists() && file.isFile()) {
                    try (FileChannel ic = new FileInputStream(file).getChannel()) {
                        oc.transferFrom(ic, oc.size(), ic.size());
                    }
                }
            }
        }
    }
  • RandomAccessFile实现,如果知道每个子文件在合并文件中的字节起止位置,可以采用多线程的方式向合并文件中同时写入数据,提高文件合并速度。
private static void mergeFileByRandomAccessFile(List<String> sourceFilePaths, File targetFile) throws IOException {
        //根据名称排序
        sourceFilePaths = sourceFilePaths.stream().sorted(String::compareTo).collect(Collectors.toList());
        for (String filePath : sourceFilePaths) {
            File file = new File(filePath);
            if (file.exists() && file.isFile()) {
                try (FileInputStream inputStream = new FileInputStream(file);
                     RandomAccessFile accessFile = new RandomAccessFile(targetFile, "rw")) {
                    byte[] bytes = new byte[2048];
                    accessFile.seek(accessFile.length());
                    int readCount;
                    while ((readCount = inputStream.read(bytes)) != -1) {
                        accessFile.write(bytes, 0, readCount);
                    }
                }
            }
        }
    }

标签:文件,分段,bytes,file,分片,new,上传,下载
From: https://www.cnblogs.com/zhaodalei/p/17137081.html

相关文章