一.断点续传
1.1原理
“断点续传”最基础的原理,用大白话说就是:我们要在下载行为出现中断的时候,记录下中断的位置信息,然后在下次行为中读取。
有了这个位置信息之后,想想我们该怎么做。是的,很简单,在新的下载行为开始的时候,直接从记录的这个位置开始下载内容,而不再从头开始,当“上传(下载)的行为”出现中断,我们需要记录本次上传(下载)的位置(position)。
当“续”这一行为开始,我们直接跳转到postion处继续上传(下载)的行为。
那么转换到所谓的“断点续传”,我们该使用什么来衡量“position”呢?
很显然,回归二进制,因为这里的本质无非就是文件的读写。那么剩下的工作就很简单了,先是记录position,因为只是数据的持久化而已(内存,文件,数据库),我们有很多方式。另一个关键在于当“续传”的行为开始,我们需要从上次记录的position位置开始读写操作,则需要一个类似于“指针”功能的东西。我们当然也可以自己想办法去实现这样一个“指针”,但高兴地是,Java已经为我们提供了这样的一个类,那就是RandomAccessFile。
这个类的功能从名字就很直观的体现了,能够随机的去访问文件。我们看一下API文档中对该类的说明:
此类的实例支持对随机访问文件的读取和写入。随机访问文件的行为类似存储在文件系统中的一个大型 byte 数组。
如果随机访问文件以读取/写入模式创建,则输出操作也可用;输出操作从文件指针开始写入字节,并随着对字节的写入而前移此文件指针。
1.2举例
假设我们现在有一款“通关制的RPG游戏”。想想我们在玩这类游戏时通常会怎么做?
很明显,第一天我们浴血奋战,大杀四方,假设终于来到了第四关。虽然激战正酣,但一看墙上的时钟,已经凌晨12点,该睡觉了。
这个时候就很尴尬了,为了能够在下一次玩的时候,顺利接轨上我们本次游戏的进度,我们应该怎么办呢?
很简单,我们不关掉游戏,直接去睡觉,第二天再接着玩呗。这样是可以,但似乎总觉着有哪里让人不爽。
那么,这个时候,如果这个游戏有一个功能叫做“存档”,就很关键了。我们直接选择存档,输入存档名“第四关”,然后就可以关闭游戏了。等到下次进行游戏时,我们直接找到“第四关”这个存档,然后进行读档,就可以接着进行游戏了。
这个时候,所谓的“断点续传”就很好理解了。我们顺着我们之前“玩游戏”的思路来理一下:
假设,现在有一个文件需要我们进行下载,当我们下载了一部分的时候,出现情况了,比如:电脑死机、没电、网络中断等等。
其实这就好比我们之前玩游戏玩着玩着,突然12点需要去睡觉休息了是一个道理。OK,那么这个时候的情况是:
- 如果游戏不能存档,那么则意味着我们下次游戏的时候,这次已经通过的4关的进度将会丢失,无法接档。
- 对应的,如果“下载”的行为无法记录本次下载的一个进度。那么,当我们再次下载这个文件也就只能从头来过。
话到这里,其实我们已经发现了,对于我们以上所说的行为,关键就在于一个字“续”!
而我们要实现让一种断开的行为“续”起来的目的,关键就在于要有“介质”能够记录和读取行为出现”中断”的这个节点的信息。
1.3代码实现
正常代码
public class Test {
public static void main(String[] args) {
// 源文件与目标文件
File sourceFile = new File("D:/", "test.txt");
File targetFile = new File("E:/", "test.txt");
// 输入输出流
FileInputStream fis = null;
FileOutputStream fos = null;
// 数据缓冲区
byte[] buf = new byte[1];
try {
fis = new FileInputStream(sourceFile);
fos = new FileOutputStream(targetFile);
// 数据读写
while (fis.read(buf) != -1) {
System.out.println("write data...");
fos.write(buf);
}
} catch (FileNotFoundException e) {
System.out.println("指定文件不存在");
} catch (IOException e) {
// TODO: handle exception
} finally {
try {
// 关闭输入输出流
if (fis != null)
fis.close();
if (fos != null)
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
该段代码运行,我们就会发现在E盘中已经成功拷贝了一份“test.txt”。这段代码很简单,唯一稍微说一下就是:
我们看到我们将buf,即缓冲区 设置的大小是1,这其实就代表我们每次read,是读取一个字节的数据(即1个英文字母)。
现在,我们就来模拟这个读写中断的行为,将之前的代码完善如下:
续传代码
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
public class Test {
private static int position = -1;
public static void main(String[] args) {
// 源文件与目标文件
File sourceFile = new File("D:/", "test.txt");
File targetFile = new File("E:/", "test.txt");
// 输入输出流
FileInputStream fis = null;
FileOutputStream fos = null;
// 数据缓冲区
byte[] buf = new byte[1];
try {
fis = new FileInputStream(sourceFile);
fos = new FileOutputStream(targetFile);
// 数据读写
while (fis.read(buf) != -1) {
fos.write(buf);
// 当已经上传了3字节的文件内容时,网络中断了,抛出异常
if (targetFile.length() == 3) {
position = 3;
throw new FileAccessException();
}
}
} catch (FileAccessException e) {
keepGoing(sourceFile,targetFile, position);
} catch (FileNotFoundException e) {
System.out.println("指定文件不存在");
} catch (IOException e) {
// TODO: handle exception
} finally {
try {
// 关闭输入输出流
if (fis != null)
fis.close();
if (fos != null)
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private static void keepGoing(File source,File target, int position) {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try {
RandomAccessFile readFile = new RandomAccessFile(source, "rw");
RandomAccessFile writeFile = new RandomAccessFile(target, "rw");
readFile.seek(position);
writeFile.seek(position);
// 数据缓冲区
byte[] buf = new byte[1];
// 数据读写
while (readFile.read(buf) != -1) {
writeFile.write(buf);
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
class FileAccessException extends Exception {
}
1.4总结
在Java中对一个文件进行读写操作很简单。假设现在的需求如果是“把D盘的这个文件写入到E盘”,那么我们会提起键盘,啪啪啪啪,搞定!
但其实所谓的文件的“上传(下载)”不是也没什么不同吗?区别就仅仅在于行为由“仅仅在本机之间”转变成了”本机与服务器之间”的文件读写。
- 首先,我们定义了一个变量position,记录在发生中断的时候,已完成读写的位置。(这是为了方便,实际来说肯定应该讲这个值存到文件或者数据库等进行持久化)
- 然后在文件读写的while循环中,我们去模拟一个中断行为的发生。这里是当targetFile的文件长度为3个字节则模拟抛出一个我们自定义的异常。(我们可以想象为实际下载中,已经上传(下载)了”x”个字节的内容,这个时候网络中断了,那么我们就在网络中断抛出的异常中将”x”记录下来)。
- 剩下的就如果我们之前说的一样,在“续传”行为开始后,通过RandomAccessFile类来包装我们的文件,然后通过seek将指针指定到之前发生中断的位置进行读写就搞定了。