天天看点

Java 多线程断点下载文件_详解

基本原理:利用URLConnection获取要下载文件的长度、头部等相关信息,并设置响应的头部信息。并且通过URLConnection获取输入流,将文件分成指定的块,每一块单独开辟一个线程完成数据的读取、写入。通过输入流读取下载文件的信息,然后将读取的信息用RandomAccessFile随机写入到本地文件中。同时,每个线程写入的数据都文件指针也就是写入数据的长度,需要保存在一个临时文件中。这样当本次下载没有完成的时候,下次下载的时候就从这个文件中读取上一次下载的文件长度,然后继续接着上一次的位置开始下载。并且将本次下载的长度写入到这个文件中。

个人博客:

<a href="http://hoojo.cnblogs.com/">http://hoojo.cnblogs.com</a>

<a href="http://blog.csdn.net/IBM_hoojo">http://blog.csdn.net/IBM_hoojo</a>

封装即将下载资源的信息

package com.hoo.entity; 

/**

* &lt;b&gt;function:&lt;/b&gt; 下载文件信息类

* @author hoojo

* @createDate 2011-9-21 下午05:14:58

* @file DownloadInfo.java

* @package com.hoo.entity

* @project MultiThreadDownLoad

* @blog http://blog.csdn.net/IBM_hoojo

* @email [email protected]

* @version 1.0

*/ 

public class DownloadInfo { 

    //下载文件url 

    private String url; 

    //下载文件名称 

    private String fileName; 

    //下载文件路径 

    private String filePath; 

    //分成多少段下载, 每一段用一个线程完成下载 

    private int splitter; 

    //下载文件默认保存路径 

    private final

static String FILE_PATH = "C:/temp"; 

    //默认分块数、线程数 

static int SPLITTER_NUM =

5; 

    public DownloadInfo() { 

        super(); 

    } 

    /**

     * @param url 下载地址

     */ 

    public DownloadInfo(String url) { 

        this(url,

null, null, SPLITTER_NUM); 

     * @param url 下载地址url

     * @param splitter 分成多少段或是多少个线程下载

    public DownloadInfo(String url,

int splitter) { 

null, null, splitter); 

    /***

     * @param fileName 文件名称

     * @param filePath 文件保存路径

    public DownloadInfo(String url, String fileName, String filePath,

        if (url == null ||

"".equals(url)) { 

            throw

new RuntimeException("url is not null!"); 

        } 

        this.url =  url; 

        this.fileName = (fileName ==

null || "".equals(fileName)) ? getFileName(url) : fileName; 

        this.filePath = (filePath ==

null || "".equals(filePath)) ? FILE_PATH : filePath; 

        this.splitter = (splitter &lt;

1) ? SPLITTER_NUM : splitter; 

     * &lt;b&gt;function:&lt;/b&gt; 通过url获得文件名称

     * @author hoojo

     * @createDate 2011-9-30 下午05:00:00

     * @param url

     * @return

    private String getFileName(String url) { 

        return url.substring(url.lastIndexOf("/") +

1, url.length()); 

    public String getUrl() { 

        return url; 

    public void setUrl(String url) { 

        if (url ==

null || "".equals(url)) { 

            throw new RuntimeException("url is not null!"); 

        this.url = url; 

    public String getFileName() { 

        return fileName; 

    public void setFileName(String fileName) { 

    public String getFilePath() { 

        return filePath; 

    public void setFilePath(String filePath) { 

    public int getSplitter() { 

        return splitter; 

    public void setSplitter(int splitter) { 

    @Override 

    public String toString() { 

        return this.url +

"#" + this.fileName +

"#" + this.filePath +

"#" + this.splitter; 

package com.hoo.download; 

import java.io.IOException; 

import java.io.RandomAccessFile; 

* &lt;b&gt;function:&lt;/b&gt; 写入文件、保存文件

* @createDate 2011-9-21 下午05:44:02

* @file SaveItemFile.java

* @package com.hoo.download

public class SaveItemFile { 

    //存储文件 

    private RandomAccessFile itemFile; 

    public SaveItemFile()

throws IOException { 

        this("",

0); 

     * @param name 文件路径、名称

     * @param pos 写入点位置 position

     * @throws IOException

    public SaveItemFile(String name,

long pos) throws IOException { 

        itemFile = new RandomAccessFile(name,

"rw"); 

        //在指定的pos位置开始写入数据 

        itemFile.seek(pos); 

     * &lt;b&gt;function:&lt;/b&gt; 同步方法写入文件

     * @createDate 2011-9-26 下午12:21:22

     * @param buff 缓冲数组

     * @param start 起始位置

     * @param length 长度

    public synchronized

int write(byte[] buff,

int start, int length) { 

        int i = -1; 

        try { 

            itemFile.write(buff, start, length); 

            i = length; 

        } catch (IOException e) { 

            e.printStackTrace(); 

        return i; 

    public void close()

        if (itemFile !=

null) { 

            itemFile.close(); 

这个类主要是完成向本地的指定文件指针出开始写入文件,并返回当前写入文件的长度(文件指针)。这个类将被线程调用,文件被分成对应的块后,将被线程调用。每个线程都将会调用这个类完成文件的随机写入。

import java.io.InputStream; 

import java.net.HttpURLConnection; 

import java.net.MalformedURLException; 

import java.net.URL; 

import java.net.URLConnection; 

import com.hoo.util.LogUtils; 

* &lt;b&gt;function:&lt;/b&gt; 单线程下载文件

* @createDate 2011-9-22 下午02:55:10

* @file DownloadFile.java

public class DownloadFile

extends Thread { 

    //下载文件起始位置   

    private long startPos; 

    //下载文件结束位置 

    private long endPos; 

    //线程id 

    private int threadId; 

    //下载是否完成 

    private boolean isDownloadOver =

false; 

    private SaveItemFile itemFile; 

    private static

final int BUFF_LENGTH =

1024 * 8; 

     * @param url 下载文件url

     * @param name 文件名称

     * @param startPos 下载文件起点

     * @param endPos 下载文件结束点

     * @param threadId 线程id

    public DownloadFile(String url, String name,

long startPos, long endPos,

int threadId) throws IOException { 

        this.startPos = startPos; 

        this.endPos = endPos; 

        this.threadId = threadId; 

        //分块下载写入文件内容 

        this.itemFile =

new SaveItemFile(name, startPos); 

    public void run() { 

        while (endPos &gt; startPos &amp;&amp; !isDownloadOver) { 

            try { 

                URL url = new URL(this.url); 

                HttpURLConnection conn = (HttpURLConnection) url.openConnection(); 

                // 设置连接超时时间为10000ms 

                conn.setConnectTimeout(10000); 

                // 设置读取数据超时时间为10000ms 

                conn.setReadTimeout(10000); 

                setHeader(conn); 

                String property = "bytes=" + startPos +

"-"; 

                conn.setRequestProperty("RANGE", property); 

                //输出log信息 

                LogUtils.log("开始 " + threadId +

":" + property + endPos); 

                //printHeader(conn); 

                //获取文件输入流,读取文件内容 

                InputStream is = conn.getInputStream(); 

                byte[] buff =

new byte[BUFF_LENGTH]; 

                int length = -1; 

                LogUtils.log("#start#Thread: " + threadId +

", startPos: " + startPos + ", endPos: " + endPos); 

                while ((length = is.read(buff)) &gt;

0 &amp;&amp; startPos &lt; endPos &amp;&amp; !isDownloadOver) { 

                    //写入文件内容,返回最后写入的长度 

                    startPos += itemFile.write(buff,

0, length); 

                } 

                LogUtils.log("#over#Thread: " + threadId +

                LogUtils.log("Thread " + threadId +

" is execute over!"); 

                this.isDownloadOver =

true; 

            } catch (MalformedURLException e) { 

                e.printStackTrace(); 

            } catch (IOException e) { 

            } finally { 

                try { 

                    if (itemFile !=

                        itemFile.close(); 

                    } 

                } catch (IOException e) { 

                    e.printStackTrace(); 

            } 

        if (endPos &lt; startPos &amp;&amp; !isDownloadOver) { 

            LogUtils.log("Thread " + threadId  +

" startPos &gt; endPos, not need download file !"); 

            this.isDownloadOver =

        if (endPos == startPos &amp;&amp; !isDownloadOver) { 

" startPos = endPos, not need download file !"); 

     * &lt;b&gt;function:&lt;/b&gt; 打印下载文件头部信息

     * @createDate 2011-9-22 下午05:44:35

     * @param conn HttpURLConnection

    public static

void printHeader(URLConnection conn) { 

        int i = 1; 

        while (true) { 

            String header = conn.getHeaderFieldKey(i); 

            i++; 

            if (header != null) { 

                LogUtils.info(header +

":" + conn.getHeaderField(i)); 

            } else { 

                break; 

     * &lt;b&gt;function:&lt;/b&gt; 设置URLConnection的头部信息,伪装请求信息

     * @createDate 2011-9-28 下午05:29:43

     * @param con

void setHeader(URLConnection conn) { 

        conn.setRequestProperty("User-Agent",

"Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.3) Gecko/2008092510 Ubuntu/8.04 (hardy) Firefox/3.0.3"); 

        conn.setRequestProperty("Accept-Language",

"en-us,en;q=0.7,zh-cn;q=0.3"); 

        conn.setRequestProperty("Accept-Encoding",

"utf-8"); 

        conn.setRequestProperty("Accept-Charset",

"ISO-8859-1,utf-8;q=0.7,*;q=0.7"); 

        conn.setRequestProperty("Keep-Alive",

"300"); 

        conn.setRequestProperty("connnection",

"keep-alive"); 

        conn.setRequestProperty("If-Modified-Since",

"Fri, 02 Jan 2009 17:00:05 GMT"); 

        conn.setRequestProperty("If-None-Match",

"\"1261d8-4290-df64d224\""); 

        conn.setRequestProperty("Cache-conntrol",

"max-age=0"); 

        conn.setRequestProperty("Referer",

"http://www.baidu.com"); 

    public boolean isDownloadOver() { 

        return isDownloadOver; 

    public long getStartPos() { 

        return startPos; 

    public long getEndPos() { 

        return endPos; 

这个类主要是完成单个线程的文件下载,将通过URLConnection读取指定url的资源信息。然后用InputStream读取文件内容,然后调用调用SaveItemFile类,向本地写入当前要读取的块的内容。

import java.io.DataInputStream; 

import java.io.DataOutputStream; 

import java.io.File; 

import java.io.FileInputStream; 

import java.io.FileOutputStream; 

import com.hoo.entity.DownloadInfo; 

* &lt;b&gt;function:&lt;/b&gt; 分批量下载文件

* @createDate 2011-9-22 下午05:51:54

* @file BatchDownloadFile.java

public class BatchDownloadFile

implements Runnable { 

    //下载文件信息  

    private DownloadInfo downloadInfo; 

    //一组开始下载位置 

    private long[] startPos; 

    //一组结束下载位置 

    private long[] endPos; 

    //休眠时间 

final int SLEEP_SECONDS =

500; 

    //子线程下载 

    private DownloadFile[] fileItem; 

    //文件长度 

    private int length; 

    //是否第一个文件 

    private boolean first =

    //是否停止下载 

    private boolean stop =

    //临时文件信息 

    private File tempFile; 

    public BatchDownloadFile(DownloadInfo downloadInfo) { 

        this.downloadInfo = downloadInfo; 

        String tempPath = this.downloadInfo.getFilePath() + File.separator + downloadInfo.getFileName() +

".position"; 

        tempFile = new File(tempPath); 

        //如果存在读入点位置的文件 

        if (tempFile.exists()) { 

            first = false; 

            //就直接读取内容 

                readPosInfo(); 

        } else { 

            //数组的长度就要分成多少段的数量 

            startPos = new

long[downloadInfo.getSplitter()]; 

            endPos = new long[downloadInfo.getSplitter()]; 

        //首次下载,获取下载文件长度 

        if (first) { 

            length = this.getFileSize();//获取文件长度 

            if (length == -1) { 

                LogUtils.log("file length is know!"); 

                stop = true; 

            } else if (length == -2) { 

                LogUtils.log("read file length is error!"); 

            } else

if (length &gt; 0) { 

                /**

                 * eg

                 * start: 1, 3, 5, 7, 9

                 * end: 3, 5, 7, 9, length

                 */ 

                for (int i =

0, len = startPos.length; i &lt; len; i++) { 

                    int size = i * (length / len); 

                    startPos[i] = size; 

                    //设置最后一个结束点的位置 

                    if (i == len -

1) { 

                        endPos[i] = length; 

                    } else { 

                        size = (i + 1) * (length / len); 

                        endPos[i] = size; 

                    LogUtils.log("start-end Position[" + i +

"]: " + startPos[i] + "-" + endPos[i]); 

                LogUtils.log("get file length is error, download is stop!"); 

        //子线程开始下载 

        if (!stop) { 

            //创建单线程下载对象数组 

            fileItem = new DownloadFile[startPos.length];//startPos.length = downloadInfo.getSplitter() 

            for (int i =

0; i &lt; startPos.length; i++) { 

                    //创建指定个数单线程下载对象,每个线程独立完成指定块内容的下载 

                    fileItem[i] = new DownloadFile( 

                        downloadInfo.getUrl(),  

                        this.downloadInfo.getFilePath() + File.separator + downloadInfo.getFileName(),  

                        startPos[i], endPos[i], i 

                    ); 

                    fileItem[i].start();//启动线程,开始下载 

                    LogUtils.log("Thread: " + i +

", startPos: " + startPos[i] + ", endPos: " + endPos[i]); 

            //循环写入下载文件长度信息 

            while (!stop) { 

                    writePosInfo(); 

                    LogUtils.log("downloading……"); 

                    Thread.sleep(SLEEP_SECONDS); 

                    stop = true; 

                } catch (InterruptedException e) { 

                    if (!fileItem[i].isDownloadOver()) { 

                        stop = false; 

                        break; 

            LogUtils.info("Download task is finished!"); 

     * 将写入点数据保存在临时文件中

     * @createDate 2011-9-23 下午05:25:37

    private void writePosInfo()

        DataOutputStream dos = new DataOutputStream(new FileOutputStream(tempFile)); 

        dos.writeInt(startPos.length); 

        for (int i =

            dos.writeLong(fileItem[i].getStartPos()); 

            dos.writeLong(fileItem[i].getEndPos()); 

            //LogUtils.info("[" + fileItem[i].getStartPos() + "#" + fileItem[i].getEndPos() + "]"); 

        dos.close(); 

     * &lt;b&gt;function:&lt;/b&gt;读取写入点的位置信息

     * @createDate 2011-9-23 下午05:30:29

    private void readPosInfo()

        DataInputStream dis = new DataInputStream(new FileInputStream(tempFile)); 

        int startPosLength = dis.readInt(); 

        startPos = new long[startPosLength]; 

        endPos = new

long[startPosLength]; 

0; i &lt; startPosLength; i++) { 

            startPos[i] = dis.readLong(); 

            endPos[i] = dis.readLong(); 

        dis.close(); 

     * &lt;b&gt;function:&lt;/b&gt; 获取下载文件的长度

     * @createDate 2011-9-26 下午12:15:08

    private int getFileSize() { 

        int fileLength = -1; 

            URL url = new URL(this.downloadInfo.getUrl()); 

            HttpURLConnection conn = (HttpURLConnection) url.openConnection(); 

            DownloadFile.setHeader(conn); 

            int stateCode = conn.getResponseCode(); 

            //判断http status是否为HTTP/1.1 206 Partial Content或者200 OK 

            if (stateCode != HttpURLConnection.HTTP_OK &amp;&amp; stateCode != HttpURLConnection.HTTP_PARTIAL) { 

                LogUtils.log("Error Code: " + stateCode); 

                return -2; 

            } else if (stateCode &gt;=

400) { 

                //获取长度 

                fileLength = conn.getContentLength(); 

                LogUtils.log("FileLength: " + fileLength); 

            //读取文件长度 

            /*for (int i = 1; ; i++) {

                String header = conn.getHeaderFieldKey(i);

                if (header != null) {

                    if ("Content-Length".equals(header)) {

                        fileLength = Integer.parseInt(conn.getHeaderField(i));

                        break;

                    }

                } else {

                    break;

                }

            }

            */ 

            DownloadFile.printHeader(conn); 

        } catch (MalformedURLException e) { 

        return fileLength; 

这个类主要是完成读取指定url资源的内容,获取该资源的长度。然后将该资源分成指定的块数,将每块的起始下载位置、结束下载位置,分别保存在一个数组中。每块都单独开辟一个独立线程开始下载。在开始下载之前,需要创建一个临时文件,写入当前下载线程的开始下载指针位置和结束下载指针位置。

日志工具类

package com.hoo.util; 

* &lt;b&gt;function:&lt;/b&gt; 日志工具类

* @createDate 2011-9-21 下午05:21:27

* @file LogUtils.java

* @package com.hoo.util

public abstract

class LogUtils { 

void log(Object message) { 

        System.err.println(message); 

void log(String message) { 

void log(int message) { 

void info(Object message) { 

        System.out.println(message); 

void info(String message) { 

void info(int message) { 

下载工具类

import com.hoo.download.BatchDownloadFile; 

* &lt;b&gt;function:&lt;/b&gt; 分块多线程下载工具类

* @createDate 2011-9-28 下午05:22:18

* @file DownloadUtils.java

class DownloadUtils { 

void download(String url) { 

        DownloadInfo bean = new DownloadInfo(url); 

        LogUtils.info(bean); 

        BatchDownloadFile down = new BatchDownloadFile(bean); 

        new Thread(down).start(); 

void download(String url, int threadNum) { 

        DownloadInfo bean = new DownloadInfo(url, threadNum); 

void download(String url, String fileName, String filePath,

int threadNum) { 

        DownloadInfo bean = new DownloadInfo(url, fileName, filePath, threadNum); 

下载测试类

package com.hoo.test; 

import com.hoo.util.DownloadUtils; 

* &lt;b&gt;function:&lt;/b&gt; 下载测试

* @createDate 2011-9-23 下午05:49:46

* @file TestDownloadMain.java

public class TestDownloadMain { 

void main(String[] args) { 

        /*DownloadInfo bean = new DownloadInfo("http://i7.meishichina.com/Health/UploadFiles/201109/2011092116224363.jpg");

        System.out.println(bean);

        BatchDownloadFile down = new BatchDownloadFile(bean);

        new Thread(down).start();*/ 

        //DownloadUtils.download("http://i7.meishichina.com/Health/UploadFiles/201109/2011092116224363.jpg"); 

        DownloadUtils.download("http://mp3.baidu.com/j?j=2&amp;url=http%3A%2F%2Fzhangmenshiting2.baidu.com%2Fdata%2Fmusic%2F1669425%2F%25E9%2599%25B7%25E5%2585%25A5%25E7%2588%25B1%25E9%2587%258C%25E9%259D%25A2.mp3%3Fxcode%3D2ff36fb70737c816553396c56deab3f1",

"aa.mp3", "c:/temp",

5); 

多线程下载主要在第三部和第四部,其他的地方还是很好理解。源码中提供相应的注释了,便于理解。