天天看點

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); 

多線程下載下傳主要在第三部和第四部,其他的地方還是很好了解。源碼中提供相應的注釋了,便于了解。