程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java網絡編程從入門到精通(24):實現HTTP斷點續傳下載工具

Java網絡編程從入門到精通(24):實現HTTP斷點續傳下載工具

編輯:關於JAVA

在前面的文章曾討論了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。

要保存的本地文件名。

下載的緩沖區大小(單位是字節)。

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

l 斷點續傳的工作原理

“斷點續傳”顧名思義,就是一個文件下載了一部分後,由於服務器或客戶端的原因,當前的網絡連接中斷了。在中斷網絡連接後,用戶還可以再次建立網絡連接來繼續下載這個文件還沒有下完的部分。

要想實現單線程斷點續傳,必須在客戶斷保存兩個數據。

1.已經下載的字節數。

2.下載文件的URL。

一但重新建立網絡連接後,就可以利用這兩個數據接著未下載完的文件繼續下載。在本下載工具中第一種數據就是文件已經下載的字節數,而第二個數據在上述的下載文件中保存。

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

Range: bytes=3000-

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

Content-Range: bytes 3000-10000/10001

l 實現斷點續傳下載工具

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

1.輸入要下載文件的URL和要保存的本地文件名,並通過Socket類連接到這個URL

所指的服務器上。

2.在客戶端根據下載文件的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
003  {
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()
002  {
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
003  {
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]; // 下載數據的緩沖
011
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方法來返回下載進度。

001  package download;
002  
003  import java.io.*;
004  
005  class NewProgress implements DownloadEvent
006  {
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          }
015      }
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);
023      }
024  }
025
026  public class Main
027  {
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

本文配套源碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved