断点续传:将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载。用户可以节省时间,提高速度。
RandomAccessFile的四种传输模式
- r 以只读的方式打开文本,也就意味着不能用write来操作文件
- rw 读操作和写操作都是允许的
- rws 每当进行写操作,同步的刷新到磁盘,刷新内容和元数据
- rwd 每当进行写操作,同步的刷新到磁盘,刷新内容
断点续传实现思路:将大文件均分成几块后,每个线程分别处理一块数据的读取和写入。每次写入都要更新记录的日志文件,断网或暂定后重新开始传输时,根据日志文件的信息,可以接着读取写入数据,不用重头开始传输
代码如下(多线程实现断点续传):
/**
* 断点续传
*
* @param dataStr 目标文件地址
* @param targetStr 存放地址
*/
public static void breakpointResume(String dataStr, String targetStr) {
File dataFile = new File(dataStr);
long length = dataFile.length();
int threadNum = 4;//指定线程数
//每个线程均分文件大小,且向上取整
long part = (long) Math.ceil(length / threadNum);
//线程减法计数器
CountDownLatch countDownLatch = new CountDownLatch(threadNum);
Instant beginTime = Instant.now();
//记录传输的日志文件
File logFile = new File(targetStr + ".log");
String[] splitData = null;//不是null就需要断点续传
BufferedReader reader = null;
try {
if (logFile.exists()) {
//存在日志文件,需要进行断点续传
reader = new BufferedReader(new FileReader(logFile));
String data = reader.readLine();
splitData = data.split(",");
} else {
//不存在日志文件,创建日志文件
logFile.createNewFile();
}
Map<Integer, Long> maps = new ConcurrentHashMap<>();
for (int i = 0; i < threadNum; i++) {
final int k = i;
System.out.println("线程正在执行任务:" + k);
String[] finalData = splitData;
new Thread(() -> {
RandomAccessFile inFile = null;
RandomAccessFile outFile = null;
RandomAccessFile rafLog = null;
try {
inFile = new RandomAccessFile(dataFile, "r");//读
outFile = new RandomAccessFile(targetStr, "rw");//写
rafLog = new RandomAccessFile(logFile, "rw");//操作日志文件的流
//确定每个线程读取文件的开始和结束的位置,有断点续传就从日志文件取出的位置开始读取
inFile.seek(finalData == null ? k * part : Long.parseLong(finalData[k]));//设置每个线程读取的启始位置
outFile.seek(finalData == null ? k * part : Long.parseLong(finalData[k]));//设置每个线程写入的启始位置
byte[] bytes = new byte[1024 * 10];//每次读取字节大小
int len = -1, allLen = 0;
while (true) {
len = inFile.read(bytes);//从磁盘读取到缓存
if (len == -1) { //数据读完,结束
break;
}
//如果不等于 -1,把每次读取的字节累加
allLen = allLen + len;
//将读取的字节数放入到map中
maps.put(k, allLen + (finalData == null ? k * part : Long.parseLong(finalData[k])));//每个线程的绝对偏移量
outFile.write(bytes, 0, len);//从缓存写入到磁盘
//将map中的字节日志信息数据写入磁盘
StringJoiner stringJoiner = new StringJoiner(",");
maps.forEach((key, value) -> stringJoiner.add(String.valueOf(value)));
//将日志信息写入磁盘
rafLog.seek(0);//覆盖之前的日志信息
rafLog.write(stringJoiner.toString().getBytes("UTF-8"));
/**
* 当前线程读取的内容
* allLen + (k * part)
* 或
* allLen + finalData[k] 日志文件里面的偏移量
* >=
* 下个线程的起始部分((k + 1) * part)
* 当前线程就不再读取写入数据,结束任务
*/
if (allLen + (finalData == null ? k * part : Long.parseLong(finalData[k])) >= (k + 1) * part) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//关流
if (null != outFile && null != inFile && null != rafLog) {
try {
outFile.close();
inFile.close();
rafLog.close();
} catch (IOException e) {
e.printStackTrace();
}
}
countDownLatch.countDown();//减一
}
}).start();
}
//主线程要等到线程计数器归零,再继续往下执行
countDownLatch.await();
Instant endTime = Instant.now();
System.out.println("总耗时:" + (Duration.between(beginTime, endTime).toMillis()) + "毫秒");
//删除日志文件
logFile.delete();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != reader) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
运行如下:
6个多G的资料 >_<
线程正在执行任务:0
线程正在执行任务:1
线程正在执行任务:2
线程正在执行任务:3
总耗时:60039毫秒
CPU 、内存、磁盘都被利用起来了: