所谓断点下载,就是把下载的进度记录下来,当下次继续下载的时候,就读取上一次记录的位置开始继续下载.
读此文章前可先读上一篇文章
实现多线程断点下载,需要考虑以下几点问题:
1.关于如何记录线程下载资源的位置,这里可以通过创建临时文件的方式来保存当前下载的byte数作为记录,当所有的线程执行完毕后,再将这些临时文件删除.
2.对于如何确保程序在记录当前线程下载记录时,避免其他因素造成此时的记录还未来的及保存到临时文件的情况呢?这时候就需要一种可以实时更新写入下载记录的方式了,可以通过设置RandomAccessFile的模式来实现,如下api中有4种模式:
这里采用rwd的模式.
3.如何确认所有的下载线程在什么时候才算执行完毕了呢?
通过定义全局的变量runningThreadCount来记录所有的线程个数,在线程的run方法执行下载任务的时候添加finally语句,每个线程跑完自己的run方法后,都会进入finally块中,然后再将runningThreadCount的个数自减,当减到小于1的时候就说明了此时所有的线程都已经执行完毕了.
4.在解决3的问题的时候还需要考虑线程安全的问题,通过加入同步锁的方式可以解决此问题.
具体代码如下:
<span style="font-family:Courier New;font-size:14px;">import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.ProtocolException; import java.net.URL; /** * * @author Chenys * 多线程下载文件 * */ public class MultiThreadDownload { /** * 线程的数量 */ private static int threadCount = 3; /** * 每个下载区块的大小 */ private static long blocksize; /** * 当前正在工作的线程个数 */ private static int runningThreadCount; public static void main(String[] args) throws IOException { // 服务器文件的路径 String path = "http://192.168.0.102:8080/video1.avi"; URL url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setConnectTimeout(5000); int code = conn.getResponseCode(); if (code == 200) { // 得到服务器端返回的文件大小,单位byte,字节 long size = conn.getContentLength(); System.out.println("服务器文件的大小:" + size); // 计算每个blocksize的大小 blocksize = size / threadCount; /** * 1.首先在本地创建一个文件大小跟服务器一模一样的空白文件,RandomAccessFile类的实例支持对随机访问文件的读取和写入 * 参数1:目标文件 参数2:打开该文件的访问模式,"r" 以只读方式打开 ,"rw" 打开以便读取和写入 */ File file = new File("temp.avi"); RandomAccessFile raf = new RandomAccessFile(file, "rw"); raf.setLength(size);// 设置文件的大小 // 2.开启若干个子线程,分别下载对应的资源 runningThreadCount = threadCount; for (int i = 1; i <= threadCount; i++) { long startIndex = (i - 1) * blocksize; // 由于服务端下载文件是从0开始的 long endIndex = i * blocksize - 1; if (i == threadCount) { // 最后一个线程 endIndex = size - 1; } System.out.println("开启线程:" + i + "下载的位置" + startIndex + "~"+ endIndex); new DownloadThread(path, i, startIndex, endIndex).start(); } } conn.disconnect(); } /** * 自定义下载线程 * @author Chenys * */ private static class DownloadThread extends Thread { private int threadId; // 线程id private long startIndex; // 开始下载的位置 private long endIndex; // 结束下载的位置 private String path; public DownloadThread(String path, int threadId, long startIndex, long endIndex) { this.path = path; this.threadId = threadId; this.startIndex = startIndex; this.endIndex = endIndex; } /** * 执行下载任务 */ public void run() { try { //创建记录资源当前下载到什么位置的临时文件 File tempFile = new File(threadId+".txt"); //记录当前线程下载的总大小total int total = 0; URL url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); //接着从上一次断点的位置继续下载数据 if(tempFile.exists() && tempFile.length()>0){ FileInputStream lastDownload = new FileInputStream(tempFile); BufferedReader br = new BufferedReader(new InputStreamReader(lastDownload)); //获取当前线程上次下载的总大小是多少 String lastTotalStr= br.readLine(); int lastTotal = Integer.parseInt(lastTotalStr); System.out.println("上次线程"+threadId+"下载的总大小:"+lastTotal); //更新startIndex的位置和每条线程下载的总大小数 startIndex+=lastTotal; total+=lastTotal; lastDownload.close(); } conn.setConnectTimeout(5000); //设置http协议请求头: 指定每条线程从文件的什么位置开始下载,下载到什么位置为止 conn.setRequestProperty("Range", "bytes=" + startIndex + "-"+ endIndex); int code = conn.getResponseCode(); System.out.println("服务器返回码code=" + code);// 如果是下载一部分资源,那么返回码是206 InputStream is = conn.getInputStream(); File file = new File("temp.avi"); RandomAccessFile raf = new RandomAccessFile(file, "rw"); // 指定文件的下载起始位置 raf.seek(startIndex); System.out.println("第" + threadId + "个线程写文件的开始位置"+ String.valueOf(startIndex)); // 开始保存文件 int len = 0; byte[] buff = new byte[1024*1024*10]; //10M的缓冲数组,提高下载速度 while ((len = is.read(buff)) != -1) { RandomAccessFile rf = new RandomAccessFile(tempFile, "rwd");//注意:这里采用的模式是"rwd",即无缓存模式的读写 //写数据到目标文件 raf.write(buff, 0, len); //记录当前记录的位置到文件中保存 total+=len; rf.write(String.valueOf(total).getBytes()); rf.close(); } is.close(); raf.close(); } catch (MalformedURLException e) { e.printStackTrace(); } catch (ProtocolException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally{ //以下是处理清理缓存的任务 synchronized (MultiThreadDownload.class) { System.out.println("线程" + threadId + "下载完毕了"); runningThreadCount--; if (runningThreadCount<1) { System.out.println("所有的线程都工作完毕了,删除临时文件"); for(int i=1;i<=threadCount;i++){ File tempFile = new File(i+".txt"); System.out.println(tempFile.delete()); } } } } } } } </span>
查看下载的文件和生成的临时文件
上图用于记录断点下载位置的临时文件是1.txt,2.txt,3.txt分别对应3个下载线程.而temp.avi则是保存下载资源的目标文件,根据大小可以知道目标文件的大小在下载的过程中是不会变的,因为代码的设计就是让资源文件在下载之前先创建一个大小和资源文件一样大的空白文件,也就是这里所说的temp.avi文件.虽然显示的大小是和服务器上的资源文件大小一样,当这并不代表该文件就已经下载完毕了,打开播放的时候你会发现,视频只能播放一部分,因为此时只下载了一部分.具体下载了多少,我们可以打开临时文件1.txt,2.txt,3.txt来查看.
如下图:
很明显,临时文件中记录如果相加起来的大小是:201425+596630+33938=1,131,993个byte,而服务器上的资源文件大小是23,377,846 个byte,这点可以证明该文件是还没有完全下载完毕的.
此时重新点击运行程序,直到程序运行完毕,执行效果如下:
查看下载的文件和临时文件,发现此时临时文件已经被删除了只剩下下载下来的资源文件了.