文件分段上传和下载
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,transferFrom和transferTo本质没什么区别,但是transferTo,对于文件大小有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); } } } } }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通