http協議 文件下載原理及多線程斷點續傳

最近研究了一下關於文件下載的相關內容,覺得還是寫些東西記下來比較好。起初只是想研究研究,但後來發現寫個可重用性比較高的模塊還是很有必要的,我想這也是大多數開發人員的習慣吧。
對於HTTP協議,向服務器請求某個文件時,只要發送類似如下的請求即可: 

GET /Path/FileName HTTP/1.0 
Host: www.server.com:80 
Accept: */* 
User-Agent: GeneralDownloadApplication 
Connection: close 

每行用一個“回車換行”分隔,末尾再追加一個“回車換行”作爲整個請求的結束。 

第一行中的GET是HTTP協議支持的方法之一,方法名是大小寫敏感的,HTTP協議還支持OPTIONS、HAED、POST、PUT、DELETE、TRACE、CONNECT等方法,而GET和HEAD這兩個方法通常被認爲是“安全的”,也就是說任何實現了HTTP協議的服務器程序都會實現這兩個方法。對於文件下載功能,GET足矣。GET後面是一個空格,其後緊跟的是要下載的文件從WEB服務器根開始的絕對路徑。該路徑後又有一個空格,然後是協議名稱及協議版本。

除第一行以外,其餘行都是HTTP頭的字段部分。Host字段表示主機名和端口號,如果端口號是默認的80則可以不寫。Accept字段中的*/*表示接收任何類型的數據。User-Agent表示用戶代理,這個字段可有可無,但強烈建議加上,因爲它是服務器統計、追蹤以及識別客戶端的依據。Connection字段中的close表示使用非持久連接。

關於HTTP協議更多的細節可以參考RFC2616(HTTP 1.1)。因爲我只是想通過HTTP協議實現文件下載,所以也只看了一部分,並沒有看全。 

如果服務器成功收到該請求,並且沒有出現任何錯誤,則會返回類似下面的數據: 

HTTP/1.0 200 OK 
Content-Length: 13057672 
Content-Type: application/octet-stream 
Last-Modified: Wed, 10 Oct 2005 00:56:34 GMT 
Accept-Ranges: bytes 
ETag: "2f38a6cac7cec51:160c" 
Server: Microsoft-IIS/6.0 
X-Powered-By: ASP.NET 
Date: Wed, 16 Nov 2005 01:57:54 GMT 
Connection: close 

不用逐一解釋,很多東西一看幾乎就明白了,只說我們大家都關心內容吧。 

第一行是協議名稱及版本號,空格後面會有一個三位數的數字,是HTTP協議的響應狀態碼,200表示成功,OK是對狀態碼的簡短文字描述。狀態碼共有5類: 
1xx屬於通知類; 
2xx屬於成功類; 
3xx屬於重定向類; 
4xx屬於客戶端錯誤類; 
5xx屬於服務端錯誤類。 
對於狀態碼,相信大家對404應該很熟悉,如果向一個服務器請求一個不存在的文件,就會得到該錯誤,通常瀏覽器也會顯示類似“HTTP 404 - 未找到文件”這樣的錯誤。Content-Length字段是一個比較重要的字段,它標明瞭服務器返回數據的長度,這個長度是不包含HTTP頭長度的。換句話說,我們的請求中並沒有Range字段(後面會說到),表示我們請求的是整個文件,所以Content-Length就是整個文件的大小。其餘各字段是一些關於文件和服務器的屬性信息。

這段返回數據同樣是以最後一行的結束標誌(回車換行)和一個額外的回車換行作爲結束,即“\r\n\r\n”。而“\r\n\r\n”後面緊接的就是文件的內容了,這樣我們就可以找到“\r\n\r\n”,並從它後面的第一個字節開始,源源不斷的讀取,再寫到文件中了。

以上就是通過HTTP協議實現文件下載的全過程。但還不能實現斷點續傳,而實際上斷點續傳的實現非常簡單,只要在請求中加一個Range字段就可以了。 

假如一個文件有1000個字節,那麼其範圍就是0-999,則: 

Range: bytes=500-      表示讀取該文件的500-999字節,共500字節。 
Range: bytes=500-599   表示讀取該文件的500-599字節,共100字節。 
Range還有其它幾種寫法,但上面這兩種是最常用的,對於斷點續傳也足矣了。如果HTTP請求中包含Range字段,那麼服務器會返回206(Partial Content),同時HTTP頭中也會有一個相應的Content-Range字段,類似下面的格式:
Content-Range: bytes 500-999/1000 
Content-Range字段說明服務器返回了文件的某個範圍及文件的總長度。這時Content-Length字段就不是整個文件的大小了,而是對應文件這個範圍的字節數,這一點一定要注意。

一切好像基本上沒有什麼問題了,本來我也是這麼認爲的,但事實並非如此。如果我們請求的文件的URL是類似http://www.server.com/filename.exe這樣的文件,則不會有問題。但是很多軟件下載網站的文件下載鏈接都是通過程序重定向的,比如pchome的ACDSee的HTTP下載地址是:

http://download.pchome.net/php/tdownload2.php?sid=5547&url=/multimedia/viewer/acdc31sr1b051007.exe&svr=1&typ=0

這種地址並沒有直接標識文件的位置,而是通過程序進行了重定向。如果向服務器請求這樣的URL,服務器就會返回302(Moved Temporarily),意思就是需要重定向,同時在HTTP頭中會包含一個Location字段,Location字段的值就是重定向後的目的URL。這時就需要斷開當前的連接,而向這個重定向後的服務器發請求。

     好了,原理基本上就是這些了。其實裝個Sniffer好好分析一下,很容易就可以分析出來的。不過NetAnts也幫了我一些忙,它的文件下載日誌對開發人員還是很有幫助的。

 

 

 

annegu做了一個簡單的Http多線程的下載程序,來討論一下多線程併發下載以及斷點續傳的問題。 
這個程序的功能,就是可以分多個線程從目標地址上下載數據,每個線程負責下載一部分,並可以支持斷點續傳和超時重連。 

下載的方法是download(),它接收兩個參數,分別是要下載的頁面的url和編碼方式。在這個負責下載的方法中,主要分了三個步驟。第一步是用來設置斷點續傳時候的一些信息的,第二步就是主要的分多線程來下載了,最後是數據的合併。 

1、多線程下載:

[java] view plaincopy
  1.     public String download(String urlStr, String charset) {  
  2.         this.charset = charset;  
  3.         long contentLength = 0;  
  4. ①       CountDownLatch latch = new CountDownLatch(threadNum);  
  5.         long[] startPos = new long[threadNum];  
  6.         long endPos = 0;  
  7.   
  8.         try {  
  9.             // 從url中獲得下載的文件格式與名字  
  10.             this.fileName = urlStr.substring(urlStr.lastIndexOf("/") + 1, urlStr.lastIndexOf("?")>0 ? urlStr.lastIndexOf("?") : urlStr.length());  
  11.             if("".equalsIgnoreCase(this.fileName)){  
  12.                 this.fileName = UUID.randomUUID().toString();  
  13.             }  
  14.   
  15.             this.url = new URL(urlStr);  
  16.             URLConnection con = url.openConnection();  
  17.             setHeader(con);  
  18.             // 得到content的長度  
  19.             contentLength = con.getContentLength();  
  20.             // 把context分爲threadNum段的話,每段的長度。  
  21.             this.threadLength = contentLength / threadNum;  
  22.               
  23.             // 第一步,分析已下載的臨時文件,設置斷點,如果是新的下載任務,則建立目標文件。在第4點中說明。  
  24.             startPos = setThreadBreakpoint(fileDir, fileName, contentLength, startPos);  
  25.   
  26.             //第二步,分多個線程下載文件  
  27.             ExecutorService exec = Executors.newCachedThreadPool();  
  28.             for (int i = 0; i < threadNum; i++) {  
  29.                 // 創建子線程來負責下載數據,每段數據的起始位置爲(threadLength * i + 已下載長度)  
  30.                 startPos[i] += threadLength * i;  
  31.   
  32.                 /*設置子線程的終止位置,非最後一個線程即爲(threadLength * (i + 1) - 1) 
  33.                 最後一個線程的終止位置即爲下載內容的長度*/  
  34.                 if (i == threadNum - 1) {  
  35.                     endPos = contentLength;  
  36.                 } else {  
  37.                     endPos = threadLength * (i + 1) - 1;  
  38.                 }  
  39.                 // 開啓子線程,並執行。  
  40.                 ChildThread thread = new ChildThread(this, latch, i, startPos[i], endPos);  
  41.                 childThreads[i] = thread;  
  42.                 exec.execute(thread);  
  43.             }  
  44.   
  45.             try {  
  46.                 // 等待CountdownLatch信號爲0,表示所有子線程都結束。  
  47. ②               latch.await();  
  48.                 exec.shutdown();  
  49.   
  50.                 // 第三步,把分段下載下來的臨時文件中的內容寫入目標文件中。在第3點中說明。  
  51.                 tempFileToTargetFile(childThreads);  
  52.   
  53.             } catch (InterruptedException e) {  
  54.                 e.printStackTrace();  
  55.             }  
  56.     }  

 

首先來看最主要的步驟:多線程下載。 
首先從url中提取目標文件的名稱,並在對應的目錄創建文件。然後取得要下載的文件大小,根據分成的下載線程數量平均分配每個線程需要下載的數據量,就是threadLength。然後就可以分多個線程來進行下載任務了。 

在這個例子中,並沒有直接顯示的創建Thread對象,而是用Executor來管理Thread對象,並且用CachedThreadPool來創建的線程池,當然也可以用FixedThreadPool。CachedThreadPool在程序執行的過程中會創建與所需數量相同的線程,當程序回收舊線程的時候就停止創建新線程。FixedThreadPool可以預先新建參數給定個數的線程,這樣就不用在創建任務的時候再來創建線程了,可以直接從線程池中取出已準備好的線程。下載線程的數量是通過一個全局變量threadNum來控制的,默認爲5。 

好了,這5個子線程已經通過Executor來創建了,下面它們就會各自爲政,互不干涉的執行了。線程有兩種實現方式:實現Runnable接口;繼承Thread類。 

ChildThread就是子線程,它作爲DownloadTask的內部類,繼承了Thread,它的構造方法需要5個參數,依次是一個對DownloadTask的引用,一個CountDownLatch,id(標識線程的id號),startPosition(下載內容的開始位置),endPosition(下載內容的結束位置)。 
這個CountDownLatch是做什麼用的呢? 

現在我們整理一下思路,要實現分多個線程來下載數據的話,我們肯定還要把這多個線程下載下來的數據進行合。主線程必須等待所有的子線程都執行結束之後,才能把所有子線程的下載數據按照各自的id順序進行合併。CountDownLatch就是來做這個工作的。 
CountDownLatch用來同步主線程,強制主線程等待所有的子線程執行的下載操作完成。在主線程中,CountDownLatch對象被設置了一個初始計數器,就是子線程的個數5個,代碼①處。在新建了5個子線程並開始執行之後,主線程用CountDownLatch的await()方法來阻塞主線程,直到這個計數器的值到達0,纔會進行下面的操作,代碼②處。 
對每個子線程來說,在執行完下載指定區間與長度的數據之後,必須通過調用CountDownLatch的countDown()方法來把這個計數器減1。 

2、在全面開啓下載任務之後,主線程就開始阻塞,等待子線程執行完畢,所以下面我們來看一下具體的下載線程ChildThread。

[java] view plaincopy
  1. public class ChildThread extends Thread {  
  2.     public static final int STATUS_HASNOT_FINISHED = 0;  
  3.     public static final int STATUS_HAS_FINISHED = 1;  
  4.     public static final int STATUS_HTTPSTATUS_ERROR = 2;  
  5.     private DownloadTask task;  
  6.     private int id;  
  7.     private long startPosition;  
  8.     private long endPosition;  
  9.     private final CountDownLatch latch;  
  10.     // private RandomAccessFile tempFile = null;  
  11.     private File tempFile = null;  
  12.     //線程狀態碼  
  13.     private int status = ChildThread.STATUS_HASNOT_FINISHED;  
  14.   
  15.     public ChildThread(DownloadTask task, CountDownLatch latch, int id, long startPos, long endPos) {  
  16.         super();  
  17.         this.task = task;  
  18.         this.id = id;  
  19.         this.startPosition = startPos;  
  20.         this.endPosition = endPos;  
  21.         this.latch = latch;  
  22.   
  23.         try {  
  24.             tempFile = new File(this.task.fileDir + this.task.fileName + "_" + id);  
  25.             if(!tempFile.exists()){  
  26.                 tempFile.createNewFile();  
  27.             }  
  28.         } catch (IOException e) {  
  29.             e.printStackTrace();  
  30.         }  
  31.   
  32.     }  
  33.   
  34.     public void run() {  
  35.         System.out.println("Thread " + id + " run ...");  
  36.         HttpURLConnection con = null;  
  37.         InputStream inputStream = null;  
  38.         BufferedOutputStream outputStream = null;  
  39.         long count = 0;   
  40.         long threadDownloadLength = endPosition - startPosition;  
  41.   
  42.         try {  
  43.             outputStream = new BufferedOutputStream(new FileOutputStream(tempFile.getPath(), true));  
  44.         } catch (FileNotFoundException e2) {  
  45.             e2.printStackTrace();  
  46.         }  
  47.           
  48. ③       for(int k = 0; k < 10; k++){  
  49.             if(k > 0)  
  50.             System.out.println("Now thread " + id + "is reconnect, start position is " + startPosition);  
  51.             try {  
  52.                 //打開URLConnection  
  53.                 con = (HttpURLConnection) task.url.openConnection();  
  54.                 setHeader(con);  
  55.                 con.setAllowUserInteraction(true);  
  56.                 //設置連接超時時間爲10000ms  
  57. ④               con.setConnectTimeout(10000);  
  58.                 //設置讀取數據超時時間爲10000ms  
  59.                 con.setReadTimeout(10000);  
  60.                   
  61.                 if(startPosition < endPosition){  
  62.                     //設置下載數據的起止區間  
  63.                     con.setRequestProperty("Range""bytes=" + startPosition + "-"  
  64.                             + endPosition);  
  65.                     System.out.println("Thread " + id + " startPosition is " + startPosition);  
  66.                     System.out.println("Thread " + id + " endPosition is " + endPosition);  
  67.   
  68.                     //判斷http status是否爲HTTP/1.1 206 Partial Content或者200 OK  
  69.                     //如果不是以上兩種狀態,把status改爲STATUS_HTTPSTATUS_ERROR  
  70. ⑤                   if (con.getResponseCode() != HttpURLConnection.HTTP_OK  
  71.                             && con.getResponseCode() != HttpURLConnection.HTTP_PARTIAL) {  
  72.                         System.out.println("Thread " + id + ": code = "  
  73.                                 + con.getResponseCode() + ", status = "  
  74.                                 + con.getResponseMessage());  
  75.                         status = ChildThread.STATUS_HTTPSTATUS_ERROR;  
  76.                         this.task.statusError = true;  
  77.                         outputStream.close();  
  78.                         con.disconnect();  
  79.                         System.out.println("Thread " + id + " finished.");  
  80.                         latch.countDown();  
  81.                         break;  
  82.                     }  
  83.   
  84.                     inputStream = con.getInputStream();  
  85.   
  86.                     int len = 0;  
  87.                     byte[] b = new byte[1024];  
  88.                     while ((len = inputStream.read(b)) != -1) {  
  89.                         outputStream.write(b, 0, len);  
  90.                         count += len;   
  91. ⑥                       startPosition += len;                             
  92.                         //每讀滿4096個byte(一個內存頁),往磁盤上flush一下  
  93.                         if(count % 4096 == 0){  
  94. ⑦                           outputStream.flush();  
  95.                         }  
  96.                     }  
  97.   
  98.                     System.out.println("count is " + count);   
  99.                     if (count >= threadDownloadLength) {  
  100.                         status = ChildThread.STATUS_HAS_FINISHED;  
  101.                     }  
  102. ⑧                   outputStream.flush();  
  103.                     outputStream.close();  
  104.                     inputStream.close();  
  105.                     con.disconnect();  
  106.                 } else {  
  107.                     status = ChildThread.STATUS_HAS_FINISHED;  
  108.                 }  
  109.   
  110.                 System.out.println("Thread " + id + " finished.");  
  111.                 latch.countDown();  
  112.                 break;  
  113.             } catch (IOException e) {  
  114.                 try {  
  115. ⑨                   outputStream.flush();  
  116. ⑩                   TimeUnit.SECONDS.sleep(getSleepSeconds());  
  117.                 } catch (InterruptedException e1) {  
  118.                     e1.printStackTrace();  
  119.                 } catch (IOException e2) {  
  120.                     e2.printStackTrace();  
  121.                 }  
  122.                 continue;  
  123.             }                 
  124.         }  
  125.     }  
  126. }  


在ChildThread的構造方法中,除了設置一些從主線程中帶來的id, 起始位置之外,就是新建了一個臨時文件用來存放當前線程的下載數據。臨時文件的命名規則是這樣的:下載的目標文件名+”_”+線程編號。 

現在讓我們來看看從網絡中讀數據是怎麼讀的。我們通過URLConnection來獲得一個http的連接。有些網站爲了安全起見,會對請求的http連接進行過濾,因此爲了僞裝這個http的連接請求,我們給httpHeader穿一件僞裝服。下面的setHeader方法展示了一些非常常用的典型的httpHeader的僞裝方法。比較重要的有:Uer-Agent模擬從Ubuntu的firefox瀏覽器發出的請求;Referer模擬瀏覽器請求的前一個觸發頁面,例如從skycn站點來下載軟件的話,Referer設置成skycn的首頁域名就可以了;Range就是這個連接獲取的流文件的起始區間。

[java] view plaincopy
  1. private void setHeader(URLConnection con) {  
  2.     con.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");  
  3.     con.setRequestProperty("Accept-Language""en-us,en;q=0.7,zh-cn;q=0.3");  
  4.     con.setRequestProperty("Accept-Encoding""aa");  
  5.     con.setRequestProperty("Accept-Charset""ISO-8859-1,utf-8;q=0.7,*;q=0.7");  
  6.     con.setRequestProperty("Keep-Alive""300");  
  7.     con.setRequestProperty("Connection""keep-alive");  
  8.     con.setRequestProperty("If-Modified-Since""Fri, 02 Jan 2009 17:00:05 GMT");  
  9.     con.setRequestProperty("If-None-Match""\"1261d8-4290-df64d224\"");  
  10.     con.setRequestProperty("Cache-Control""max-age=0");  
  11.     con.setRequestProperty("Referer""http://www.dianping.com");  
  12. }  


另外,爲了避免線程因爲網絡原因而阻塞,設置了ConnectTimeout和ReadTimeout,代碼④處。setConnectTimeout設置的連接的超時時間,而setReadTimeout設置的是讀取數據的超時時間,發生超時的話,就會拋出socketTimeout異常,兩個方法的參數都是超時的毫秒數。 

這裏對超時的發生,採用的是等候一段時間重新連接的方法。整個獲取網絡連接並讀取下載數據的過程都包含在一個循環之中(代碼③處),如果發生了連接或者讀取數據的超時,在拋出的異常裏面就會sleep一定的時間(代碼⑩處),然後continue,再次嘗試獲取連接並讀取數據,這個時間可以通過setSleepSeconds()方法來設置。我們在迅雷等下載工具的使用中,經常可以看到狀態欄會輸出類似“連接超時,等待*秒後重試”的話,這個就是通過ConnectTimeout,ReadTimeout來實現的。 

連接建立好之後,我們要檢查一下返回響應的狀態碼。常見的Http Response Code有以下幾種: 
a) 200 OK 一切正常,對GET和POST請求的應答文檔跟在後面。 
b) 206 Partial Content 客戶發送了一個帶有Range頭的GET請求,服務器完成。 
c) 404 Not Found 無法找到指定位置的資源。這也是一個常用的應答。 
d) 414 Request URI Too Long URI太長。 
e) 416 Requested Range Not Satisfiable 服務器不能滿足客戶在請求中指定的Range頭。 
f) 500 Internal Server Error 服務器遇到了意料不到的情況,不能完成客戶的請求。 
g) 503 Service Unavailable 服務器由於維護或者負載過重未能應答。例如,Servlet可能在數據庫連接池已滿的情況下返回503。 
在這些狀態裏面,只有200與206纔是我們需要的正確的狀態。所以在代碼⑤處,進行了狀態碼的判斷,如果返回不符合要求的狀態碼,則結束線程,返回主線程並提示報錯。 

假設一切正常,下面我們就要考慮從網絡中讀數據了。正如我之前在分析mysql的數據庫驅動中看的一樣,網絡中發送數據都是以數據包的形式來發送的,也就是說不管是客戶端向服務器發出的請求數據,還是從服務器返回給客戶端的響應數據,都會被拆分成若干個小型數據包在網絡中傳遞,等數據包到達了目的地,網絡接口會依據數據包的編號來組裝它們,成爲完整的比特數據。因此,我們可以想到在這裏也是一樣的,我們用inputStream的read方法來通過網卡從網絡中讀取數據,並不一定一次就能把所有的數據包都讀完,所以我們要不斷的循環來從inputStream中讀取數據。Read方法有一個int型的返回值,表示每次從inputStream中讀取的字節數,如果把這個inputStream中的數據讀完了,那麼就返回-1。 
Read方法最多可以有三個參數,byte b[]是讀取數據之後存放的目標數組,off標識了目標數組中存儲的開始位置,len是想要讀取的數據長度,這個長度必定不能大於b[]的長度。 
public synchronized int read(byte b[], int off, int len); 

我們的目標是要把目標地址的內容下載下來,現在分了5個線程來分段下載,那麼這些分段下載的數據保存在哪裏呢?如果把它們都保存在內存中是非常糟糕的做法,如果文件相當之大,例如是一個視頻的話,難道把這麼大的數據都放在內存中嗎,這樣的話,萬一連接中斷,那前面下載的東西就都沒有了?我們當然要想辦法及時的把下載的數據刷到磁盤上保存下來。當用bt下載視頻的時候,通常都會有個臨時文件,當視頻完全下載結束之後,這個臨時文件就會被刪除,那麼下次繼續下載的時候,就會接着上次下載的點繼續下載。所以我們的outputStream就是往這個臨時文件來輸出了。 
OutputStream的write方法和上面InputStream的read方法有類似的參數,byte b[]是輸出數據的來源,off標識了開始位置,len是數據長度。 
public synchronized void write(byte b[], int off, int len) throws IOException; 
在往臨時文件的outputStream中寫數據的時候,我會加上一個計數器,每滿4096個比特就往文件中flush一下(代碼⑦處)。 

對於輸出流的flush,有些要注意的地方,在程序中有三個地方調用了outputStream.flush()。第一個是在循環的讀取網絡數據並往outputStream中寫入的時候,每滿4096個byte就flush一下(代碼⑦處);第二個是循環之後(代碼⑧處),這時候正常的讀取寫入操作已經完成,但是outputStream中還有沒有刷入磁盤的數據,所以要flush一下才能關閉連接;第三個就是在異常中的flush(代碼⑨處),因爲如果發生了連接超時或者讀取數據超時的話,就會直接跑到catch的exception中去,這個時候outputStream中的數據如果不flush的話,重新連接的時候這部分數據就會丟失了。另外,當拋出異常,重新連接的時候,下載的起始位置也要重新設置,所以在代碼⑥處,即每次從inputStream中讀取數據之後,startPosition就要重新設置,count標識了已經下載的字節數。 


3、現在每個分段的下載線程都順利結束了,也都創建了相應的臨時文件,接下來在主線程中會對臨時文件進行合併,並寫入目標文件,最後刪除臨時文件。這部分很簡單,就是一個對所有下載線程進行遍歷的過程。這裏outputStream也有兩次flush,與上面類似,不再贅述。 

[java] view plaincopy
  1. private void tempFileToTargetFile(ChildThread[] childThreads) {  
  2.     try {  
  3.         BufferedOutputStream outputStream = new BufferedOutputStream(  
  4.                 new FileOutputStream(fileDir + fileName));  
  5.   
  6.         // 遍歷所有子線程創建的臨時文件,按順序把下載內容寫入目標文件中  
  7.         for (int i = 0; i < threadNum; i++) {  
  8.             if (statusError) {  
  9.                 for (int k = 0; k < threadNum; k++) {  
  10.                     if (childThreads[k].tempFile.length() == 0)  
  11.                         childThreads[k].tempFile.delete();  
  12.                 }  
  13.                 System.out.println("本次下載任務不成功,請重新設置線程數。");  
  14.                 break;  
  15.             }  
  16.   
  17.             BufferedInputStream inputStream = new BufferedInputStream(  
  18.                     new FileInputStream(childThreads[i].tempFile));  
  19.             System.out.println("Now is file " + childThreads[i].id);  
  20.             int len = 0;  
  21.             long count = 0;  
  22.             byte[] b = new byte[1024];  
  23.             while ((len = inputStream.read(b)) != -1) {  
  24.                 count += len;  
  25.                 outputStream.write(b, 0, len);  
  26.                 if ((count % 4096) == 0) {  
  27.                     outputStream.flush();  
  28.                 }  
  29.   
  30.                 // b = new byte[1024];  
  31.             }  
  32.   
  33.             inputStream.close();  
  34.             // 刪除臨時文件  
  35.             if (childThreads[i].status == ChildThread.STATUS_HAS_FINISHED) {  
  36.                 childThreads[i].tempFile.delete();  
  37.             }  
  38.         }  
  39.   
  40.         outputStream.flush();  
  41.         outputStream.close();  
  42.     } catch (FileNotFoundException e) {  
  43.         e.printStackTrace();  
  44.     } catch (IOException e) {  
  45.         e.printStackTrace();  
  46.     }  
  47. }  


4、最後,說說斷點續傳,前面爲了實現斷點續傳,在每個下載線程中都創建了一個臨時文件,現在我們就要利用這個臨時文件來設置斷點的位置。由於臨時文件的命名方式都是固定的,所以我們就專門找對應下載的目標文件的臨時文件,臨時文件中已經下載的字節數就是我們需要的斷點位置。startPos是一個數組,存放了每個線程的已下載的字節數。

[java] view plaincopy
  1. //第一步,分析已下載的臨時文件,設置斷點,如果是新的下載任務,則建立目標文件。  
  2. private long[] setThreadBreakpoint(String fileDir2, String fileName2,  
  3.         long contentLength, long[] startPos) {  
  4.     File file = new File(fileDir + fileName);  
  5.     long localFileSize = file.length();  
  6.   
  7.     if (file.exists()) {  
  8.         System.out.println("file " + fileName + " has exists!");  
  9.         // 下載的目標文件已存在,判斷目標文件是否完整  
  10.         if (localFileSize < contentLength) {  
  11.             System.out.println("Now download continue ... ");  
  12.   
  13.             // 遍歷目標文件的所有臨時文件,設置斷點的位置,即每個臨時文件的長度  
  14.             File tempFileDir = new File(fileDir);  
  15.             File[] files = tempFileDir.listFiles();  
  16.             for (int k = 0; k < files.length; k++) {  
  17.                 String tempFileName = files[k].getName();  
  18.                 // 臨時文件的命名方式爲:目標文件名+"_"+編號  
  19.                 if (tempFileName != null && files[k].length() > 0  
  20.                         && tempFileName.startsWith(fileName + "_")) {  
  21.                     int fileLongNum = Integer.parseInt(tempFileName  
  22.                             .substring(tempFileName.lastIndexOf("_") + 1,  
  23.                                     tempFileName.lastIndexOf("_") + 2));  
  24.                     // 爲每個線程設置已下載的位置  
  25.                     startPos[fileLongNum] = files[k].length();  
  26.                 }  
  27.             }  
  28.         }  
  29.     } else {  
  30.         // 如果下載的目標文件不存在,則創建新文件  
  31.         try {  
  32.             file.createNewFile();  
  33.         } catch (IOException e) {  
  34.             e.printStackTrace();  
  35.         }  
  36.     }  
  37.   
  38.     return startPos;  
  39. }  


5、測試 

[java] view plaincopy
  1. public class DownloadStartup {  
  2.     private static final String encoding = "utf-8";   
  3.     public static void main(String[] args) {  
  4.         DownloadTask downloadManager = new DownloadTask();        
  5.         String urlStr = "http://apache.freelamp.com/velocity/tools/1.4/velocity-tools-1.4.zip";       
  6.         downloadManager.setSleepSeconds(5);  
  7.         downloadManager.download(urlStr, encoding);  
  8.     }  
  9. }  


測試從apache下載一個velocity的壓縮包,臨時文件保留,看一下下載結果:

另:在測試從skycn下載軟件的過程中,碰到了一個錯誤: 
java.io.IOException: Server returned HTTP response code: 416 for URL: http://www.skycn.com/ 
上網查了一下:416  Requested Range Not Satisfiable  服務器不能滿足客戶在請求中指定的Range頭,於是把threadNum改爲1就可以了。 

這個下載功能現在只是完成了很基礎的一部分,最初的初衷就是爲了演練一下CountdownLatch。CountdownLatch就是一個計數器,就像一個攔截的柵欄,用await()方法來把柵欄關上,線程就跑不下去了,只有等計數器減爲0的時候,柵欄纔會自動打開,被暫停的線程纔會繼續運行。CountdownLatch的應用場景可以有很多,分段下載就是一個很好的例子。 

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