Java 多线程下载技术实现
来源:互联网 发布:淘宝网div css布局实例 编辑:程序博客网 时间:2024/05/22 14:57
多线程下载
多线程下载技术,简单的说就是把要下载的文件分成几块,由不同的线程来负责每一块数据的下载任务。
技术要点
- RandomAccessFile:
Java中用来实现随机访问文件的类 - http Range请求头
具体思路
1、文件分块。 文件分块大小(blockSize)= (文件大小 +线程数 - 1 )/ 线程数 ;
2、确定每一个线程所要下载的 文件的起始和结束位置。
现假设为每个线程分别编号:0,1, 2,3;则
第一个线程负责的下载位置是: 0*blockSize - (0+1)*blockSize -1,
第二个线程负责的下载位置是: 1*blockSize - (1+1)*blockSize -1,
以此类推第i个线程负责的下载位置是:i*blockSize - (i+1)*blockSize -1;
即线程(编号为id)下载开始位置 start = id*block;
即线程(编号为id)下载结束位置 end = (id+1)*block -1;
3、设置http 请求头, conn.setRequestProperty(“Range”, “bytes=” + start + “-” + end);
代码实现
一个简单的Java多线程下载代码如下:
package com.ricky.java.test.download;import java.io.File;import java.io.IOException;import java.io.InputStream;import java.io.RandomAccessFile;import java.net.HttpURLConnection;import java.net.URL;public class Downloader { private URL url; // 目标地址 private File file; // 本地文件 private static final int THREAD_AMOUNT = 8; // 线程数 private static final String DOWNLOAD_DIR_PATH = "D:/Download"; // 下载目录 private int threadLen; // 每个线程下载多少 public Downloader(String address, String filename) throws IOException { // 通过构造函数传入下载地址 url = new URL(address); File dir = new File(DOWNLOAD_DIR_PATH); if(!dir.exists()){ dir.mkdirs(); } file = new File(dir, filename); } public void download() throws IOException { HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(5000); int totalLen = conn.getContentLength(); // 获取文件长度 threadLen = (totalLen + THREAD_AMOUNT - 1) / THREAD_AMOUNT; // 计算每个线程要下载的长度 System.out.println("totalLen="+totalLen+",threadLen:"+threadLen); RandomAccessFile raf = new RandomAccessFile(file, "rws"); // 在本地创建一个和服务端大小相同的文件 raf.setLength(totalLen); // 设置文件的大小 raf.close(); for (int i = 0; i < THREAD_AMOUNT; i++) // 开启3条线程, 每个线程下载一部分数据到本地文件中 new DownloadThread(i).start(); } private class DownloadThread extends Thread { private int id; public DownloadThread(int id) { this.id = id; } public void run() { int start = id * threadLen; // 起始位置 int end = id * threadLen + threadLen - 1; // 结束位置 System.out.println("线程" + id + ": " + start + "-" + end); try { HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(5000); conn.setRequestProperty("Range", "bytes=" + start + "-" + end); // 设置当前线程下载的范围 InputStream in = conn.getInputStream(); RandomAccessFile raf = new RandomAccessFile(file, "rws"); raf.seek(start); // 设置保存数据的位置 byte[] buffer = new byte[1024]; int len; while ((len = in.read(buffer)) != -1) raf.write(buffer, 0, len); raf.close(); System.out.println("线程" + id + "下载完毕"); } catch (IOException e) { e.printStackTrace(); } } } public static void main(String[] args) throws IOException { String address = "http://dldir1.qq.com/qqfile/qq/QQ7.9/16621/QQ7.9.exe"; new Downloader(address, "QQ7.9.exe").download();// String address = "http://api.t.dianping.com/n/api.xml?cityId=2";// new Downloader(address, "2.xml").download(); }}
封装多线程下载
文件下载是一个常用的模块,我们可以对其封装一下,方便以后调用。涉及到的开发技术如下:
- JDK 1.7
- Eclipse Juno
- Maven 3
- HttpClient 4.3.6
工程目录结构如下所示:
com.ricky.common.java.download.FileDownloader
package com.ricky.common.java.download;import org.apache.log4j.Logger;import com.ricky.common.java.download.config.FileDownloaderConfiguration;/** * Java 文件多线程下载 * @author Ricky Fung * */public class FileDownloader { protected Logger mLogger = Logger.getLogger("devLog"); private volatile static FileDownloader fileDownloader; private FileDownloaderEngine downloaderEngine; private FileDownloaderConfiguration configuration; public static FileDownloader getInstance(){ if(fileDownloader==null){ synchronized (FileDownloader.class) { if(fileDownloader==null){ fileDownloader = new FileDownloader(); } } } return fileDownloader; } protected FileDownloader(){ } public synchronized void init(FileDownloaderConfiguration configuration){ if (configuration == null) { throw new IllegalArgumentException("FileDownloader configuration can not be initialized with null"); } if (this.configuration == null) { mLogger.info("init FileDownloader"); downloaderEngine = new FileDownloaderEngine(configuration); this.configuration = configuration; }else{ mLogger.warn("Try to initialize FileDownloader which had already been initialized before."); } } public boolean download(String url, String filename){ return downloaderEngine.download(url, filename); } public boolean isInited() { return configuration != null; } public void destroy() { if(downloaderEngine!=null){ downloaderEngine.close(); downloaderEngine = null; } }}
com.ricky.common.java.download.config.FileDownloaderConfiguration
package com.ricky.common.java.download.config;import java.io.File;public class FileDownloaderConfiguration { private final int connectTimeout; private final int socketTimeout; private final int maxRetryCount; private final int coreThreadNum; private final long requestBytesSize; private final File downloadDestinationDir; private FileDownloaderConfiguration(Builder builder) { this.connectTimeout = builder.connectTimeout; this.socketTimeout = builder.socketTimeout; this.maxRetryCount = builder.maxRetryCount; this.coreThreadNum = builder.coreThreadNum; this.requestBytesSize = builder.requestBytesSize; this.downloadDestinationDir = builder.downloadDestinationDir; } public int getConnectTimeout() { return connectTimeout; } public int getSocketTimeout() { return socketTimeout; } public int getMaxRetryCount() { return maxRetryCount; } public int getCoreThreadNum() { return coreThreadNum; } public long getRequestBytesSize() { return requestBytesSize; } public File getDownloadDestinationDir() { return downloadDestinationDir; } public static FileDownloaderConfiguration.Builder custom() { return new Builder(); } public static class Builder { private int connectTimeout; private int socketTimeout; private int maxRetryCount; private int coreThreadNum; private long requestBytesSize; private File downloadDestinationDir; public Builder connectTimeout(int connectTimeout) { this.connectTimeout = connectTimeout; return this; } public Builder socketTimeout(int socketTimeout) { this.socketTimeout = socketTimeout; return this; } public Builder coreThreadNum(int coreThreadNum) { this.coreThreadNum = coreThreadNum; return this; } public Builder maxRetryCount(int maxRetryCount) { this.maxRetryCount = maxRetryCount; return this; } public Builder requestBytesSize(long requestBytesSize) { this.requestBytesSize = requestBytesSize; return this; } public Builder downloadDestinationDir(File downloadDestinationDir) { this.downloadDestinationDir = downloadDestinationDir; return this; } public FileDownloaderConfiguration build() { initDefaultValue(this); return new FileDownloaderConfiguration(this); } private void initDefaultValue(Builder builder) { if(builder.connectTimeout<1){ builder.connectTimeout = 6*1000; } if(builder.socketTimeout<1){ builder.socketTimeout = 6*1000; } if(builder.maxRetryCount<1){ builder.maxRetryCount = 1; } if(builder.coreThreadNum<1){ builder.coreThreadNum = 3; } if(builder.requestBytesSize<1){ builder.requestBytesSize = 1024*128; } if(builder.downloadDestinationDir==null){ builder.downloadDestinationDir = new File("./"); } } } }
com.ricky.common.java.download.FileDownloaderEngine
package com.ricky.common.java.download;import java.io.File;import java.io.FileNotFoundException;import java.io.IOException;import java.io.RandomAccessFile;import java.util.BitSet;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import org.apache.log4j.Logger;import com.ricky.common.java.download.config.FileDownloaderConfiguration;import com.ricky.common.java.download.job.DownloadWorker;import com.ricky.common.java.download.job.Worker.DownloadListener;public class FileDownloaderEngine { protected Logger mLogger = Logger.getLogger("devLog"); private FileDownloaderConfiguration configuration; private ExecutorService pool; private HttpRequestImpl httpRequestImpl; private File downloadDestinationDir; private int coreThreadNum; public FileDownloaderEngine(FileDownloaderConfiguration configuration){ this.configuration = configuration; this.coreThreadNum = configuration.getCoreThreadNum(); this.httpRequestImpl = new HttpRequestImpl(this.configuration); this.pool = Executors.newFixedThreadPool(this.configuration.getCoreThreadNum()); this.downloadDestinationDir = this.configuration.getDownloadDestinationDir(); if(!this.downloadDestinationDir.exists()){ this.downloadDestinationDir.mkdirs(); } } public boolean download(String url, String filename){ long start_time = System.currentTimeMillis(); mLogger.info("开始下载,url:"+url+",filename:"+filename); long total_file_len = httpRequestImpl.getFileSize(url); // 获取文件长度 if(total_file_len<1){ mLogger.warn("获取文件大小失败,url:"+url+",filename:"+filename); return false; } final BitSet downloadIndicatorBitSet = new BitSet(coreThreadNum); //标记每个线程下载是否成功 File file = null; try { file = new File(downloadDestinationDir, filename); RandomAccessFile raf = new RandomAccessFile(file, "rws"); // 在本地创建一个和服务端大小相同的文件 raf.setLength(total_file_len); // 设置文件的大小 raf.close(); mLogger.info("create new file:"+file); } catch (FileNotFoundException e) { mLogger.error("create new file error", e); } catch (IOException e) { mLogger.error("create new file error", e); } if(file==null || !file.exists()){ mLogger.warn("创建文件失败,url:"+url+",filename:"+filename); return false; } long thread_download_len = (total_file_len + coreThreadNum - 1) / coreThreadNum; // 计算每个线程要下载的长度 mLogger.info("filename:"+filename+",total_file_len="+total_file_len+",coreThreadNum:"+coreThreadNum+",thread_download_len:"+thread_download_len); CountDownLatch latch = new CountDownLatch(coreThreadNum);//两个工人的协作 for (int i = 0; i < coreThreadNum; i++){ DownloadWorker worker = new DownloadWorker(i, url, thread_download_len, file, httpRequestImpl, latch); worker.addListener(new DownloadListener() { @Override public void notify(int thread_id, String url, long start, long end, boolean result, String msg) { mLogger.info("thread_id:"+thread_id+" download result:"+result+",url->"+url); modifyState(downloadIndicatorBitSet, thread_id); } }); pool.execute(worker); } try { latch.await(); } catch (InterruptedException e) { mLogger.error("CountDownLatch Interrupt", e); } mLogger.info("下载结束,url:"+url+",耗时:"+((System.currentTimeMillis()-start_time)/1000)+"(s)"); return downloadIndicatorBitSet.cardinality()==coreThreadNum; } private synchronized void modifyState(BitSet bitSet, int index){ bitSet.set(index); } /**释放资源*/ public void close(){ if(httpRequestImpl!=null){ httpRequestImpl.close(); httpRequestImpl = null; } if(pool!=null){ pool.shutdown(); pool = null; } }}
com.ricky.common.java.download.job.DownloadWorker
package com.ricky.common.java.download.job;import java.io.File;import java.util.concurrent.CountDownLatch;import org.apache.log4j.Logger;import com.ricky.common.java.download.HttpRequestImpl;import com.ricky.common.java.download.RetryFailedException;public class DownloadWorker extends Worker { protected Logger mLogger = Logger.getLogger("devLog"); private int id; private String url; private File file; private long thread_download_len; private CountDownLatch latch; private HttpRequestImpl httpRequestImpl; public DownloadWorker(int id, String url, long thread_download_len, File file, HttpRequestImpl httpRequestImpl, CountDownLatch latch) { this.id = id; this.url = url; this.thread_download_len = thread_download_len; this.file = file; this.httpRequestImpl = httpRequestImpl; this.latch = latch; } @Override public void run() { long start = id * thread_download_len; // 起始位置 long end = id * thread_download_len + thread_download_len - 1; // 结束位置 mLogger.info("线程:" + id +" 开始下载 url:"+url+ ",range:" + start + "-" + end); boolean result = false; try { httpRequestImpl.downloadPartFile(id, url, file, start, end); result = true; mLogger.info("线程:" + id + " 下载 "+url+ " range[" + start + "-" + end+"] 成功"); } catch (RetryFailedException e) { mLogger.error("线程:" + id +" 重试出错", e); }catch (Exception e) { mLogger.error("线程:" + id +" 下载出错", e); } if(listener!=null){ mLogger.info("notify FileDownloaderEngine download result"); listener.notify(id, url, start, end, result, ""); } latch.countDown(); }}
com.ricky.common.java.download.job.Worker
package com.ricky.common.java.download.job;public abstract class Worker implements Runnable { protected DownloadListener listener; public void addListener(DownloadListener listener){ this.listener = listener; } public interface DownloadListener{ public void notify(int thread_id, String url, long start, long end, boolean result, String msg); }}
com.ricky.common.java.download.HttpRequestImpl
package com.ricky.common.java.download;import java.io.File;import java.io.FileNotFoundException;import java.io.IOException;import java.io.InputStream;import java.io.RandomAccessFile;import org.apache.commons.io.IOUtils;import org.apache.http.HttpEntity;import org.apache.http.HttpStatus;import org.apache.http.client.ClientProtocolException;import org.apache.http.client.config.RequestConfig;import org.apache.http.client.methods.CloseableHttpResponse;import org.apache.http.client.methods.HttpGet;import org.apache.http.impl.client.CloseableHttpClient;import org.apache.log4j.Logger;import com.ricky.common.java.download.config.FileDownloaderConfiguration;import com.ricky.common.java.http.HttpClientManager;public class HttpRequestImpl { protected Logger mLogger = Logger.getLogger("devLog"); private int connectTimeout; private int socketTimeout; private int maxRetryCount; private long requestBytesSize; private CloseableHttpClient httpclient = HttpClientManager.getHttpClient(); public HttpRequestImpl(FileDownloaderConfiguration configuration){ connectTimeout = configuration.getConnectTimeout(); socketTimeout = configuration.getSocketTimeout(); maxRetryCount = configuration.getMaxRetryCount(); requestBytesSize = configuration.getRequestBytesSize(); } public void downloadPartFile(int id, String url, File file, long start, long end){ RandomAccessFile raf = null; try { raf = new RandomAccessFile(file, "rws"); } catch (FileNotFoundException e) { mLogger.error("file not found:"+file, e); throw new IllegalArgumentException(e); } int retry = 0; long pos = start; while(pos<end){ long end_index = pos + requestBytesSize; if(end_index>end){ end_index = end; } boolean success = false; try { success = requestByRange(url, raf, pos, end_index); } catch (ClientProtocolException e) { mLogger.error("download error,start:"+pos+",end:"+end_index, e); }catch (IOException e) { mLogger.error("download error,start:"+pos+",end:"+end_index, e); }catch (Exception e) { mLogger.error("download error,start:"+pos+",end:"+end_index, e); }// mLogger.info("线程:" + id +",download url:"+url+",range:"+ pos + "-" + end_index+",success="+success ); if(success){ pos += requestBytesSize; retry = 0; }else{ if(retry < maxRetryCount){ retry++; mLogger.warn("线程:" + id +",url:"+url+",range:"+pos+","+end_index+" 下载失败,重试"+retry+"次"); }else{ mLogger.warn("线程:" + id +",url:"+url+",range:"+pos+","+end_index+" 下载失败,放弃重试!"); throw new RetryFailedException("超过最大重试次数"); } } } } private boolean requestByRange(String url, RandomAccessFile raf, long start, long end) throws ClientProtocolException, IOException { HttpGet httpget = new HttpGet(url); httpget.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.152 Safari/537.36"); httpget.setHeader("Range", "bytes=" + start + "-" + end); RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(connectTimeout) .setSocketTimeout(socketTimeout) .build(); httpget.setConfig(requestConfig); CloseableHttpResponse response = null; try { response = httpclient.execute(httpget); int code = response.getStatusLine().getStatusCode(); if(code==HttpStatus.SC_OK || code== HttpStatus.SC_PARTIAL_CONTENT){ HttpEntity entity = response.getEntity(); if (entity != null) { InputStream in = entity.getContent(); raf.seek(start);// 设置保存数据的位置 byte[] buffer = new byte[1024]; int len; while ((len = in.read(buffer)) != -1){ raf.write(buffer, 0, len); } return true; }else{ mLogger.warn("response entity is null,url:"+url); } }else{ mLogger.warn("response error, code="+code+",url:"+url); } }finally { IOUtils.closeQuietly(response); } return false; } public long getFileSize(String url){ int retry = 0; long filesize = 0; while(retry<maxRetryCount){ try { filesize = getContentLength(url); } catch (Exception e) { mLogger.error("get File Size error", e); } if(filesize>0){ break; }else{ retry++; mLogger.warn("get File Size failed,retry:"+retry); } } return filesize; } private long getContentLength(String url) throws ClientProtocolException, IOException{ HttpGet httpget = new HttpGet(url); httpget.setHeader("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.152 Safari/537.36"); RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(connectTimeout) .setSocketTimeout(socketTimeout) .build(); httpget.setConfig(requestConfig); CloseableHttpResponse response = null; try { response = httpclient.execute(httpget); int code = response.getStatusLine().getStatusCode(); if(code==HttpStatus.SC_OK){ HttpEntity entity = response.getEntity(); if (entity != null) { return entity.getContentLength(); } }else{ mLogger.warn("response code="+code); } }finally { IOUtils.closeQuietly(response); } return -1; } public void close(){ if(httpclient!=null){ try { httpclient.close(); } catch (IOException e) { e.printStackTrace(); } httpclient = null; } }}
最后是客户端调用代码
package com.ricky.common.java;import java.io.File;import com.ricky.common.java.download.FileDownloader;import com.ricky.common.java.download.config.FileDownloaderConfiguration;public class FileDownloaderTest { public static void main(String[] args) { FileDownloader fileDownloader = FileDownloader.getInstance(); FileDownloaderConfiguration configuration = FileDownloaderConfiguration .custom() .coreThreadNum(5) .downloadDestinationDir(new File("D:/Download")) .build(); fileDownloader.init(configuration); String url = "http://dldir1.qq.com/qqfile/qq/QQ7.9/16621/QQ7.9.exe";; String filename = "QQ7.9.exe"; boolean result = fileDownloader.download(url, filename); System.out.println("download result:"+result); fileDownloader.destroy(); //close it when you not need }}
源代码
https://github.com/TiFG/FileDownloader
- java实现多线程下载技术
- Java 多线程下载技术实现
- java实现多线程下载
- JAVA实现多线程下载
- java实现多线程下载
- java实现多线程下载
- Java多线程下载实现
- Java-实现多线程下载
- Java实现多线程下载
- Java实现多线程下载
- 【Java】Java实现多线程下载
- 【Java】Java实现多线程下载
- java实现http多线程下载
- Java实现多线程下载、断点续传
- Java多线程实现下载功能
- JAVA多线程下载的实现
- Java实现多线程断点下载
- JAVA实现多线程断点下载
- spring-boot结合logback日志框架
- 来我博客看看
- linux时间类型localtime_r (转载)
- C++ 输入输出运算符重载
- 设计模式-状态模式
- Java 多线程下载技术实现
- 点击图片放大查看
- jquery eval解析JSON中的注意点介绍
- maven上找不到的包到maven库里去搜
- Objective-C特有类型——id
- Retrofit之Query注解
- OSG多线程,多显示场景图形设计
- iOS 当页面中没有数据的时候出现的一个指向创建按钮的 UIView
- Zigbee协议按键触发流程