天天看點

Java網絡程式設計從入門到精通(24):實作HTTP斷點續傳下載下傳工具(附源代碼)

本文為原創,如需轉載,請注明作者和出處,謝謝!

    在前面的文章曾讨論了HTTP消息頭的三個和斷點繼傳有關的字段。一個是請求消息的字段Range,另兩個是響應消息字段Accept-Ranges和Content-Range。其中Accept-Ranges用來斷定Web伺服器是否支援斷點繼傳功能。在這裡為了示範如何實作斷點繼傳功能,假設Web伺服器支援這個功能;是以,我們隻使用Range和Content-Range來完成一個斷點繼傳工具的開發。

l        

要實作一個什麼樣的斷點續傳工具?

這個斷點續工具是一個單線程的下載下傳工具。它通過參數傳入一個文本檔案。這個檔案的格式如下:

http://www.ishare.cc/d/1174254-2/106.jpg  d:/ok1.jpg  8192

http://www.ishare.cc/d/1174292-2/156.jpg   d:/ok2.jpg   12345

http://www.ishare.cc/d/1174277-2/147.jpg   d:/ok3.jpg  3456

這個文本檔案的每一行是一個下載下傳項,這個下載下傳項分為三部分:

要下載下傳的Web資源的URL。

要儲存的本地檔案名。

下載下傳的緩沖區大小(機關是位元組)。

使用至少一個空格來分隔這三部分。這個下載下傳工具逐個下載下傳這些檔案,在這些檔案全部下載下傳完後程式退出。

斷點續傳的工作原理

“斷點續傳”顧名思義,就是一個檔案下載下傳了一部分後,由于伺服器或用戶端的原因,目前的網絡連接配接中斷了。在中斷網絡連接配接後,使用者還可以再次建立網絡連接配接來繼續下載下傳這個檔案還沒有下完的部分。

要想實作單線程斷點續傳,必須在客戶斷儲存兩個資料。

1.      

已經下載下傳的位元組數。

2.      

下載下傳檔案的URL。

一但重建立立網絡連接配接後,就可以利用這兩個資料接着未下載下傳完的檔案繼續下載下傳。在本下載下傳工具中第一種資料就是檔案已經下載下傳的位元組數,而第二個資料在上述的下載下傳檔案中儲存。

在繼續下載下傳時檢測已經下載下傳的位元組數,假設已經下載下傳了3000個位元組,那麼HTTP請求消息頭的Range字段被設為如下形式:

Range: bytes=3000-

HTTP響應消息頭的Content-Range字段被設為如下的形式:

Content-Range: bytes 3000-10000/10001

實作斷點續傳下載下傳工具

一個斷點續傳下載下傳程式可按如下幾步實作:

輸入要下載下傳檔案的URL和要儲存的本地檔案名,并通過Socket類連接配接到這個URL

所指的伺服器上。

在用戶端根據下載下傳檔案的URL和這個本地檔案生成HTTP請求消息。在生成請求

消息時分為兩種情況:

(1)第一次下載下傳這個檔案,按正常情況生成請求消息,也就是說生成不包含Range

字段的請求消息。

(2)以前下載下傳過,這次是接着下載下傳這個檔案。這就進入了斷點續傳程式。在這種情況生成的HTTP請求消息中必須包含Range字段。由于是單線程下載下傳,是以,這個已經下載下傳了一部分的檔案的大小就是Range的值。假設目前檔案的大小是1234個位元組,那麼将Range設成如下的值:

Range:bytes=1234-

3.      

向伺服器發送HTTP請求消息。

4.      

接收伺服器傳回的HTTP響應消息。

5.      

處理HTTP響應消息。在本程式中需要從響應消息中得到下載下傳檔案的總位元組數。如

果是第一次下載下傳,也就是說響應消息中不包含Content-Range字段時,這個總位元組數也就是Content-Length字段的值。如果響應消息中不包含Content-Length字段,則這個總位元組數無法确定。這就是為什麼使用下載下傳工具下載下傳一些檔案時沒有檔案大小和下載下傳進度的原因。如果響應消息中包含Content-Range字段,總位元組數就是Content-Range:bytes m-n/k中的k,如Content-Range的值為:

Content-Range:bytes 1000-5000/5001

則總位元組數為5001。由于本程式使用的Range值類型是得到從某個位元組開始往後的所有位元組,是以,目前的響應消息中的Content-Range總是能傳回還有多少個位元組未下載下傳。如上面的例子未下載下傳的位元組數為5000-1000+1=4001。

6. 開始下載下傳檔案,并計算下載下傳進度(百分比形式)。如果網絡連接配接斷開時,檔案仍未下載下傳完,重新執行第一步。也果檔案已經下載下傳完,退出程式。

分析以上六個步驟得知,有四個主要的功能需要實作:

1. 生成HTTP請求消息,并将其發送到伺服器。這個功能由generateHttpRequest方法來完成。

2. 分析HTTP響應消息頭。這個功能由analyzeHttpHeader方法來完成。

3. 得到下載下傳檔案的實際大小。這個功能由getFileSize方法來完成。

4. 下載下傳檔案。這個功能由download方法來完成。

以上四個方法均被包含在這個斷點續傳工具的核心類HttpDownload.java中。在給出HttpDownload類的實作之前先給出一個接口DownloadEvent接口,從這個接口的名字就可以看出,它是用來處理下載下傳過程中的事件的。下面是這個接口的實作代碼:

  package download;

  public interface DownloadEvent

  {

      void percent(long n);             // 下載下傳進度

      void state(String s);              // 連接配接過程中的狀态切換

      void viewHttpHeaders(String s);    // 枚舉每一個響應消息字段

  }

從上面的代碼可以看出,DownloadEvent接口中有三個事件方法。在以後的主函數中将實作這個接口,來向控制台輸出相應的資訊。下面給出了HttpDownload類的主體架構代碼:

  001  package download;

  002  

  003  import java.net.*;

  004  import java.io.*;

  005  import java.util.*;

  006  

  007  public class HttpDownload

  008  {

  009      private HashMap httpHeaders = new HashMap();

  010      private String stateCode;

  011  

  012      // generateHttpRequest方法

  013      

  014      /*  ananlyzeHttpHeader方法

  015       *  

  016       *  addHeaderToMap方法

  017       * 

  018       *  analyzeFirstLine方法

  019       */     

  020  

  021      // getFileSize方法

  022  

  023      // download方法

  024          

  025      /*  getHeader方法

  026       *  

  027       *  getIntHeader方法

  028       */

  029  }

上面的代碼隻是HttpDownload類的架構代碼,其中的方法并未直正實作。我們可以從中看出第012、014、021和023行就是上述的四個主要的方法。在016和018行的addHeaderToMap和analyzeFirstLine方法将在analyzeHttpHeader方法中用到。而025和027行的getHeader和getIntHeader方法在getFileSize和download方法都會用到。上述的八個方法的實作都會在後面給出。

  001  private void generateHttpRequest(OutputStream out, String host,

  002          String path, long startPos) throws IOException

  003  {

  004      OutputStreamWriter writer = new OutputStreamWriter(out);

  005      writer.write("GET " + path + " HTTP/1.1/r/n");

  006      writer.write("Host: " + host + "/r/n");

  007      writer.write("Accept: */*/r/n");

  008      writer.write("User-Agent: My First Http Download/r/n");

  009      if (startPos > 0) // 如果是斷點續傳,加入Range字段

  010          writer.write("Range: bytes=" + String.valueOf(startPos) + "-/r/n");

  011      writer.write("Connection: close/r/n/r/n");

  012      writer.flush();

  013  }

這個方法有四個參數:

1.  

OutputStream out

使用Socket對象的getOutputStream方法得到的輸出流。

2.  String host

下載下傳檔案所在的伺服器的域名或IP。

3.  String path

       下載下傳檔案在伺服器上的路徑,也就跟在GET方法後面的部分。

4.  long startPos

       從檔案的startPos位置開始下載下傳。如果startPos為0,則不生成Range字段。

  001  private void analyzeHttpHeader(InputStream inputStream, DownloadEvent de)

  002        throws Exception

  004      String s = "";

  005      byte b = -1;

  006      while (true)

  007      {

  008          b = (byte) inputStream.read();

  009          if (b == '/r')

  010          {

  011              b = (byte) inputStream.read();

  012              if (b == '/n')

  013              {

  014                  if (s.equals(""))

  015                      break;

  016                  de.viewHttpHeaders(s);

  017                  addHeaderToMap(s);

  018                  s = "";

  019              }

  020          }

  021          else

  022              s += (char) b;

  023      }

  024  }

  025

  026  private void analyzeFirstLine(String s)

  027  {

  028      String[] ss = s.split("[ ]+");

  029      if (ss.length > 1)

  030          stateCode = ss[1];

  031  }

  032  private void addHeaderToMap(String s)

  033  {

  034      int index = s.indexOf(":");

  035      if (index > 0)

  036          httpHeaders.put(s.substring(0, index), s.substring(index + 1) .trim());

  037      else

  038          analyzeFirstLine(s);

  039  }

第001 〜 024行:analyzeHttpHeader方法的實作。這個方法有兩個參數。其中inputStream是用Socket對象的getInputStream方法得到的輸入流。這個方法是直接使用位元組流來分析的HTTP響應頭(主要是因為下載下傳的檔案不一定是文本檔案;是以,都統一使用位元組流來分析和下載下傳),每兩個""r"n"之間的就是一個字段和字段值對。在016行調用了DownloadEvent接口的viewHttpHeaders事件方法來枚舉每一個響應頭字段。

第026 〜 031行:analyzeFirstLine方法的實作。這個方法的功能是分析響應消息頭的第一行,并從中得到狀态碼後,将其儲存在stateCode變量中。這個方法的參數s就是響應消息頭的第一行。

第032 〜 039行:addHeaderToMap方法的實作。這個方法的功能是将每一個響應請求消息字段和字段值加到在HttpDownload類中定義的httpHeaders哈希映射中。在第034行查找每一行消息頭是否包含":",如果包含":",這一行必是消息頭的第一行。是以,在第038行調用了analyzeFirstLine方法從第一行得到響應狀态碼。

  001  private String getHeader(String header)

  002  {

  003      return (String) httpHeaders.get(header);

  004  }

  005  private int getIntHeader(String header)

  006  {

  007      return Integer.parseInt(getHeader(header));

  008  }

    這兩個方法将會在getFileSize和download中被調用。它們的功能是從響應消息中根據字段字得到相應的字段值。getHeader得到字元串形式的字段值,而getIntHeader得到整數型的字段值。

  001  public long getFileSize()

  003      long length = -1;

  004      try

  005      {

  006          length = getIntHeader("Content-Length");

  007          String[] ss = getHeader("Content-Range").split("[/]");

  008          if (ss.length > 1)

  009              length = Integer.parseInt(ss[1]);

  010          else

  011              length = -1;

  012      }

  013      catch (Exception e)

  014      {

  015      }

  016      return length;

  017  }

getFileSize方法的功能是得到下載下傳檔案的實際大小。首先在006行通過Content-Length得到了目前響應消息的實體内容大小。然後在009行得到了Content-Range字段值所描述的檔案的實際大小("""後面的值)。如果Content-Range字段不存在,則檔案的實際大小就是Content-Length字段的值。如果Content-Length字段也不存在,則傳回-1,表示檔案實際大小無法确定。

  001  public void download(DownloadEvent de, String url, String localFN,

  002          int cacheSize) throws Exception

  004      File file = new File(localFN); 

  005      long finishedSize = 0;

  006      long fileSize = 0;  // localFN所指的檔案的實際大小

  007      FileOutputStream fileOut = new FileOutputStream(localFN, true);

  008      URL myUrl = new URL(url);

  009      Socket socket = new Socket();

  010      byte[] buffer = new byte[cacheSize]; // 下載下傳資料的緩沖

  012      if (file.exists())

  013          finishedSize = file.length();        

  014      

  015      // 得到要下載下傳的Web資源的端口号,未提供,預設是80

  016      int port = (myUrl.getPort() == -1) ? 80 : myUrl.getPort();

  017      de.state("正在連接配接" + myUrl.getHost() + ":" + String.valueOf(port));

  018      socket.connect(new InetSocketAddress(myUrl.getHost(), port), 20000);

  019      de.state("連接配接成功!");

  020      

  021      // 産生HTTP請求消息

  022      generateHttpRequest(socket.getOutputStream(), myUrl.getHost(), myUrl

  023              .getPath(), finishedSize);

  024        

  025      InputStream inputStream = socket.getInputStream();

  026      // 分析HTTP響應消息頭

  027      analyzeHttpHeader(inputStream, de);

  028      fileSize = getFileSize();  // 得到下載下傳檔案的實際大小

  029      if (finishedSize >= fileSize)  

  030          return;

  031      else

  032      {

  033          if (finishedSize > 0 && stateCode.equals("200"))

  034              return;

  035      }

  036      if (stateCode.charAt(0) != '2')

  037          throw new Exception("不支援的響應碼");

  038      int n = 0;

  039      long m = finishedSize;

  040      while ((n = inputStream.read(buffer)) != -1)

  041      {

  042          fileOut.write(buffer, 0, n);

  043          m += n;

  044          if (fileSize != -1)

  045          {

  046              de.percent(m * 100 / fileSize);

  047          }

  048      }

  049      fileOut.close();

  050      socket.close();

  051  }

download方法是斷點續傳工具的核心方法。它有四個參數:

1. DownloadEvent de

用于處理下載下傳事件的接口。

2. String url

要下載下傳檔案的URL。

3. String localFN

要儲存的本地檔案名,可以用這個檔案的大小來确定已經下載下傳了多少個位元組。

4. int cacheSize

下載下傳資料的緩沖區。也就是一次從伺服器下載下傳多個位元組。這個值不宜太小,因為,頻繁地從伺服器下載下傳資料,會降低網絡的使用率。一般可以将這個值設為8192(8K)。

為了分析下載下傳檔案的url,在008行使用了URL類,這個類在以後還會介紹,在這裡隻要知道使用這個類可以将使用各種協定的url(包括HTTP和FTP協定)的各個部分分解,以便單獨使用其中的一部分。

第029行:根據檔案的實際大小和已經下載下傳的位元組數(finishedSize)來判斷是否檔案是否已經下載下傳完成。當檔案的實際大小無法确定時,也就是fileSize傳回-1時,不能下載下傳。

第033行:如果檔案已經下載下傳了一部分,并且傳回的狀态碼仍是200(應該是206),則表明伺服器并不支援斷點續傳。當然,這可以根據另一個字段Accept-Ranges來判斷。

第036行:由于本程式未考慮重定向(狀态碼是3xx)的情況,是以,在使用download時,不要下載下傳傳回3xx狀态碼的Web資源。

第040 〜 048行:開始下載下傳檔案。第046行調用DownloadEvent的percent方法來傳回下載下傳進度。

  003  import java.io.*;

  004  

  005  class NewProgress implements DownloadEvent

  007      private long oldPercent = -1;

  008      public void percent(long n)

  009      {

  010          if (n > oldPercent)

  011          {

  012              System.out.print("[" + String.valueOf(n) + "%]");

  013              oldPercent = n;

  014          }

  016      public void state(String s)

  017      {

  018          System.out.println(s);

  019      }

  020      public void viewHttpHeaders(String s)

  021      {

  022          System.out.println(s);

  025  

  026  public class Main

  028      public static void main(String[] args) throws Exception

  029      {

  030          

  031          DownloadEvent progress = new NewProgress();

  032          if (args.length < 1)

  033          {

  034              System.out.println("用法:java class 下載下傳檔案名");

  035              return;

  036          }

  037          FileInputStream fis = new FileInputStream(args[0]);

  038          BufferedReader fileReader = new BufferedReader(new InputStreamReader(

  039                          fis));

  040          String s = "";

  041          String[] ss;

  042          while ((s = fileReader.readLine()) != null)

  043          {

  044              try

  045              {

  046                  ss = s.split("[ ]+");

  047                  if (ss.length > 2)

  048                  {

  049                      System.out.println("/r/n---------------------------");

  050                      System.out.println("正在下載下傳:" + ss[0]);

  051                      System.out.println("檔案儲存位置:" + ss[1]);

  052                      System.out.println("下載下傳緩沖區大小:" + ss[2]);

  053                      System.out.println("---------------------------");

  054                      HttpDownload httpDownload = new HttpDownload();

  055                      httpDownload.download(new NewProgress(), ss[0], ss[1],

  056                                      Integer.parseInt(ss[2]));

  057                  }

  058              }

  059             catch (Exception e)

  060              {

  061                  System.out.println(e.getMessage());

  062              }

  063          }

  064          fileReader.close();

  065      }

  066  }

第005 〜 024行:實作DownloadEvent接口的NewDownloadEvent類。用于在Main函數裡接收相應事件傳遞的資料。

第026 〜 065 行:下載下傳工具的Main方法。在這個Main方法裡,打開下載下傳資源清單檔案,逐行下載下傳相應的Web資源。

測試

假設download.txt在目前目錄中,内容如下:

http://files.cnblogs.com/nokiaguy/HttpSimulator.rar HttpSimulator.rar 8192

http://files.cnblogs.com/nokiaguy/designpatterns.rar designpatterns.rar 4096

http://files.cnblogs.com/nokiaguy/download.rar download.rar 8192

這兩個URL是在本機的Web伺服器(如IIS)的虛拟目錄中的兩個檔案,将它們下載下傳在D盤根目錄。

運作下面的指令:

java download.Main download.txt

    運作的結果如圖1所示。

圖1

<a href="http://www.eoeandroid.com/forumdisplay.php?fid=4">國内最棒的Google Android技術社群(eoeandroid),歡迎通路!</a>

繼續閱讀