Я полагаю, что многие студенты часто задают такой вопрос интервьюеру на собеседовании: как реализовать загрузку с точки останова, то есть когда файл не скачивается, сохранить прогресс и продолжить загрузку в следующий раз. Реализовать эту функцию несложно, просто используйте временный файл для записи текущего хода загрузки, а затем начните загрузку с хода, записанного временным файлом при следующей загрузке, чтобы реализовать функцию.
После того, как вы реализуете вышеуказанные функции, интервьюер может снова спросить: можно ли реализовать многопоточную загрузку с точки останова? Для этой проблемы, на самом деле, если решить первую проблему, решить эту проблему несложно, это не что иное, как разделить размер файла на несколько частей и загрузить разные части в разные потоки. И этот пример решает две вышеупомянутые проблемы.
Вот решения:
1. Размер файла делится на разные куски в соответствии с количеством потоков.
2. Создайте имя файла в соответствии с URL-адресом и одновременно создайте путь загрузки.
3. Создайте начальную позицию и конечную позицию каждого сегмента блока.
4. Создайте несколько потоков и выполните операции 5-7 в каждом потоке.
5. Установите соединение, прочитайте начальную позицию последней загрузки из временного файла и установите диапазон загрузки (начальная позиция, конечная позиция) в заголовке запроса.
6. Откройте загруженный файл, переместите курсор в начальную позицию и запишите в него данные, а также записывайте положение файла во временный файл каждый раз, когда он записывается.
7. После завершения загрузки временный файл удаляется.
После того, как вы реализуете вышеуказанные функции, интервьюер может снова спросить: можно ли реализовать многопоточную загрузку с точки останова? Для этой проблемы, на самом деле, если решить первую проблему, решить эту проблему несложно, это не что иное, как разделить размер файла на несколько частей и загрузить разные части в разные потоки. И этот пример решает две вышеупомянутые проблемы.
Вот решения:
1. Размер файла делится на разные куски в соответствии с количеством потоков.
2. Создайте имя файла в соответствии с URL-адресом и одновременно создайте путь загрузки.
3. Создайте начальную позицию и конечную позицию каждого сегмента блока.
4. Создайте несколько потоков и выполните операции 5-7 в каждом потоке.
5. Установите соединение, прочитайте начальную позицию последней загрузки из временного файла и установите диапазон загрузки (начальная позиция, конечная позиция) в заголовке запроса.
6. Откройте загруженный файл, переместите курсор в начальную позицию и запишите в него данные, а также записывайте положение файла во временный файл каждый раз, когда он записывается.
7. После завершения загрузки временный файл удаляется.
В этой статье просто представлены идеи для объяснения. Существует много места для оптимизации кода, и детали не нужно запутывать. Также можно использовать другие сетевые фреймворки. HttpUrlConnection — один из самых основных сетевых фреймворков, предоставляемых Java. Если вы это понимаете, остальные такие же. .
Полный класс находится на Github:GitHub.com/People’s Eyes Persist…
- import java.io.BufferedInputStream;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.RandomAccessFile;
- import java.net.HttpURLConnection;
- import java.net.URL;
- public class ResumeDownload {
- public static final String DOWNLOAD_URL = "http://7xs0af.com1.z0.glb.clouddn.com/High-Wake.mp3";
- public static final String DOWNLOAD_PARENT_PATH = "D:\\test_resume_download\\hi";
- public static final int THREAD_COUNT = 3;
- public static void main(String[] args) {
- try {
- // Получаем соединение с адресом загрузки
- URL mUrl = new URL(DOWNLOAD_URL);
- HttpURLConnection conn = (HttpURLConnection) mUrl.openConnection();
- // Получаем размер загруженного файла
- int fileLen = conn.getContentLength();
- // Получить имя загруженного файла по ссылке для скачивания
- String filePathUrl = conn.getURL().getFile();
- String fileName = filePathUrl.substring(filePathUrl.lastIndexOf(File.separator) + 1);
- // Генерируем путь загрузки
- String fileDownloadPath = DOWNLOAD_PARENT_PATH + File.separator + fileName;
- // Определяем, существует ли родительский путь, и генерируем его, если он не существует
- File file = new File(fileDownloadPath);
- if (!file.getParentFile().exists()) {
- file.getParentFile().mkdirs();
- }
- // закрываем соединение
- conn.disconnect();
- /**
- *Далее идет многопоточная загрузка.Основной принцип – разделить размер файла на несколько блоков (по количеству потоков).Каждый поток скачивает файлы одинакового размера с разных начальных позиций.В основном через
- Установите параметр Range в HttpUrlConnection, чтобы установить диапазон загрузки каждого потока.
- * setRequestProperty("Range", "bytes=" + startPos + "-" + endPos);
- */
- int blockSize = fileLen / THREAD_COUNT;
- for (int threadId = 1; threadId <= THREAD_COUNT; threadId++) {
- // Получаем начальную и конечную позиции загрузки каждого потока
- long startPos = (threadId - 1) * blockSize;
- long endPos = threadId * blockSize - 1;
- if (threadId == THREAD_COUNT) {
- endPos = fileLen;
- }
- // Затем реализуем логику загрузки в разных потоках
- // Специально реализовано в DownloadThread Runnable
- new Thread(new DownLoadTask(threadId, startPos, endPos, fileDownloadPath, DOWNLOAD_URL)).start();
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
- /**
- * Определенная логика загрузки
- *
- * @author Administrator
- *
- */
- class DownLoadTask implements Runnable {
- public static final String TEMP_NAME = "_tempfile";
- private int threadId; // идентификатор текущего потока
- private long startPos; // Начальное место загрузки
- private long endPos; // Конечная позиция загрузки
- private String fileDownloadPath; // Расположение файла, где хранится загруженный файл
- private String downloadUrl; // ссылка для скачивания
- private String tempFilePath; // Временный путь к файлу для записи прогресса
- public DownLoadTask(int threadId, long startPos, long endPos, String fileDownloadPath, String downloadUrl) {
- super();
- this.threadId = threadId;
- this.startPos = startPos;
- this.endPos = endPos;
- this.fileDownloadPath = fileDownloadPath;
- this.downloadUrl = downloadUrl;
- this.tempFilePath = fileDownloadPath + TEMP_NAME + threadId;
- }
- @Override
- public void run() {
- try {
- // записываем время начала загрузки
- long startTime = System.currentTimeMillis();
- URL mUrl = new URL(downloadUrl);
- // Чтобы добиться точки останова загрузки, получить начальную позицию загрузки из файла кеша при повторной загрузке
- if (getProgress(threadId) != 0) {
- startPos = getProgress(threadId);
- }
- System.out.println("нить" + threadId + "Продолжить загрузку, начальное местоположение:" + startPos + «Конечная позиция:» + endPos);
- // Общие операции HttpUrlConnection
- // Чтобы реализовать загрузку точки останова, вы должны установить mConnection.setRequestProperty("Range", "bytes=" +
- // startPos + "-" + endPos);
- HttpURLConnection mConnection = (HttpURLConnection) mUrl.openConnection();
- mConnection.setRequestMethod("POST");
- mConnection.setReadTimeout(5000);
- mConnection.setRequestProperty("Charset", "UTF-8");
- mConnection.setRequestProperty("Range", "bytes=" + startPos + "-" + endPos);
- mConnection.connect();
- // Если путь загрузки не существует, создайте путь к файлу
- File file = new File(fileDownloadPath);
- if (!file.getParentFile().exists()) {
- file.getParentFile().mkdirs();
- }
- // Чтение и запись файла для загрузки через RandomAccessFile
- RandomAccessFile downloadFile = new RandomAccessFile(fileDownloadPath, "rw");
- // При записи переместите курсор в начальную позицию для загрузки
- downloadFile.seek(startPos);
- BufferedInputStream bis = new BufferedInputStream(mConnection.getInputStream());
- int size = 0; // Получаем размер в байтах, хранящихся в буфере
- long len = 0; // Запишите размер этой загрузки, чтобы рассчитать, куда переместилась начальная позиция этой загрузки
- byte[] buf = new byte[ 1024];
- while ((size = bis.read(buf)) != -1) {
- // накапливаем
- len += size;
- // Затем записываем содержимое буфера в файл загрузки
- downloadFile.write(buf, 0, size);
- // Затем перемещаем начальную позицию загрузки в конец загруженного файла и записываем в файл кеша
- setProgress(threadId, startPos + len);
- }
- // Получаем время окончания загрузки, вывод
- long curTime = System.currentTimeMillis();
- System.out.println("нить" + threadId + "Скачивание завершено, занимает много времени:" + (curTime - startTime) + "ms.");
- // закрываем потоки, файлы и соединения
- downloadFile.close();
- mConnection.disconnect();
- bis.close();
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- /**
- * Получить ход загрузки из временного файла
- *
- * @param threadId
- * @return
- */
- private long getProgress(int threadId) {
- try {
- File markFile = new File(tempFilePath);
- if (!markFile.exists()) {
- return 0;
- }
- FileInputStream fis = new FileInputStream(markFile);
- BufferedInputStream bis = new BufferedInputStream(fis);
- byte[] buf = new byte[ 1024];
- String startPos = "";
- int len = -1;
- while ((len = bis.read(buf)) != -1) {
- startPos += new String(buf, 0, len);
- }
- // Файл нельзя удалить, не закрыв поток
- fis.close();
- bis.close();
- return Long.parseLong(startPos);
- } catch (Exception e) {
- e.printStackTrace();
- }
- return 0;
- }
- /**
- * Запись процесса загрузки во временный файл
- *
- * @param threadId
- * @param startPos
- */
- private void setProgress(int threadId, long startPos) {
- try {
- File markFile = new File(tempFilePath);
- if (!markFile.getParentFile().exists()) {
- markFile.getParentFile().mkdirs();
- }
- RandomAccessFile rr = new RandomAccessFile(markFile, "rw"); // сохраняем файл, помеченный для скачивания
- String strStartPos = String.valueOf(startPos);
- rr.write(strStartPos.getBytes(), 0, strStartPos.length());
- rr.close();
- } catch (Exception e) {
- e.printStackTrace();
- } finally {
- // Когда загрузка файла будет завершена, то есть когда начальная позиция и конечная позиция совпадут, удаляем файл кеша, в котором фиксируется прогресс
- if (startPos >= endPos) {
- File markFile = new File(tempFilePath);
- if (markFile.exists()) {
- System.out.println("markFile delete");
- markFile.delete();
- }
- }
- }
- }
- }
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
public class ResumeDownload {
public static final String DOWNLOAD_URL = "http://7xs0af.com1.z0.glb.clouddn.com/High-Wake.mp3";
public static final String DOWNLOAD_PARENT_PATH = "D:\\test_resume_download\\hi";
public static final int THREAD_COUNT = 3;
public static void main(String[] args) {
try {
// 获取到下载地址的连接
URL mUrl = new URL(DOWNLOAD_URL);
HttpURLConnection conn = (HttpURLConnection) mUrl.openConnection();
// 获取下载文件的大小
int fileLen = conn.getContentLength();
// 通过下载链接获取下载文件的文件名
String filePathUrl = conn.getURL().getFile();
String fileName = filePathUrl.substring(filePathUrl.lastIndexOf(File.separator) + 1);
// 生成下载路径
String fileDownloadPath = DOWNLOAD_PARENT_PATH + File.separator + fileName;
// 判断父路径是否存在,不存在就生成
File file = new File(fileDownloadPath);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
// 关闭连接
conn.disconnect();
/**
* 以下为多线程下载,主要原理就是将文件大小均分多块(根据线程数) 每一个线程从不同的起始位置,下载相等大小的文件 主要通过
* HttpUrlConnection里面设置Range参数,设置每一个线程下载的范围
* setRequestProperty("Range", "bytes=" + startPos + "-" + endPos);
*/
int blockSize = fileLen / THREAD_COUNT;
for (int threadId = 1; threadId <= THREAD_COUNT; threadId++) {
// 获取每一个线程下载的起始位置和结束位置
long startPos = (threadId - 1) * blockSize;
long endPos = threadId * blockSize - 1;
if (threadId == THREAD_COUNT) {
endPos = fileLen;
}
// 然后通过再不同线程里面实现下载逻辑
// 具体实现在DownloadThread这个Runnable里面
new Thread(new DownLoadTask(threadId, startPos, endPos, fileDownloadPath, DOWNLOAD_URL)).start();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 具体下载逻辑
*
* @author Administrator
*
*/
class DownLoadTask implements Runnable {
public static final String TEMP_NAME = "_tempfile";
private int threadId; // 当前线程id
private long startPos; // 下载的起始位置
private long endPos; // 下载的结束位置
private String fileDownloadPath; // 下载文件存放的文件位置
private String downloadUrl; // 下载链接
private String tempFilePath; // 记录进度的临时文件路径
public DownLoadTask(int threadId, long startPos, long endPos, String fileDownloadPath, String downloadUrl) {
super();
this.threadId = threadId;
this.startPos = startPos;
this.endPos = endPos;
this.fileDownloadPath = fileDownloadPath;
this.downloadUrl = downloadUrl;
this.tempFilePath = fileDownloadPath + TEMP_NAME + threadId;
}
@Override
public void run() {
try {
// 记录下载的开始时间
long startTime = System.currentTimeMillis();
URL mUrl = new URL(downloadUrl);
// 为了实现断点下载,在重新下载时从缓存文件里面获取下载的起始位置
if (getProgress(threadId) != 0) {
startPos = getProgress(threadId);
}
System.out.println("线程" + threadId + "继续下载,开始位置:" + startPos + "结束位置是:" + endPos);
// HttpUrlConnection的常规操作
// 要实现断点下载的话,必须要设置mConnection.setRequestProperty("Range", "bytes=" +
// startPos + "-" + endPos);
HttpURLConnection mConnection = (HttpURLConnection) mUrl.openConnection();
mConnection.setRequestMethod("POST");
mConnection.setReadTimeout(5000);
mConnection.setRequestProperty("Charset", "UTF-8");
mConnection.setRequestProperty("Range", "bytes=" + startPos + "-" + endPos);
mConnection.connect();
// 如果下载路径不存在的话,则创建文件路径
File file = new File(fileDownloadPath);
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
// 通过RandomAccessFile对要下载的文件进行读写
RandomAccessFile downloadFile = new RandomAccessFile(fileDownloadPath, "rw");
// 写的时候,将光标移到要下载的起始位置
downloadFile.seek(startPos);
BufferedInputStream bis = new BufferedInputStream(mConnection.getInputStream());
int size = 0; // 获取缓存区存放的字节大小
long len = 0; // 记录本次下载的大小,以便计算本次下载的起始位置移动到了哪里
byte[] buf = new byte[1024];
while ((size = bis.read(buf)) != -1) {
// 累加
len += size;
// 然后将缓冲区的内容写到下载文件中
downloadFile.write(buf, 0, size);
// 然后将下载的起始位置移动到已经下载完的末尾,写到缓存文件里面去
setProgress(threadId, startPos + len);
}
// 获取下载结束时间,输出
long curTime = System.currentTimeMillis();
System.out.println("线程" + threadId + "已经下载完成,耗时:" + (curTime - startTime) + "ms.");
// 关闭流、文件和连接
downloadFile.close();
mConnection.disconnect();
bis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 从temp文件获取下载进度
*
* @param threadId
* @return
*/
private long getProgress(int threadId) {
try {
File markFile = new File(tempFilePath);
if (!markFile.exists()) {
return 0;
}
FileInputStream fis = new FileInputStream(markFile);
BufferedInputStream bis = new BufferedInputStream(fis);
byte[] buf = new byte[1024];
String startPos = "";
int len = -1;
while ((len = bis.read(buf)) != -1) {
startPos += new String(buf, 0, len);
}
// 不关闭流的话,不能删除文件
fis.close();
bis.close();
return Long.parseLong(startPos);
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
/**
* 在temp文件记录下载进度
*
* @param threadId
* @param startPos
*/
private void setProgress(int threadId, long startPos) {
try {
File markFile = new File(tempFilePath);
if (!markFile.getParentFile().exists()) {
markFile.getParentFile().mkdirs();
}
RandomAccessFile rr = new RandomAccessFile(markFile, "rw");// 存储下载标记的文件
String strStartPos = String.valueOf(startPos);
rr.write(strStartPos.getBytes(), 0, strStartPos.length());
rr.close();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 当文件下载完成时,即开始位置和结束位置重合时,删除记录进度的缓存文件
if (startPos >= endPos) {
File markFile = new File(tempFilePath);
if (markFile.exists()) {
System.out.println("markFile delete");
markFile.delete();
}
}
}
}
}