基本原理:
首先利用URLConnection获取想要下载的文件的长度,然后由URLConnection获取输入流,根据文件的长度以及下载线程的个数,将文件分成固定大小的块,每一块单独启动一个线程读取、写入。通过输入流读取下载文件的信息,然后将读取的信息用RandomAccessFile随机写入到本地文件中。同时,在每个线程中需要用一个临时文件来保存当前已经下载文件的长度,这样的话如果本次下载没有完成,下一次可以在原来已经下载好的基础上继续下载,就不需要重新下载原来已经下过的部分。
代码如下
如果想要多线程下载只需要传入文件下载路径,文件保存路径以及线程的个数,便可以将文件下载至本地。
package com.xqq.mutilthreaddownload;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 多线程下载
* @author xqq
*/
public class MutilThreadDownload extends Thread {
private String downloadPath;
private String savePath;
private int nThread;
private final ExecutorService exec;
public MutilThreadDownload(String downloadPath, String savePath, int nThread) {
this.downloadPath = downloadPath;
this.savePath = savePath;
this.nThread = nThread;
this.exec = Executors.newFixedThreadPool(nThread);
}
@Override
public void run() {
try {
// 连接获取文件信息
URL url = new URL(downloadPath);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setRequestMethod("GET");
int code = conn.getResponseCode();
if (code == 200) {
int length = conn.getContentLength();// 获取文件长度
System.out.println("文件的总长度为: " + length);
//每个线程所需下载文件的大小
int blockSize = length / nThread;
for (int i = 1; i <= nThread; i++) {
int startIndex = (i - 1) * blockSize;
int endIndex = i * blockSize - 1;
if (i == nThread) {
endIndex = length;
}
//开始下载文件
exec.execute(new Download(startIndex, endIndex, downloadPath, i, savePath));
}
} else {
System.out.println("文件错误......");
}
} catch (MalformedURLException e) {
System.out.println("MalformedURLException");
} catch (IOException e) {
System.out.println("IOException");
}
//当所有线程均下载完成之后,关闭线程池
exec.shutdown();
}
}
根据文件开始和结束位置从输入流中读取文件并利用RandomAccessFile保存至相应位置。
package com.xqq.mutilthreaddownload;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
public class Download implements Runnable{
private int startIndex;
private int endIndex;
private String path;
private static int runningThread = Test.nThread;
private int threadID;
private String savePath;
public Download(int startIndex, int endIndex, String path, int threadID, String savePath) {
this.startIndex = startIndex;
this.endIndex = endIndex;
this.path = path;
this.threadID = threadID;
this.savePath = savePath;
}
public void run() {
try {
File tempFile = new File(savePath + threadID + ".txt");
if(tempFile.exists() && tempFile.length() > 0){
FileInputStream fis = new FileInputStream(tempFile);
byte [] temp = new byte[1024];
int length = fis.read(temp);
String downloadLen = new String(temp, 0, length);
startIndex = Integer.parseInt(downloadLen);
fis.close();
}
System.out.println(Thread.currentThread() + " 下载范围: " + startIndex + " - >" + endIndex);
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);
conn.setConnectTimeout(5000);
conn.setRequestMethod("GET");
int code = conn.getResponseCode();
//获取部分数据,成功返回206
if(code == 206){
RandomAccessFile file = new RandomAccessFile(savePath + "setup.exe", "rwd");
file.seek(startIndex);//定位到文件的位置
InputStream in = conn.getInputStream();
byte [] buffer = new byte[1024 * 2];
int len = 0;
int total = 0;
while((len = in.read(buffer)) != -1){
RandomAccessFile totalFile = new RandomAccessFile(savePath + threadID + ".txt", "rwd");
file.write(buffer, 0, len);
total += len;
totalFile.write((total + startIndex + "").getBytes());
totalFile.close();
}
file.close();
}else{
System.out.println(Thread.currentThread() + " 下载失败");
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally{
System.out.println(Thread.currentThread() + " 下载完成.....");
runningThread --;
if(runningThread == 0){
for(int i = 1; i <= Test.nThread; i++){
File file = new File(savePath + i + ".txt");
file.delete();
}
}
}
}
}
测试文件
package com.xqq.mutilthreaddownload;
public class Test {
private final static String downloadPath = "http://localhost:8080/123.exe";
private static final String savePath = "c://";
public static final int nThread = 5;
public static void main(String[] args) {
MutilThreadDownload thread = new MutilThreadDownload(downloadPath,savePath, nThread);
thread.start();
}
}
测试结果:
文件的总长度为: 13087904
Thread[pool-1-thread-2,5,main] 下载范围: 3283180 - >5235159
Thread[pool-1-thread-3,5,main] 下载范围: 5900760 - >7852739
Thread[pool-1-thread-4,5,main] 下载范围: 8518340 - >10470319
Thread[pool-1-thread-5,5,main] 下载范围: 11133872 - >13087904
Thread[pool-1-thread-1,5,main] 下载范围: 665600 - >2617579
Thread[pool-1-thread-4,5,main] 下载完成.....
Thread[pool-1-thread-2,5,main] 下载完成.....
Thread[pool-1-thread-3,5,main] 下载完成.....
Thread[pool-1-thread-5,5,main] 下载完成.....
Thread[pool-1-thread-1,5,main] 下载完成.....
总结:
我今天突然发现,使用了线程池,如果你不调用shutdown()或者shutdownNow()显示的关闭线程池,那么main函数就不会停止,会一直等待线程加入线程池中运行。知道了这个之后,我就顿悟了为什么之前我在Android开发的时候使用了线程池却一直打印线程太多的问题。