# FTP(文件传输协议的简称)(File Transfer Protocol、 FTP)客户端软件断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载。用户可以节省时间,提高速度。
2. RandomAccessFile类
2.1 读写的函数
package com.liu.learn;
import org.junit.Test;
import java.io.File;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
/**
* 读写函数
* @author lms
* @date 2021-12-25 - 19:43
*/
public class RandomAccessFileTest {
@Test
public void read() throws Exception {
File file = new File("D:/E_DISK/IDEAJavaCode/BreakingPointFile/a.txt");
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
// 默认读取一个字节
randomAccessFile.read();
// 相对路径,跳过前3个字节开始读取数据信息
// randomAccessFile.skipBytes(3);
// 绝对路径(从文件头开始读取数据)
randomAccessFile.seek(3);
byte[] bytes = new byte[1024];
// 返回读取到的字节数
int len = randomAccessFile.read(bytes);
System.out.println("len = " + len);
System.out.println("str = " + new String(bytes, 0, len));
}
@Test
public void write() throws Exception {
File file = new File("D:/E_DISK/IDEAJavaCode/BreakingPointFile/a.txt");
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
String str = "zhangsan";
randomAccessFile.write(str.getBytes(StandardCharsets.UTF_8));
}
}
2.2 单线程的文件复制
package com.liu.learn;
import org.junit.Test;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* @author lms
* @date 2021-12-25 - 20:25
* 实现多线程的文件复制操作
*/
public class RandomAccessFileTest2 {
/**
* 测试单线程的文件复制
*/
@Test
public void SingleFile() {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
File file = new File("C:/Users/Administrator/Desktop/PCQQ2021.exe");
fis = new FileInputStream(file);
fos = new FileOutputStream("D:\\360Downloads\\xxx.exe");
// 一次读取8k字节
byte[] bytes = new byte[1024 * 8];
int len = -1;
long start = System.currentTimeMillis();
while ((len = fis.read(bytes)) != -1) {
fos.write(bytes, 0, len);
}
long end = System.currentTimeMillis();
System.out.println("总耗时: " + (end - start));
System.out.println("文件复制完成~~~");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
2.3 多线程下的文件复制
package com.liu.learn;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
/**
* 使用RandomAccessFile实现多线程文件的复制
*
* @author lms
* @date 2021-12-25 - 20:45
*/
public class RandomAccessFileTest3 {
public static void main(String[] args) {
File file = new File("C:/Users/Administrator/Desktop/PCQQ2021.exe");
// 用于存放线程
ArrayList<Thread> threadArrayList = new ArrayList<>();
// 线程的数量
int threadNum = 5;
// 计算文件的总长度
long length = file.length();
// 每个线程分得的长度(向上取整,因为randomAccessFile会对重叠的部分进行合并整合)
int partLen = (int) Math.ceil(length / threadNum);
// 创建线程
for (int i = 0; i < threadNum; i++) {
final int k = i;
Thread thread = new Thread(() -> {
// 每个线程需要做的事情
try {
// 进行文件的读写
RandomAccessFile rafIn = new RandomAccessFile(file, "r");
RandomAccessFile rafOut = new RandomAccessFile("D:/360Downloads/xxx.exe", "rw");
// 计算每个线程读取文件的开始位置
rafIn.seek(k * partLen);
// 每个写文件的开始位置
rafOut.seek(k * partLen);
int plen = 0, len = -1;
// 每次读取的文件大小为 8k
byte[] bytes = new byte[1024 * 8];
// 不断的读取,写文件
while (true) {
// 记录当前读取的字节数
len = rafIn.read(bytes);
// -1说明整个文件已经被读取完毕
if (len == -1) {
break;
}
// 否则记录每次读取的字节数,并将其累加到plen中
plen += len;
rafOut.write(bytes, 0, len);
// 如果plen大于partLen,说明当前线程要写的文件长度已经达到,就执行退出操作
if (plen >= partLen) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
});
// 开启线程
thread.start();
threadArrayList.add(thread);
}
long start = System.currentTimeMillis();
// 阻塞主线程
for (Thread thread : threadArrayList) {
// 等所有的线程结束之后,main线程才能继续执行
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println("总耗时: " + (end - start));
System.out.println("文件复制完成~~~");
}
}
2.4 断点续传的实现
package com.liu.learn;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.StringJoiner;
import java.util.concurrent.ConcurrentHashMap;
/**
* 基于多线程的断点续传方法
*
* @author lms
* @date 2021-12-25 - 20:45
*/
public class RandomAccessFileTest4 {
public static void main(String[] args) throws Exception {
File file = new File("C:/Users/Administrator/Desktop/PCQQ2021.exe");
// 使用ConcurrentHashMap记录每个线程读取到位置
ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>();
// 用于存放线程
ArrayList<Thread> threadArrayList = new ArrayList<>();
// 线程的数量
int threadNum = 5;
// 计算文件的总长度
long length = file.length();
// 每个线程分得的长度(向上取整,因为randomAccessFile会对重叠的部分进行合并整合)
int partLen = (int) Math.ceil(length / threadNum);
// 日记文件
String logFile = "D:/360Downloads/xxx.exe.log";
// 读取日记文件
String[] data = null;
File fl = new File(logFile);
if (fl.exists()) {
BufferedReader reader = new BufferedReader(new FileReader(fl));
String line = reader.readLine();
// 拆分字符串
data = line.split(",");
reader.close();
}
final String[] _data = data;
// 创建线程
for (int i = 0; i < threadNum; i++) {
final int k = i;
Thread thread = new Thread(() -> {
RandomAccessFile log = null;
// 每个线程需要做的事情
try {
// 进行文件的读写
RandomAccessFile rafIn = new RandomAccessFile(file, "r");
RandomAccessFile rafOut = new RandomAccessFile("D:/360Downloads/xxx.exe", "rw");
// 记录读到的日记文件
log = new RandomAccessFile(logFile, "rw");
// 计算每个线程读取文件的开始位置
// 需要从指定的位置开始读取数据,从而实现断点续传
rafIn.seek(_data == null ? k * partLen : Integer.parseInt(_data[k]));
// 每个写文件的开始位置
// 从指定的位置开始写输入数据
rafOut.seek(_data == null ? k * partLen : Integer.parseInt(_data[k]));
int plen = 0, len = -1;
// 每次读取的文件大小为 8k
byte[] bytes = new byte[1024 * 8];
// 不断的读取,写文件
while (true) {
// 记录当前读取的字节数
len = rafIn.read(bytes);
// -1说明整个文件已经被读取完毕
if (len == -1) {
break;
}
// 否则记录每次读取的字节数,并将其累加到plen中
plen += len;
// 将读取到的字节数量添加到map中
map.put(k, plen + (_data == null ? k * partLen : Integer.parseInt(_data[k])));
// 将读取到的数据进行写入
rafOut.write(bytes, 0, len);
// 写入完成之后,覆盖之前的log文件
log.seek(0);
// 以逗号进行分割,写入
StringJoiner joiner = new StringJoiner(",");
map.forEach((key, val) -> {
String.valueOf(val);
});
log.write(joiner.toString().getBytes(StandardCharsets.UTF_8));
// 如果plen大于等于下一个线程的开始位置,说明当前线程要写的文件长度已经达到,就执行退出操作
if (plen + (_data == null ? k * partLen : Integer.parseInt(_data[k])) >= (k + 1) * partLen) {
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (log != null) {
try {
log.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
});
// 开启线程
thread.start();
threadArrayList.add(thread);
}
long start = System.currentTimeMillis();
// 阻塞主线程
for (Thread thread : threadArrayList) {
// 等所有的线程结束之后,main线程才能继续执行
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 线程读取完毕,将日记数据进行删除
fl.delete();
long end = System.currentTimeMillis();
System.out.println("总耗时: " + (end - start));
System.out.println("文件复制完成~~~");
}
}