Java Http斷點續傳(下載)


先說使用Java實現斷點續傳的關鍵點:

  • 本地能夠判斷當前請求下載的文件是否已經存在,或者部分存在;
  • 如果存在且爲部分,則本地可以記錄下載位置,並從服務器端繼續完成下載;
技術上的關鍵:
  • 請求的需要:
    URL url = new URL("urlStr"); 
    HttpURLConnection httpConnection = (HttpURLConnection)url.openConnection
    //向服務器請求指定區間段的數據,這是實現斷點續傳的根本。
    httpcon.setRequestProperty("Range", "bytes=" + startPos+ "-" + endPos);

  • 保存文件的方法:採用IO包中的RandomAccessFile類:
    RandomAccess downfile = new RandomAccessFile("file","rw"); 
    Array[] startPos ;
    downloadfile.seek(startPos);
    byte[] buf = new byte[1024]; 
    downloadfile.write(buf, 0, length);

下面是功能實現的代碼以及測試類,在測試類中傳入實現類中的兩個參數是線程數與文件來源(鏈接),斷點續傳的體現可以在下載過程中關閉IDE,在工程文件目錄下,可以看到未下載完全的部分文件以及臨時文件。重新啓動IDE運行程序,文件繼續下載,以下程序基本行行註釋,在後面我還會說明一些:

一、功能實現類MultiThreadDownLoad
package **********;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/*
 * Encode:UTF-8
 * 
 * Author:GFC
 * 
 * 根據輸入的url和設定的線程數,來完成斷點續傳功能。
 * 
 * 每個線程支負責某一小段的數據下載;再通過RandomAccessFile完成數據的整合。
 */




public class MultiTheradDownLoad {
	private String urlStr = null;
    private String filename = null;
    private String tmpfilename = null;

    private int threadNum = 0;

    private CountDownLatch latch = null;//設置一個計數器,代碼內主要用來完成對緩存文件的刪除

    private long fileLength = 0l;
    private long threadLength = 0l;
    private long[] startPos;//保留每個線程下載數據的起始位置。
    private long[] endPos;//保留每個線程下載數據的截止位置。

    private boolean bool = false;

    private URL url = null;

    //有參構造函數,先構造需要的數據
    public MultiTheradDownLoad(String urlStr, int threadNum) {
        this.urlStr = urlStr;
        this.threadNum = threadNum;
        startPos = new long[this.threadNum];
        endPos = new long[this.threadNum];
        latch = new CountDownLatch(this.threadNum);
    }

    /*
     * 組織斷點續傳功能的方法
     */
    public void downloadPart() {

        File file = null;
        File tmpfile = null;
        
    	//設置HTTP網絡訪問代理
    	System.setProperty("http.proxySet", "true");  
   		System.setProperty("http.proxyHost", "proxy3.bj.petrochina");  
 		System.setProperty("http.proxyPort", "8080"); 


        //從文件鏈接中獲取文件名,此處沒考慮文件名爲空的情況,此種情況可能需使用UUID來生成一個唯一數來代表文件名。
        filename = urlStr.substring(urlStr.lastIndexOf('/') + 1, urlStr
                .contains("?") ? urlStr.lastIndexOf('?') : urlStr.length());
        tmpfilename = filename + "_tmp";

        try {
        	//創建url
            url = new URL(urlStr);
            
            //打開下載鏈接,並且得到一個HttpURLConnection的一個對象httpcon
            HttpURLConnection httpcon = (HttpURLConnection) url.openConnection();
            httpcon.setRequestMethod("GET");
            
            //獲取請求資源的總長度。
            fileLength = httpcon.getContentLengthLong();

            //下載文件和臨時文件
            file = new File(filename);//相對目錄
            tmpfile = new File(tmpfilename);

            //每個線程需下載的資源大小;由於文件大小不確定,爲避免數據丟失
            threadLength = fileLength%threadNum == 0 ? fileLength/threadNum : fileLength/threadNum+1;
            //打印下載信息
            System.out.println("fileName: " + filename + " ," + "fileLength= "
                    + fileLength + " the threadLength= " + threadLength);
            
            //各個線程在exec線程池中進行,起始位置--結束位置
            if (file.exists() && file.length() == fileLength) {
                System.out.println("文件已存在!!");
                return;
            } else {
                setBreakPoint(startPos, endPos, tmpfile);
                ExecutorService exec = Executors.newCachedThreadPool();
                for (int i = 0; i < threadNum; i++) {
                    exec.execute(new DownLoadThread(startPos[i], endPos[i],
                            this, i, tmpfile, latch));
                }
                latch.await();//當你的計數器減爲0之前,會在此處一直阻塞。
                exec.shutdown();
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        //下載完成後,判斷文件是否完整,並刪除臨時文件
        if (file.length() == fileLength) {
            if (tmpfile.exists()) {
                System.out.println("刪除臨時文件!!");
                tmpfile.delete();
            }
        }
    }

    /*
     * 斷點設置方法,當有臨時文件時,直接在臨時文件中讀取上次下載中斷時的斷點位置。沒有臨時文件,即第一次下載時,重新設置斷點。
     * 
     * Rantmpfile.seek()跳轉到一個位置的目的是爲了讓各個斷點存儲的位置儘量分開。
     * 
     * 這是實現斷點續傳的重要基礎。
     */
    private void setBreakPoint(long[] startPos, long[] endPos, File tmpfile) {
        RandomAccessFile rantmpfile = null;
        try {
            if (tmpfile.exists()) {
                System.out.println("繼續下載!!");
                rantmpfile = new RandomAccessFile(tmpfile, "rw");
                for (int i = 0; i < threadNum; i++) {
                    rantmpfile.seek(8 * i + 8);
                    startPos[i] = rantmpfile.readLong();

                    rantmpfile.seek(8 * (i + 1000) + 16);
                    endPos[i] = rantmpfile.readLong();

                    System.out.println("the Array content in the exit file: ");
                    System.out.println("thre thread" + (i + 1) + " startPos:"
                            + startPos[i] + ", endPos: " + endPos[i]);
                }
            } else {
                System.out.println("the tmpfile is not available!!");
                rantmpfile = new RandomAccessFile(tmpfile, "rw");
                
                //最後一個線程的截止位置大小爲請求資源的大小
                for (int i = 0; i < threadNum; i++) {
                    startPos[i] = threadLength * i;
                    if (i == threadNum - 1) {
                        endPos[i] = fileLength;
                    } else {
                        endPos[i] = threadLength * (i + 1) - 1;
                    }

                    rantmpfile.seek(8 * i + 8);
                    rantmpfile.writeLong(startPos[i]);

                    rantmpfile.seek(8 * (i + 1000) + 16);
                    rantmpfile.writeLong(endPos[i]);

                    System.out.println("the Array content: ");
                    System.out.println("thre thread" + (i + 1) + " startPos:"
                            + startPos[i] + ", endPos: " + endPos[i]);
                }
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (rantmpfile != null) {
                    rantmpfile.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    /*
     * 實現下載功能的內部類,通過讀取斷點來設置向服務器請求的數據區間。
     */
    class DownLoadThread implements Runnable {

        private long startPos;
        private long endPos;
        private MultiTheradDownLoad task = null;
        private RandomAccessFile downloadfile = null;
        private int id;
        private File tmpfile = null;
        private RandomAccessFile rantmpfile = null;
        private CountDownLatch latch = null;

        public DownLoadThread(long startPos, long endPos,
                MultiTheradDownLoad task, int id, File tmpfile,
                CountDownLatch latch) {
            this.startPos = startPos;
            this.endPos = endPos;
            this.task = task;
            this.tmpfile = tmpfile;
            try {
            	//
                this.downloadfile = new RandomAccessFile(this.task.filename,"rw");
                this.rantmpfile = new RandomAccessFile(this.tmpfile, "rw");
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            this.id = id;
            this.latch = latch;
        }

        @Override
        public void run() {

            HttpURLConnection httpcon = null;
            InputStream is = null;
            int length = 0;

            System.out.println("線程" + id + " 開始下載!!");

            while (true) {
                try {
                    httpcon = (HttpURLConnection) task.url.openConnection();
                    httpcon.setRequestMethod("GET");
                    
                    //防止網絡阻塞,設置指定的超時時間;單位都是ms。超過指定時間,就會拋出異常
                    httpcon.setReadTimeout(20000);//讀取數據的超時設置
                    httpcon.setConnectTimeout(20000);//連接的超時設置

                    if (startPos < endPos) {
                        
                        //向服務器請求指定區間段的數據,這是實現斷點續傳的根本。
                        httpcon.setRequestProperty("Range", "bytes=" + startPos+ "-" + endPos);

                        System.out.println("線程 " + id+ " 長度:---- "+ (endPos - startPos));

                        downloadfile.seek(startPos);

                        if (httpcon.getResponseCode() != HttpURLConnection.HTTP_OK
                                && httpcon.getResponseCode() != HttpURLConnection.HTTP_PARTIAL) {
                            this.task.bool = true;
                            httpcon.disconnect();
                            downloadfile.close();
                            System.out.println("線程 ---" + id + " 下載完成!!");
                            latch.countDown();//計數器自減
                            break;
                        }

                        is = httpcon.getInputStream();//獲取服務器返回的資源流
                        long count = 0l;
                        byte[] buf = new byte[1024];

                        while (!this.task.bool && (length = is.read(buf)) != -1) {
                            count += length;
                            downloadfile.write(buf, 0, length);
                            
                            //不斷更新每個線程下載資源的起始位置,並寫入臨時文件;爲斷點續傳做準備
                            startPos += length;
                            rantmpfile.seek(8 * id + 8);
                            rantmpfile.writeLong(startPos);
                        }
                        System.out.println("線程 " + id
                                + " 總下載大小: " + count);
                        
                        //關閉流
                        is.close();
                        httpcon.disconnect();
                        downloadfile.close();
                        rantmpfile.close();
                    }
                    latch.countDown();//計數器自減
                    System.out.println("線程 " + id + " 下載完成!!");
                    break;
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        if (is != null) {
                            is.close();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }

}

其中設置HTTP訪問代理在公網上不需要。可以看出來,整個類中分爲三大部分:下載功能方法downloadPart();設置斷點的私有方法setBreakPoint(long[] startPos, long[] endPos, File tmpfile);以及實現下載功能的內部類,通過讀取斷點來設置向服務器請求的數據區間class DownLoadThread,其中重寫了線程的run()方法。


二、測試類 
package Download;

public class DownLoadTest {
	public static void main(String[] args) {
		//http://down.sandai.net/thunder9/Thunder9.1.40.898.exe
		//http://download.firefox.com.cn/releases-sha2/stub/official/zh-CN/Firefox-latest.exe
		//http://p0.ifengimg.com/pmop/2017/0830/4AB0880C38002DC837856D274469E3D09BDF43B9_size28_w500_h314.jpeg
		//http://img.article.pchome.net/00/22/70/23/pic_lib/wm/03.jpg
		
		int threadNum = 4;
        String filepath = "http://down.sandai.net/thunder9/Thunder9.1.40.898.exe";
        
        MultiTheradDownLoad load = new MultiTheradDownLoad(filepath ,threadNum);    
        load.downloadPart();    
    }

}




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章