在项目开发过程中,需要对服务器上的某些文件进行下载,最好还是多线程的,网查查阅一下资料,整理两个可以用的案例,特此记录。两个案例分别采用文件流的方式和RandomAccessFile实现。
一、文件流操作
DownloadManager:下载管理,是总线程,在此线程中开启多个分线程进行文件下载
public class DownloadManager implements Runnable {
// 保存路径
private String savePath;
// 总的下载线程数
private int threadNum;
// 下载的链接地址
private String urlFile;
// 是否下载开始
private boolean isStarted;
// 用于监视何时合并文件存放Thread的list
private List<DownloadThread> downloadList = new ArrayList<DownloadThread>();
public DownloadManager(String savePath, int threadNum, String urlFile) {
super();
this.savePath = savePath;
this.threadNum = threadNum;
this.urlFile = urlFile;
}
// 最终调用线程下载。本线程中调用分线程。
public void action() {
new Thread(this).start();
}
@Override
public void run() {
long t1 = System.currentTimeMillis();
System.out.println(t1);
// 如果没有下载 , 就开始 , 并且将已经下载的变量值设为true
if (!isStarted) {
startDownload();
isStarted = true;
}
while (true) {
// 初始化认为所有线程下载完成,逐个检查
boolean finish = true;
// 如果有任何一个没完成,说明下载没完成,不能合并文件
for (DownloadThread thread : downloadList) {
if (!thread.isFinish()) {
finish = false;
break;
}
}
// 全部下载完成才为真
if (finish) {
// 合并文件
mergeFiles();
// 跳出循环 , 下载结束
break;
}
// 休息一会 , 减少cpu消耗
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long t2 = System.currentTimeMillis();
System.out.println(t2);
System.out.println("下载用时:" + (t2 -t1));
}
public void startDownload() {
// 得到每个线程开始值 , 下载字节数大小
int[][] posAndLength = getPosAndLength();
// 根据下载信息创建每个下载线程,并且启动他们。
for (int i = 0; i < posAndLength.length; i++) {
int pos = posAndLength[i][0];
int length = posAndLength[i][1];
DownloadThread downloadThread = new DownloadThread(i + 1, length,
pos, savePath, urlFile);
new Thread(downloadThread).start();
downloadList.add(downloadThread);
}
}
/**
* 获得文件大小
*
* @return 文件大小
*/
public long getFileLength() {
System.out.println("获得文件大小 start......");
HttpURLConnection conn = null;
long result = 0;
try {
URL url = new URL(urlFile);
conn = (HttpURLConnection) url.openConnection();
// 使用Content-Length头信息获得文件大小
result = Long.parseLong(conn.getHeaderField("Content-Length"));
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.disconnect();
}
}
System.out.println("获得文件大小 end......" + result);
return result;
}
// 具体细节求出每个线程的开始位置和文件下载大小
public int[][] getPosAndLength() {
int[][] result = new int[threadNum][2];
int fileLength = (int) getFileLength();
int every = fileLength % threadNum == 0 ? fileLength / threadNum
: fileLength / threadNum + 1;
for (int i = 0; i < result.length; i++) {
int length = 0;
if (i != result.length - 1) {
length = every;
} else {
length = fileLength - i * every;
}
result[i][0] = i * every;
result[i][1] = length;
}
return result;
}
// 合并文件
public void mergeFiles() {
System.out.println("合并文件 start......");
OutputStream out = null;
try {
out = new FileOutputStream(savePath);
for (int i = 1; i <= threadNum; i++) {
InputStream in = new FileInputStream(savePath + i);
byte[] bytes = new byte[2048];
int read = 0;
while ((read = in.read(bytes)) != -1) {
out.write(bytes, 0, read);
out.flush();
}
if (in != null) {
in.close();
new File(savePath + i).delete();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
System.out.println("合并文件 end......");
}
public String getSavePath() {
return savePath;
}
public void setSavePath(String savePath) {
this.savePath = savePath;
}
public int getThreadNum() {
return threadNum;
}
public void setThreadNum(int threadNum) {
this.threadNum = threadNum;
}
public String getUrlFile() {
return urlFile;
}
public void setUrlFile(String urlFile) {
this.urlFile = urlFile;
}
public boolean isStarted() {
return isStarted;
}
public void setStarted(boolean isStarted) {
this.isStarted = isStarted;
}
public List<DownloadThread> getDownloadList() {
return downloadList;
}
public void setDownloadList(List<DownloadThread> downloadList) {
this.downloadList = downloadList;
}
}
DownloadThread:分线程,实现Runnable接口,重写run()方法进行文件下载
public class DownloadThread implements Runnable {
// 当前第几个线程 , 用于给下载文件起名 file1 file2 file3 ...
private int whichThread;
// 监听单一线程下载是否完成
private boolean isFinish;
// 本线程要下载的文件字节数
private int length;
// 本线程向服务器发送请求时输入流的首位置
private int startPosition;
// 保存的路径
private String savePath;
// 要下载的文件 , 用于创建连接
private String url;
@Override
public void run() {
HttpURLConnection conn = null;
InputStream in = null;
OutputStream out = null;
try {
System.out.println("正在执行的线程:" + whichThread);
URL fileUrl = new URL(url);
// 与服务器创建连接
conn = (HttpURLConnection) fileUrl.openConnection();
// 下载使用get请求
conn.setRequestMethod("GET");
// 告诉服务器 , 我是火狐 , 不要不让我下载。
conn.setRequestProperty(
"User-Agent",
"Firefox Mozilla/5.0 (Windows; U; Windows NT 5.1; zh-CN; rv:1.9.1.3) Gecko/20090824 Firefox/3.5.3");
// 这里是设置文件输入流的首位置
conn.setRequestProperty("Range", "bytes=" + startPosition + "-");
// 与服务器创建连接
conn.connect();
// 获得输入流
in = conn.getInputStream();
// 在硬盘上创建file1 , file2 , ...这样的文件 , 准备往里面写东西
out = new FileOutputStream(savePath + whichThread);
// 用于写入的字节数组
byte[] bytes = new byte[4096];
// 一共下载了多少字节
int count = 0;
// 单次读取的字节数
int read = 0;
while ((read = in.read(bytes)) != -1) {
// 检查一下是不是下载到了本线程需要的长度
if (length - count < bytes.length) {
// 比如说本线程还需要900字节,但是已经读取1000
// 字节,则用要本线程总下载长度减去
// 已经下载的长度
read = length - count;
}
// 将准确的字节写入输出流
out.write(bytes, 0, read);
// 已经下载的字节数加上本次循环字节数
count = count + read;
// 如果下载字节达到本线程所需要字节数,消除循环,
// 停止下载
if (count == length) {
break;
}
}
// 将监视变量设置为true
isFinish = true;
} catch (Exception e) {
e.printStackTrace();
} finally {
// 最后进行输入、输出、连接的关闭
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (conn != null) {
conn.disconnect();
}
}
}
public int getStartPosition() {
return startPosition;
}
public void setStartPosition(int startPosition) {
this.startPosition = startPosition;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public int getWhichThread() {
return whichThread;
}
public void setWhichThread(int whichThread) {
this.whichThread = whichThread;
}
public int getLength() {
return length;
}
public void setLength(int length) {
this.length = length;
}
public String getSavePath() {
return savePath;
}
public void setSavePath(String savePath) {
this.savePath = savePath;
}
public DownloadThread(int whichThread, int length, int startPosition,
String savePath, String url) {
super();
this.whichThread = whichThread;
this.length = length;
this.startPosition = startPosition;
this.savePath = savePath;
this.url = url;
}
public DownloadThread() {
super();
}
public boolean isFinish() {
return isFinish;
}
public void setFinish(boolean isFinish) {
this.isFinish = isFinish;
}
}
TestDownload:测试,开启总线程。
public class TestDownload {
public static void main(String[] args) {
DownloadManager downloadManager = new DownloadManager("e:/test.png" , 8 , "http://localhost:8080/test.png");
downloadManager.action();
}
}
二、RandomAccessFile 操作
RandomAccessFile是Java语言中功能最为丰富的文件访问类,它提供了众多的文件访问方法,有兴趣的同学请自行百度
DownloadThread :继承Thread类,重写run()方法下载文件
/**
* 下载文件的子线程 每一个文件都下载对应的数据
* @author YUANYUAN
*
*/
public class DownloadThread extends Thread{
private String path;
private int threadId;
private int startIndex;
private int endIndex;
//下载所使用的线程数
private static int threadCount=8;
//当前活动的线程数
private static int activeThread;
/**
* 构造方法
* @param path 下载文件的路径
* @param threadId 下载文件的线程
* @param startIndex 下载文件开始的位置
* @param endIndex 下载文件结束的位置
*/
public DownloadThread(String path, int threadId, int startIndex,
int endIndex) {
this.path = path;
this.threadId = threadId;
this.startIndex = startIndex;
this.endIndex = endIndex;
}
@Override
public void run() {
//构造URL地址
try {
File tempFile=new File(threadId+".txt");
//检查记录是否存在,如果存在读取数据
if (tempFile.exists()) {
FileInputStream fis=new FileInputStream(tempFile);
byte[] temp=new byte[1024];
int length=fis.read(temp);
//读取到已经下载的位置
int downloadNewIndex=Integer.parseInt(new String(temp, 0, length));
//设置重新开始下载的开始位置
startIndex=downloadNewIndex;
fis.close();
//显示真实下载数据的区间
System.out.println("线程【"+threadId+"】真实开始下载数据区间:"+startIndex+"---->"+endIndex);
}
URL url = new URL(path);
HttpURLConnection conn=(HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
conn.setRequestMethod("GET");
//设置请求属性,请求部分资源
conn.setRequestProperty("Range", "bytes="+startIndex+"-"+endIndex);
int code=conn.getResponseCode();
if (code==206) {//下载部分资源,正常返回的状态码为206
InputStream is=conn.getInputStream();//已经设置了请求的位置,所以返回的是对应的部分资源
//构建随机访问文件
RandomAccessFile raf=new RandomAccessFile("test.png", "rwd");
//设置 每一个线程随机写文件开始的位置
raf.seek(startIndex);
//开始写文件
int len=0;
byte[] buffer=new byte[1024];
//该线程已经下载数据的长度
int total=0;
while((len=is.read(buffer))!=-1){//读取输入流
//记录当前线程已下载数据的长度
RandomAccessFile file=new RandomAccessFile(threadId+".txt","rwd");
raf.write(buffer,0,len);//写文件
total+=len;//更新该线程已下载数据的总长度
System.out.println("线程【"+threadId+"】已下载数据:"+(total+startIndex));
//将已下载数据的位置记录写入到文件
file.write((startIndex+total+"").getBytes());
file.close();
}
is.close();
raf.close();
//提示下载完毕
System.out.println("线程【"+threadId+"】下载完毕");
}
} catch (Exception e) {
e.printStackTrace();
System.out.println("线程【"+threadId+"】下载出现异常!!");
}finally{
//活动的线程数减少
activeThread--;
if (activeThread==0) {
for (int i = 1; i <= threadCount; i++) {
File tempFile=new File(i+".txt");
tempFile.delete();
}
System.out.println("下载完毕,已清除全部临时文件");
}
}
}
}
TestDownload:测试类
public class TestDownload {
//下载所使用的线程数
private static int threadCount=8;
//当前活动的线程数
private static int activeThread;
public static void main(String[] args) throws Exception{
//请求服务器的路径
String path="http://localhost:8080/test.png";
//构造URL地址
URL url=new URL(path);
//打开连接
HttpURLConnection conn=(HttpURLConnection) url.openConnection();
//设置请求超时的时间
conn.setConnectTimeout(5000);
//设置请求方式
conn.setRequestMethod("GET");
//获取相应码
int code=conn.getResponseCode();
if (code==200) {//请求成功
//获取请求数据的长度
int length=conn.getContentLength();
//在客户端创建一个跟服务器文件大小相同的临时文件
RandomAccessFile raf=new RandomAccessFile("test.png", "rwd");
//指定临时文件的长度
raf.setLength(length);
raf.close();
//假设3个线程去下载资源
//平均每一个线程要下载的文件的大小
int blockSize=length/threadCount;
for (int threadId = 1; threadId <= threadCount; threadId++) {
//当前线程下载数据的开始位置
int startIndex=blockSize*(threadId-1);
//当前线程下载数据的结束位置
int endIndex=blockSize*threadId-1;
//确定最后一个线程要下载数据的最大位置
if (threadId==threadCount) {
endIndex=length;
}
//显示下载数据的区间
System.out.println("线程【"+threadId+"】开始下载:"+startIndex+"---->"+endIndex);
//开启下载的子线程
new DownloadThread(path, threadId, startIndex, endIndex).start();
activeThread++;
System.out.println("当前活动的线程数:"+activeThread);
}
}else{//请求失败
System.out.println("服务器异常,下载失败!");
}
}
}
三、两者结合,访问接口写法
DownloadController:服务器接口
@RestController
public class DownloadController {
@RequestMapping(value = "/download", method = RequestMethod.GET)
public void getDownload(String url, HttpServletRequest request, HttpServletResponse response) throws FileNotFoundException {
//获取文件名称
String fileName = url.substring(url.indexOf("/",url.indexOf("/",url.indexOf("/",url.indexOf("/")+1)+1))+1);
File downloadFile = ResourceUtils.getFile("classpath:static/"+fileName);
ServletContext context = request.getServletContext();
// 获取文件类型
String mimeType = context.getMimeType(url);
if (mimeType == null) {
// 设置二进制类型
mimeType = "application/octet-stream";
}
// 设置响应内容类型
response.setContentType(mimeType);
// 设置响应头
String headerKey = "Content-Disposition";
String headerValue = String.format("attachment; filename=\"%s\"", downloadFile.getName());
response.setHeader(headerKey, headerValue);
// 解析断点续传相关信息
response.setHeader("Accept-Ranges", "bytes");
long downloadSize = downloadFile.length();
long fromPos = 0, toPos = 0;
if (request.getHeader("Range") == null) {
response.setHeader("Content-Length", downloadSize + "");
} else {
// 若客户端传来Range,说明之前下载了一部分,设置206状态(SC_PARTIAL_CONTENT)
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
String range = request.getHeader("Range");
String bytes = range.replaceAll("bytes=", "");
String[] ary = bytes.split("-");
fromPos = Long.parseLong(ary[0]);
if (ary.length == 2) {
toPos = Long.parseLong(ary[1]);
}
int size;
if (toPos > fromPos) {
size = (int) (toPos - fromPos);
} else {
size = (int) (downloadSize - fromPos);
}
response.setHeader("Content-Length", size + "");
downloadSize = size;
}
//复制内容到响应流
RandomAccessFile in = null;
OutputStream out = null;
try {
in = new RandomAccessFile(downloadFile, "rw");
// 设置下载起始位置
if (fromPos > 0) {
in.seek(fromPos);
}
// 缓冲区大小
int bufLen = (int) (downloadSize < 2048 ? downloadSize : 2048);
byte[] buffer = new byte[bufLen];
int num;
int count = 0; // 当前写到客户端的大小
out = response.getOutputStream();
while ((num = in.read(buffer)) != -1) {
out.write(buffer, 0, num);
count += num;
//处理最后一段,计算不满缓冲区的大小
if (downloadSize - count < bufLen) {
bufLen = (int) (downloadSize-count);
if(bufLen==0){
break;
}
buffer = new byte[bufLen];
}
}
response.flushBuffer();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != out) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != in) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
TestDownload:测试类
public class TestDownload {
//下载所使用的线程数
private static int threadCount=3;
public static void main(String[] args) throws Exception{
URL url=new URL("http://localhost:8080/idea.jpeg");
//打开连接
HttpURLConnection conn=(HttpURLConnection) url.openConnection();
//设置请求超时的时间
conn.setConnectTimeout(5000);
//设置请求方式
conn.setRequestMethod("GET");
//获取相应码
int code=conn.getResponseCode();
int length;
if (code==200) {//请求成功
length=conn.getContentLength();
int blockSize=length/threadCount;
for (int threadId = 1; threadId <= threadCount; threadId++) {
//当前线程下载数据的开始位置
long startIndex=blockSize*(threadId-1);
//当前线程下载数据的结束位置
long endIndex=blockSize*threadId-1;
//确定最后一个线程要下载数据的最大位置
if (threadId==threadCount) {
endIndex=length;
}
//显示下载数据的区间
System.out.println("线程【"+threadId+"】开始下载:"+startIndex+"---->"+endIndex);
//开启下载的子线程
new DownloadThread("http://localhost:8080/download?url=http://localhost:8080/idea.jpeg", startIndex, endIndex,"E://file.jpeg").start();
}
}
}
public static class DownloadThread extends Thread{
private String url;
private long from;
private long to;
private String savePath;
public DownloadThread(String url, long from,
long to, String savePath) {
this.url = url;
this.from = from;
this.to = to;
this.savePath = savePath;
}
@Override
public void run() {
try {
URL link = new URL(url);
HttpURLConnection conn = (HttpURLConnection) link.openConnection();
// 设置断点续传的开始位置
if (to != 0) {
conn.setRequestProperty("Range", "bytes=" + from + "-" + to);
}else{
conn.setRequestProperty("Range", "bytes=" + from + "-");
}
if (conn.getResponseCode() == 206) {
RandomAccessFile file = new RandomAccessFile(savePath, "rw");
file.seek(from);
InputStream in = conn.getInputStream();
byte[] buffer = new byte[1024];
int num;
while ((num = in.read(buffer)) > 0) {
file.write(buffer, 0, num);
}
file.close();
in.close();
}
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}