在前面的文章曾討論了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
本文配套源碼