Java использует HttpUrlConnection для реализации многопоточной загрузки точки останова.

Java задняя часть .NET GitHub
Я полагаю, что многие студенты часто задают такой вопрос интервьюеру на собеседовании: как реализовать загрузку с точки останова, то есть когда файл не скачивается, сохранить прогресс и продолжить загрузку в следующий раз. Реализовать эту функцию несложно, просто используйте временный файл для записи текущего хода загрузки, а затем начните загрузку с хода, записанного временным файлом при следующей загрузке, чтобы реализовать функцию.

После того, как вы реализуете вышеуказанные функции, интервьюер может снова спросить: можно ли реализовать многопоточную загрузку с точки останова? Для этой проблемы, на самом деле, если решить первую проблему, решить эту проблему несложно, это не что иное, как разделить размер файла на несколько частей и загрузить разные части в разные потоки. И этот пример решает две вышеупомянутые проблемы.

Вот решения:

1. Размер файла делится на разные куски в соответствии с количеством потоков.
2. Создайте имя файла в соответствии с URL-адресом и одновременно создайте путь загрузки.
3. Создайте начальную позицию и конечную позицию каждого сегмента блока.
4. Создайте несколько потоков и выполните операции 5-7 в каждом потоке.
5. Установите соединение, прочитайте начальную позицию последней загрузки из временного файла и установите диапазон загрузки (начальная позиция, конечная позиция) в заголовке запроса.
6. Откройте загруженный файл, переместите курсор в начальную позицию и запишите в него данные, а также записывайте положение файла во временный файл каждый раз, когда он записывается.
7. После завершения загрузки временный файл удаляется.

В этой статье просто представлены идеи для объяснения. Существует много места для оптимизации кода, и детали не нужно запутывать. Также можно использовать другие сетевые фреймворки. HttpUrlConnection — один из самых основных сетевых фреймворков, предоставляемых Java. Если вы это понимаете, остальные такие же. .

Полный класс находится на Github:GitHub.com/People’s Eyes Persist…

  1. import java.io.BufferedInputStream;  
  2. import java.io.File;  
  3. import java.io.FileInputStream;  
  4. import java.io.RandomAccessFile;  
  5. import java.net.HttpURLConnection;  
  6. import java.net.URL;  
  7.   
  8. public class ResumeDownload {  
  9.     public static final String DOWNLOAD_URL =  "http://7xs0af.com1.z0.glb.clouddn.com/High-Wake.mp3";  
  10.     public static final String DOWNLOAD_PARENT_PATH =  "D:\\test_resume_download\\hi";  
  11.     public static final int THREAD_COUNT =  3;  
  12.   
  13.     public static void main(String[] args) {  
  14.         try {  
  15.             // Получаем соединение с адресом загрузки  
  16.             URL mUrl = new URL(DOWNLOAD_URL);  
  17.             HttpURLConnection conn = (HttpURLConnection) mUrl.openConnection();  
  18.             // Получаем размер загруженного файла  
  19.             int fileLen = conn.getContentLength();  
  20.             // Получить имя загруженного файла по ссылке для скачивания  
  21.             String filePathUrl = conn.getURL().getFile();  
  22.             String fileName = filePathUrl.substring(filePathUrl.lastIndexOf(File.separator) + 1);  
  23.             // Генерируем путь загрузки  
  24.             String fileDownloadPath = DOWNLOAD_PARENT_PATH + File.separator + fileName;  
  25.             // Определяем, существует ли родительский путь, и генерируем его, если он не существует  
  26.             File file = new File(fileDownloadPath);  
  27.             if (!file.getParentFile().exists()) {  
  28.                 file.getParentFile().mkdirs();  
  29.             }  
  30.             // закрываем соединение  
  31.             conn.disconnect();  
  32.   
  33.             /** 
  34. *Далее идет многопоточная загрузка.Основной принцип – разделить размер файла на несколько блоков (по количеству потоков).Каждый поток скачивает файлы одинакового размера с разных начальных позиций.В основном через 
  35. Установите параметр Range в HttpUrlConnection, чтобы установить диапазон загрузки каждого потока. 
  36.              * setRequestProperty("Range", "bytes=" + startPos + "-" + endPos); 
  37.              */  
  38.   
  39.             int blockSize = fileLen / THREAD_COUNT;  
  40.             for (int threadId = 1; threadId <= THREAD_COUNT; threadId++) {  
  41.                 // Получаем начальную и конечную позиции загрузки каждого потока  
  42.                 long startPos = (threadId - 1) * blockSize;  
  43.                 long endPos = threadId * blockSize -  1;  
  44.                 if (threadId == THREAD_COUNT) {  
  45.                     endPos = fileLen;  
  46.                 }  
  47.   
  48.                 // Затем реализуем логику загрузки в разных потоках  
  49.                 // Специально реализовано в DownloadThread Runnable  
  50.                 new Thread(new DownLoadTask(threadId, startPos, endPos, fileDownloadPath, DOWNLOAD_URL)).start();  
  51.             }  
  52.   
  53.         } catch (Exception e) {  
  54.             e.printStackTrace();  
  55.         }  
  56.   
  57.     }  
  58. }  
  59.   
  60. /** 
  61. * Определенная логика загрузки 
  62.  *  
  63.  * @author Administrator 
  64.  * 
  65.  */  
  66. class DownLoadTask implements Runnable {  
  67.     public static final String TEMP_NAME =  "_tempfile";  
  68.     private int threadId; // идентификатор текущего потока  
  69.     private long startPos; // Начальное место загрузки  
  70.     private long endPos; // Конечная позиция загрузки  
  71.     private String fileDownloadPath; // Расположение файла, где хранится загруженный файл  
  72.     private String downloadUrl; // ссылка для скачивания  
  73.   
  74.     private String tempFilePath; // Временный путь к файлу для записи прогресса  
  75.   
  76.     public DownLoadTask(int threadId, long startPos,  long endPos, String fileDownloadPath, String downloadUrl) {  
  77.         super();  
  78.         this.threadId = threadId;  
  79.         this.startPos = startPos;  
  80.         this.endPos = endPos;  
  81.         this.fileDownloadPath = fileDownloadPath;  
  82.         this.downloadUrl = downloadUrl;  
  83.   
  84.         this.tempFilePath = fileDownloadPath + TEMP_NAME + threadId;  
  85.     }  
  86.   
  87.     @Override  
  88.     public void run() {  
  89.         try {  
  90.             // записываем время начала загрузки  
  91.             long startTime = System.currentTimeMillis();  
  92.   
  93.             URL mUrl = new URL(downloadUrl);  
  94.   
  95.             // Чтобы добиться точки останова загрузки, получить начальную позицию загрузки из файла кеша при повторной загрузке  
  96.             if (getProgress(threadId) != 0) {  
  97.                 startPos = getProgress(threadId);  
  98.             }  
  99.   
  100.             System.out.println("нить" + threadId + "Продолжить загрузку, начальное местоположение:" + startPos +  «Конечная позиция:» + endPos);  
  101.   
  102.             // Общие операции HttpUrlConnection  
  103.             // Чтобы реализовать загрузку точки останова, вы должны установить mConnection.setRequestProperty("Range", "bytes=" +  
  104.             // startPos + "-" + endPos);  
  105.             HttpURLConnection mConnection = (HttpURLConnection) mUrl.openConnection();  
  106.             mConnection.setRequestMethod("POST");  
  107.             mConnection.setReadTimeout(5000);  
  108.             mConnection.setRequestProperty("Charset""UTF-8");  
  109.             mConnection.setRequestProperty("Range""bytes=" + startPos +  "-" + endPos);  
  110.             mConnection.connect();  
  111.   
  112.             // Если путь загрузки не существует, создайте путь к файлу  
  113.             File file = new File(fileDownloadPath);  
  114.             if (!file.getParentFile().exists()) {  
  115.                 file.getParentFile().mkdirs();  
  116.             }  
  117.   
  118.             // Чтение и запись файла для загрузки через RandomAccessFile  
  119.             RandomAccessFile downloadFile = new RandomAccessFile(fileDownloadPath,  "rw");  
  120.             // При записи переместите курсор в начальную позицию для загрузки  
  121.             downloadFile.seek(startPos);  
  122.   
  123.             BufferedInputStream bis = new BufferedInputStream(mConnection.getInputStream());  
  124.             int size = 0// Получаем размер в байтах, хранящихся в буфере  
  125.             long len = 0// Запишите размер этой загрузки, чтобы рассчитать, куда переместилась начальная позиция этой загрузки  
  126.             byte[] buf = new byte[ 1024];  
  127.             while ((size = bis.read(buf)) != -1) {  
  128.                 // накапливаем  
  129.                 len += size;  
  130.                 // Затем записываем содержимое буфера в файл загрузки  
  131.                 downloadFile.write(buf, 0, size);  
  132.                 // Затем перемещаем начальную позицию загрузки в конец загруженного файла и записываем в файл кеша  
  133.                 setProgress(threadId, startPos + len);  
  134.             }  
  135.   
  136.             // Получаем время окончания загрузки, вывод  
  137.             long curTime = System.currentTimeMillis();  
  138.             System.out.println("нить" + threadId + "Скачивание завершено, занимает много времени:" + (curTime - startTime) +  "ms.");  
  139.   
  140.             // закрываем потоки, файлы и соединения  
  141.             downloadFile.close();  
  142.             mConnection.disconnect();  
  143.             bis.close();  
  144.   
  145.         } catch (Exception e) {  
  146.             e.printStackTrace();  
  147.         }  
  148.     }  
  149.   
  150.     /** 
  151. * Получить ход загрузки из временного файла 
  152.      *  
  153.      * @param threadId 
  154.      * @return 
  155.      */  
  156.     private long getProgress(int threadId) {  
  157.         try {  
  158.             File markFile = new File(tempFilePath);  
  159.             if (!markFile.exists()) {  
  160.                 return 0;  
  161.             }  
  162.             FileInputStream fis = new FileInputStream(markFile);  
  163.             BufferedInputStream bis = new BufferedInputStream(fis);  
  164.             byte[] buf = new byte[ 1024];  
  165.             String startPos = "";  
  166.             int len = -1;  
  167.             while ((len = bis.read(buf)) != -1) {  
  168.                 startPos += new String(buf, 0, len);  
  169.             }  
  170.   
  171.             // Файл нельзя удалить, не закрыв поток  
  172.             fis.close();  
  173.             bis.close();  
  174.   
  175.             return Long.parseLong(startPos);  
  176.   
  177.         } catch (Exception e) {  
  178.             e.printStackTrace();  
  179.         }  
  180.         return 0;  
  181.     }  
  182.   
  183.     /** 
  184. * Запись процесса загрузки во временный файл 
  185.      *  
  186.      * @param threadId 
  187.      * @param startPos 
  188.      */  
  189.     private void setProgress(int threadId,  long startPos) {  
  190.         try {  
  191.             File markFile = new File(tempFilePath);  
  192.             if (!markFile.getParentFile().exists()) {  
  193.                 markFile.getParentFile().mkdirs();  
  194.             }  
  195.               
  196.             RandomAccessFile rr = new RandomAccessFile(markFile, "rw"); // сохраняем файл, помеченный для скачивания  
  197.             String strStartPos = String.valueOf(startPos);  
  198.             rr.write(strStartPos.getBytes(), 0, strStartPos.length());  
  199.               
  200.             rr.close();  
  201.         } catch (Exception e) {  
  202.             e.printStackTrace();  
  203.         } finally {  
  204.             // Когда загрузка файла будет завершена, то есть когда начальная позиция и конечная позиция совпадут, удаляем файл кеша, в котором фиксируется прогресс  
  205.             if (startPos >= endPos) {  
  206.                 File markFile = new File(tempFilePath);  
  207.                 if (markFile.exists()) {  
  208.                     System.out.println("markFile delete");  
  209.                     markFile.delete();  
  210.                 }  
  211.             }  
  212.         }  
  213.   
  214.     }  
  215. }  
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();
				}
			}
		}

	}
}