主程序
/**
* 客户端
*/
package ThreadDownload;
public class Client {
public String urlFile; // 网络文件地址
public int threadNum; // 需要启动下载的线程数
public String localFilePath; // 需要保存下载文件的本地地址,保证该目录下没有名为"tmp"的文件夹
/**
* 客户端自己设定
*/
public Client() {
urlFile = "http://dl.google.com/android/android-sdk_r12-windows.zip";
threadNum = 9;
localFilePath = "d://";
}
private void start() {
Thread thread = new Thread(new MultiThreadGetFile(urlFile,
localFilePath, threadNum));
thread.start();
}
public static void main(String[] args) {
Client client = new Client();
client.start();
}
}
多线程下载调度程序
/**
* 多线程下载调度程序
*/
package ThreadDownload;
import java.io.File;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.StringTokenizer;
public class MultiThreadGetFile extends Thread {
public long startPos = 0;
public long endPos = 0;
public String currentFileThreadName; // 要带上完整的路径
public String urlFile; // 网络文件地址
public String urlFileName; // 网络文件名
public String localFilePath; // 需要保存下载文件的本地地址
public int threadNum; // 需要启动下载的线程数
public long[] eachThreadLength; // 每个线程要下载的文件分块的大小
public long urlFileLength; // 网络文件的大小
URL url;
HttpURLConnection httpURLConnection;
public static boolean[] checkList; // 检测线程
/**
* @param urlFile 网络文件地址
* @param localFilePath 需要保存下载文件的本地地址
* @param threadNum 需要启动下载的线程数
*/
public MultiThreadGetFile(String urlFile, String localFilePath,
int threadNum) {
super();
this.urlFile = urlFile;
this.localFilePath = localFilePath;
this.threadNum = threadNum;
}
/**
* 确定每个线程文件最终要写的文件的大小
*/
private void init_getEachThreadLength() {
long l;
l = urlFileLength / threadNum;
for (int i = 0; i < threadNum; i++) {
if (i == threadNum - 1) {
eachThreadLength[i] = urlFileLength - i * l;
} else {
eachThreadLength[i] = l;
}
}
}
/**
* 得到传递过来的带路径的文件的文件名 eg:GetFileName("d://test/test.txt") 返回为:test.txt
*/
private String getFileName(String file) {
StringTokenizer st = new StringTokenizer(file, "/");
while (st.hasMoreTokens()) {
file = st.nextToken();
}
return file;
}
private void init() {
// 创建一个临时文件夹tmp
if (!new File(localFilePath + "tmp").mkdir()) {
// System.out.println("创建文件夹失败!");
}
eachThreadLength = new long[threadNum];
try {
url = new URL(urlFile);
// 此处的Connection仅仅是用于获取服务端的要下载的资源的名称和资源的大小,所以一旦得到后就关闭
httpURLConnection = (HttpURLConnection) url.openConnection();
urlFileLength = Long.parseLong(httpURLConnection
.getHeaderField("Content-Length"));
urlFileName = url.getFile(); // 取得在服务器上的路径及文件名
urlFileName = getFileName(urlFileName); // 只取得文件名
init_getEachThreadLength(); // 确定每个线程最终要写的文件的大小
httpURLConnection.disconnect();
checkList = new boolean[threadNum + 1]; // 记载每个线程是否下载完毕
for (int i = 1; i <= threadNum; i++) {
if (i > 1) {
startPos = startPos + eachThreadLength[i - 2];
}
endPos = startPos + eachThreadLength[i - 1];
currentFileThreadName = localFilePath + "tmp\\" + urlFileName
+ ".part" + i;
System.out.println("part" + i + ": " + startPos + "--->"
+ endPos + ", size: " + (endPos - startPos));
Thread thread = new Thread(new GetFileThread(startPos, endPos,
currentFileThreadName, urlFile, i));
thread.start();
checkList[i] = false; // 表示该线程开始
}
Thread monitorThread = new Thread(new MonitorThread(threadNum,
localFilePath, localFilePath + "tmp"));
monitorThread.start();
} catch (Exception e) {
e.printStackTrace();
}
}
public void run() {
init();
}
}
下载线程
/**
* 下载线程
* 原理:
* 根据传入的下载开始点以及文件下载结束点,利用HttpURLConnection的RANGE属性
* 从网络文件开始下载,并结合判断是否下载的文件大小已经等于(文件下载结束点-下载开始点)
* 这里结合断点续传原理,可以更快、更有效的下载文件
*/
package ThreadDownload;
import java.io.BufferedInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
public class GetFileThread extends Thread {
public long startPos; // 传入的文件下载开始点
public long endPos; // 传入的文件下载结束点
public String currentFileThreadName; // 当前线程的完程路径及名字
public String urlFile; // 网络文件地址
int currentThread; // 当前线程,用于下载完成后将对应的检测标志设为true,表示下载完成
/**
* @param startPos 传入的文件下载开始点
* @param endPos 传入的文件下载结束点
* @param currentFileThreadName 当前线程的完程路径及名字
* @param urlFile 网络文件地址
* @param currentThread 当前线程
*/
public GetFileThread(long startPos, long endPos,
String currentFileThreadName, String urlFile, int currentThread) {
super();
this.startPos = startPos;
this.endPos = endPos;
this.currentFileThreadName = currentFileThreadName;
this.urlFile = urlFile;
this.currentThread = currentThread;
}
private boolean fileExist(String pathAndFile) {
File file = new File(pathAndFile);
if (file.exists())
return true;
else
return false;
}
private long fileSize(String pathAndFile) {
long fileSize = 0;
File filet = new File(pathAndFile);
fileSize = filet.length();
return fileSize;
}
private void fileRename(String fName, String nName) {
File file = new File(fName);
file.renameTo(new File(nName));
file.delete();
}
public void run() {
URL url = null;
HttpURLConnection httpURLConnection = null;
BufferedInputStream bis = null; // 缓存流
int len = 0;
byte[] bt = new byte[1024]; // 缓冲区
DataOutputStream dos = null;
FileOutputStream fos = null;
RandomAccessFile raf = null;
String localFile = currentFileThreadName; // 文件保存的地方及文件名,具体情况可以改
String localFile_tp = localFile + ".tp"; // 未下载完文件加.tp扩展名,以便于区别
long fileSize = 0; // 在断点续传中,用于取得当前文件已经下载的大小
long totalSize = 0; // 当前块要下载的文件总大小
try {
url = new URL(urlFile);
httpURLConnection = (HttpURLConnection) url.openConnection();
totalSize = endPos - startPos; // 取得该快实际要写的文件大小
long downSize = 0; // 已经下载的大小
// 确定临时文件是否存在
if (fileExist(localFile_tp)) { // 采用断点续传,这里的依据是看下载文件是否在本地有.tp有扩展名同名文件
System.out.println("线程" + currentThread + "文件正在续传...");
fileSize = new File(localFile_tp).length(); // 取得已经下载的大小,以便确定随机写入的位置
downSize = fileSize; // 下载大小
fileSize = startPos + fileSize; // 取得文件开始写入点
// httpURLConnection属性的设置一定要在得到输入流之前,否则会报已经连接的错误
httpURLConnection.setRequestProperty("RANGE", "bytes="
+ fileSize + "-");
// 设置接受信息
httpURLConnection.setRequestProperty("Accept",
"image/gif,image/x-xbitmap,application/msword,*/*");
raf = new RandomAccessFile(localFile_tp, "rw"); // 随机方式读取
raf.seek(downSize); // 定位指针到downSize已下载文件大小的位置
bis = new BufferedInputStream(
httpURLConnection.getInputStream()); // 缓存流
while ((len = bis.read(bt)) > 0) {
if (downSize < (endPos - startPos)) {
downSize = downSize + len;
// 说明上一步是多加了一个downSize,这里再减去一个downSize
if (downSize > (endPos - startPos)) {
len = (int) ((endPos - startPos) - (downSize - len));
}
raf.write(bt, 0, len);
} else {
break;
}
}
System.out.println("线程" + currentThread + "文件续传完成...");
}
// 采用原始下载,但保证该文件没有下载
else if (!fileExist(localFile)) {
// 设置断点续传的开始位置
httpURLConnection.setRequestProperty("RANGE", "bytes="
+ startPos + "-");
bis = new BufferedInputStream(
httpURLConnection.getInputStream());
fos = new FileOutputStream(localFile_tp); // 没有下载完毕就将文件的扩展名命名.tp
dos = new DataOutputStream(fos);
System.out.println("线程" + currentThread + "正在接收文件...");
while ((len = bis.read(bt)) > 0) {
// 确定没有下载完毕
if (downSize < (endPos - startPos)) {
downSize = downSize + len;
// 如果当前下载的加上要下载的已经超过要求的下载范围
if (downSize > (endPos - startPos)) {
// 就只取满足要求的下载部分
len = (int) ((endPos - startPos) - (downSize - len));
}
dos.write(bt, 0, len); // 写文件
} else {
break;
}
}
}
// 下载完毕后,将文件重命名
if (fileSize(localFile_tp) == totalSize) {
fileRename(localFile_tp, localFile);
}
MultiThreadGetFile.checkList[currentThread] = true;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (bis != null) {
bis.close();
}
if (dos != null) {
dos.close();
}
if (fos != null) {
fos.close();
}
if (raf != null) {
raf.close();
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
}
监视线程
/**
* 监视线程,检测其它的线程是否已经运行完毕
* 原理:
* 在MultiThreadGetFile里定义一个全局静态boolean数组,在启动每个GetFileThread的时候
* 就将对应的数组的值设为false,当对应线程完成后就把对应的数组设为true
* 在当前线程采用不停检测是否所有数组的值都为true,
* 如是那就说明所有的线程已经运行完毕,如果没有就继续检测。
* 等到所有的GetFileThread线程都完成后,那么就调用文件拼合线程,合并下载的文件块并删除 临时文件块。
*/
package ThreadDownload;
public class MonitorThread extends Thread {
public int totalThread;
public String localFilePath;
public String localFilePath_tmp;
/**
* @param totalThread 总线程数
* @param localFilePath 本地下载文件存放路径
* @param localFilePath_tmp 正在下载的文件存放路径
*/
public MonitorThread(int totalThread, String localFilePath,
String localFilePath_tmp) {
super();
this.totalThread = totalThread;
this.localFilePath = localFilePath;
this.localFilePath_tmp = localFilePath_tmp;
}
public void run() {
boolean isRun = true;
int allStop = 0;
while (isRun) {
allStop = 0;
for (int i = 1; i <= totalThread; i++) {
if (MultiThreadGetFile.checkList[i] == true) {
allStop++;
}
}
try {
Thread.sleep(500);
} catch (Exception e) {
e.printStackTrace();
}
// 说明都已经下载完毕,此时停止线程下载
if (allStop == totalThread) {
isRun = false;
}
}
// 到此,说明九个线程均已下载完毕,开始合并文件的线程操作
Thread thread = new Thread(new FileCombination(localFilePath,
localFilePath_tmp));
thread.start();
}
}
文件合并线程
/**
* 合并文件:合并由拆分文件拆分的文件
* 要求将拆分文件放到一个文件中
* 主要利用随机文件读取和文件输入输出流
*/
package ThreadDownload;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.Arrays;
import java.util.StringTokenizer;
public class FileCombination extends Thread {
public String srcDirectory = null; // 拆分文件存放的目录
public String saveDirectory; // 结果文件存放目录
public String[] separatedFiles; // 存放所有拆分文件名
public String fileRealName = ""; // 据拆分文件名确定现在原文件名
public String[][] separatedFilesAndSize; // 存放所有拆分文件名及分件大小
public int FileNum = 0; // 确定文件个数
/**
* @param srcDirectory 拆分文件存放的目录
* @param saveDirectory 结果文件存放目录
*/
public FileCombination(String trueDirectory, String srcDirectory) {
super();
this.srcDirectory = srcDirectory;
this.saveDirectory = trueDirectory;
}
/**
* @param sFileName 任意一个拆分文件名
* @return 原文件名
*/
private String getRealName(String sFileName) {
StringTokenizer st = new StringTokenizer(sFileName, ".");
return st.nextToken() + "." + st.nextToken();
}
/**
* @param FileName 拆分的文件名
* @return 取得指定拆分文件模块的文件大小
*/
private long getFileSize(String FileName) {
FileName = srcDirectory + "//" + FileName;
return (new File(FileName).length());
}
/**
* 生成一些属性,做初使化
*
* @param drictory 拆分文件属性
*/
private void getFileAttribute(String drictory) {
File file = new File(drictory);
separatedFiles = new String[file.list().length];// 依文件数目动态生成一维数组,只有文件名
separatedFiles = file.list();
// 依文件数目动态生成二维数组,包括文件名和文件大小
// 第一维装文件名,第二维为该文件的字节大小
separatedFilesAndSize = new String[separatedFiles.length][2];
Arrays.sort(separatedFiles); // 排序
FileNum = separatedFiles.length; // 当前文件夹下面有多少个文件
for (int i = 0; i < FileNum; i++) {
separatedFilesAndSize[i][0] = separatedFiles[i]; // 文件名
separatedFilesAndSize[i][1] = String
.valueOf(getFileSize(separatedFiles[i])); // 文件大小
}
fileRealName = getRealName(separatedFiles[FileNum - 1]); // 取得文件分隔前的原文件名
}
/**
* 合并文件:利用随机文件读写
*
* @return true 成功合并文件
*/
private boolean CombFile() {
RandomAccessFile raf = null;
long alreadyWrite = 0;
FileInputStream fis = null;
int len = 0;
byte[] bt = new byte[1024];
try {
raf = new RandomAccessFile(saveDirectory + "//" + fileRealName,
"rw");
for (int i = 0; i < FileNum; i++) {
raf.seek(alreadyWrite);
// System.out.println("alreadyWrite:"+alreadyWrite);
fis = new FileInputStream(srcDirectory + "//"
+ separatedFilesAndSize[i][0]);
while ((len = fis.read(bt)) > 0) {
raf.write(bt, 0, len);
}
fis.close();
alreadyWrite = alreadyWrite
+ Long.parseLong(separatedFilesAndSize[i][1]);
}
raf.close();
} catch (Exception e) {
e.printStackTrace();
try {
if (raf != null)
raf.close();
if (fis != null)
fis.close();
} catch (IOException f) {
f.printStackTrace();
}
return false;
}
return true;
}
public void deleteTmp() {
for (int i = 0; i < FileNum; i++) {
File file = new File(srcDirectory + "//"
+ separatedFilesAndSize[i][0]);
file.delete();
}
File file1 = new File(srcDirectory);
file1.delete();
}
public void run() {
getFileAttribute(srcDirectory);
CombFile();
deleteTmp();
}
}
代码测试:
第一次下载
part1: 0--->4054021, size: 4054021
part2: 4054021--->8108042, size: 4054021
part3: 8108042--->12162063, size: 4054021
part4: 12162063--->16216084, size: 4054021
part5: 16216084--->20270105, size: 4054021
part6: 20270105--->24324126, size: 4054021
part7: 24324126--->28378147, size: 4054021
part8: 28378147--->32432168, size: 4054021
part9: 32432168--->36486190, size: 4054022
线程3正在接收文件...
线程2正在接收文件...
线程9正在接收文件...
线程6正在接收文件...
线程7正在接收文件...
线程4正在接收文件...
线程1正在接收文件...
线程5正在接收文件...
线程8正在接收文件...
第二次下载
part1: 0--->4054021, size: 4054021
part2: 4054021--->8108042, size: 4054021
part3: 8108042--->12162063, size: 4054021
线程1文件正在续传...
线程2文件正在续传...
part4: 12162063--->16216084, size: 4054021
part5: 16216084--->20270105, size: 4054021
线程3文件正在续传...
part6: 20270105--->24324126, size: 4054021
线程5文件正在续传...
part7: 24324126--->28378147, size: 4054021
part8: 28378147--->32432168, size: 4054021
线程6文件正在续传...
part9: 32432168--->36486190, size: 4054022
线程7文件正在续传...
线程4文件正在续传...
线程9文件正在续传...
线程8文件正在续传...
线程5文件续传完成...
线程9文件续传完成...
线程7文件续传完成...
线程6文件续传完成...
线程3文件续传完成...
线程2文件续传完成...
线程1文件续传完成...
线程4文件续传完成...
线程8文件续传完成...