Java網(wǎng)絡(luò)編程從入門(mén)到精通(24):實(shí)現(xiàn)HTTP斷點(diǎn)續(xù)傳下載工具(附源代碼)
本文為原創(chuàng),如需轉(zhuǎn)載,請(qǐng)注明作者和出處,謝謝!
上一篇:Java網(wǎng)絡(luò)編程從入門(mén)到精通(23):HTTP消息頭字段
源代碼下載:download.rar
在前面的文章曾討論了HTTP消息頭的三個(gè)和斷點(diǎn)繼傳有關(guān)的字段。一個(gè)是請(qǐng)求消息的字段Range,另兩個(gè)是響應(yīng)消息字段Accept-Ranges和Content-Range。其中Accept-Ranges用來(lái)斷定Web服務(wù)器是否支持?jǐn)帱c(diǎn)繼傳功能。在這里為了演示如何實(shí)現(xiàn)斷點(diǎn)繼傳功能,假設(shè)Web服務(wù)器支持這個(gè)功能;因此,我們只使用Range和Content-Range來(lái)完成一個(gè)斷點(diǎn)繼傳工具的開(kāi)發(fā)。
l 要實(shí)現(xiàn)一個(gè)什么樣的斷點(diǎn)續(xù)傳工具?
這個(gè)斷點(diǎn)續(xù)工具是一個(gè)單線(xiàn)程的下載工具。它通過(guò)參數(shù)傳入一個(gè)文本文件。這個(gè)文件的格式如下:
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
這個(gè)文本文件的每一行是一個(gè)下載項(xiàng),這個(gè)下載項(xiàng)分為三部分:
- 要下載的Web資源的URL。
- 要保存的本地文件名。
- 下載的緩沖區(qū)大小(單位是字節(jié))。
使用至少一個(gè)空格來(lái)分隔這三部分。這個(gè)下載工具逐個(gè)下載這些文件,在這些文件全部下載完后程序退出。
l 斷點(diǎn)續(xù)傳的工作原理
“斷點(diǎn)續(xù)傳”顧名思義,就是一個(gè)文件下載了一部分后,由于服務(wù)器或客戶(hù)端的原因,當(dāng)前的網(wǎng)絡(luò)連接中斷了。在中斷網(wǎng)絡(luò)連接后,用戶(hù)還可以再次建立網(wǎng)絡(luò)連接來(lái)繼續(xù)下載這個(gè)文件還沒(méi)有下完的部分。
要想實(shí)現(xiàn)單線(xiàn)程斷點(diǎn)續(xù)傳,必須在客戶(hù)斷保存兩個(gè)數(shù)據(jù)。
1. 已經(jīng)下載的字節(jié)數(shù)。
2. 下載文件的URL。
一但重新建立網(wǎng)絡(luò)連接后,就可以利用這兩個(gè)數(shù)據(jù)接著未下載完的文件繼續(xù)下載。在本下載工具中第一種數(shù)據(jù)就是文件已經(jīng)下載的字節(jié)數(shù),而第二個(gè)數(shù)據(jù)在上述的下載文件中保存。
在繼續(xù)下載時(shí)檢測(cè)已經(jīng)下載的字節(jié)數(shù),假設(shè)已經(jīng)下載了3000個(gè)字節(jié),那么HTTP請(qǐng)求消息頭的Range字段被設(shè)為如下形式:
HTTP響應(yīng)消息頭的Content-Range字段被設(shè)為如下的形式:
l 實(shí)現(xiàn)斷點(diǎn)續(xù)傳下載工具
一個(gè)斷點(diǎn)續(xù)傳下載程序可按如下幾步實(shí)現(xiàn):
1. 輸入要下載文件的URL和要保存的本地文件名,并通過(guò)Socket類(lèi)連接到這個(gè)URL
所指的服務(wù)器上。
2. 在客戶(hù)端根據(jù)下載文件的URL和這個(gè)本地文件生成HTTP請(qǐng)求消息。在生成請(qǐng)求
消息時(shí)分為兩種情況:
(1)第一次下載這個(gè)文件,按正常情況生成請(qǐng)求消息,也就是說(shuō)生成不包含Range
字段的請(qǐng)求消息。
(2)以前下載過(guò),這次是接著下載這個(gè)文件。這就進(jìn)入了斷點(diǎn)續(xù)傳程序。在這種情況生成的HTTP請(qǐng)求消息中必須包含Range字段。由于是單線(xiàn)程下載,因此,這個(gè)已經(jīng)下載了一部分的文件的大小就是Range的值。假設(shè)當(dāng)前文件的大小是1234個(gè)字節(jié),那么將Range設(shè)成如下的值:
3. 向服務(wù)器發(fā)送HTTP請(qǐng)求消息。
4. 接收服務(wù)器返回的HTTP響應(yīng)消息。
5. 處理HTTP響應(yīng)消息。在本程序中需要從響應(yīng)消息中得到下載文件的總字節(jié)數(shù)。如
果是第一次下載,也就是說(shuō)響應(yīng)消息中不包含Content-Range字段時(shí),這個(gè)總字節(jié)數(shù)也就是Content-Length字段的值。如果響應(yīng)消息中不包含Content-Length字段,則這個(gè)總字節(jié)數(shù)無(wú)法確定。這就是為什么使用下載工具下載一些文件時(shí)沒(méi)有文件大小和下載進(jìn)度的原因。如果響應(yīng)消息中包含Content-Range字段,總字節(jié)數(shù)就是Content-Range:bytes m-n/k中的k,如Content-Range的值為:
則總字節(jié)數(shù)為5001。由于本程序使用的Range值類(lèi)型是得到從某個(gè)字節(jié)開(kāi)始往后的所有字節(jié),因此,當(dāng)前的響應(yīng)消息中的Content-Range總是能返回還有多少個(gè)字節(jié)未下載。如上面的例子未下載的字節(jié)數(shù)為5000-1000+1=4001。
6. 開(kāi)始下載文件,并計(jì)算下載進(jìn)度(百分比形式)。如果網(wǎng)絡(luò)連接斷開(kāi)時(shí),文件仍未下載完,重新執(zhí)行第一步。也果文件已經(jīng)下載完,退出程序。
分析以上六個(gè)步驟得知,有四個(gè)主要的功能需要實(shí)現(xiàn):
1. 生成HTTP請(qǐng)求消息,并將其發(fā)送到服務(wù)器。這個(gè)功能由generateHttpRequest方法來(lái)完成。
2. 分析HTTP響應(yīng)消息頭。這個(gè)功能由analyzeHttpHeader方法來(lái)完成。
3. 得到下載文件的實(shí)際大小。這個(gè)功能由getFileSize方法來(lái)完成。
4. 下載文件。這個(gè)功能由download方法來(lái)完成。
以上四個(gè)方法均被包含在這個(gè)斷點(diǎn)續(xù)傳工具的核心類(lèi)HttpDownload.java中。在給出HttpDownload類(lèi)的實(shí)現(xiàn)之前先給出一個(gè)接口DownloadEvent接口,從這個(gè)接口的名字就可以看出,它是用來(lái)處理下載過(guò)程中的事件的。下面是這個(gè)接口的實(shí)現(xiàn)代碼:
public interface DownloadEvent
{
void percent(long n); // 下載進(jìn)度
void state(String s); // 連接過(guò)程中的狀態(tài)切換
void viewHttpHeaders(String s); // 枚舉每一個(gè)響應(yīng)消息字段
}
從上面的代碼可以看出,DownloadEvent接口中有三個(gè)事件方法。在以后的主函數(shù)中將實(shí)現(xiàn)這個(gè)接口,來(lái)向控制臺(tái)輸出相應(yīng)的信息。下面給出了HttpDownload類(lèi)的主體框架代碼:
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類(lèi)的框架代碼,其中的方法并未直正實(shí)現(xiàn)。我們可以從中看出第012、014、021和023行就是上述的四個(gè)主要的方法。在016和018行的addHeaderToMap和analyzeFirstLine方法將在analyzeHttpHeader方法中用到。而025和027行的getHeader和getIntHeader方法在getFileSize和download方法都會(huì)用到。上述的八個(gè)方法的實(shí)現(xiàn)都會(huì)在后面給出。
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) // 如果是斷點(diǎn)續(xù)傳,加入Range字段
010 writer.write("Range: bytes=" + String.valueOf(startPos) + "-\r\n");
011 writer.write("Connection: close\r\n\r\n");
012 writer.flush();
013 }
這個(gè)方法有四個(gè)參數(shù):
1. OutputStream out
使用Socket對(duì)象的getOutputStream方法得到的輸出流。
2. String host
下載文件所在的服務(wù)器的域名或IP。
3. String path
下載文件在服務(wù)器上的路徑,也就跟在GET方法后面的部分。
4. long startPos
從文件的startPos位置開(kāi)始下載。如果startPos為0,則不生成Range字段。
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方法的實(shí)現(xiàn)。這個(gè)方法有兩個(gè)參數(shù)。其中inputStream是用Socket對(duì)象的getInputStream方法得到的輸入流。這個(gè)方法是直接使用字節(jié)流來(lái)分析的HTTP響應(yīng)頭(主要是因?yàn)橄螺d的文件不一定是文本文件;因此,都統(tǒng)一使用字節(jié)流來(lái)分析和下載),每?jī)蓚€(gè)""r"n"之間的就是一個(gè)字段和字段值對(duì)。在016行調(diào)用了DownloadEvent接口的viewHttpHeaders事件方法來(lái)枚舉每一個(gè)響應(yīng)頭字段。
第026 ? 031行:analyzeFirstLine方法的實(shí)現(xiàn)。這個(gè)方法的功能是分析響應(yīng)消息頭的第一行,并從中得到狀態(tài)碼后,將其保存在stateCode變量中。這個(gè)方法的參數(shù)s就是響應(yīng)消息頭的第一行。
第032 ? 039行:addHeaderToMap方法的實(shí)現(xiàn)。這個(gè)方法的功能是將每一個(gè)響應(yīng)請(qǐng)求消息字段和字段值加到在HttpDownload類(lèi)中定義的httpHeaders哈希映射中。在第034行查找每一行消息頭是否包含":",如果包含":",這一行必是消息頭的第一行。因此,在第038行調(diào)用了analyzeFirstLine方法從第一行得到響應(yīng)狀態(tài)碼。
002 {
003 return (String) httpHeaders.get(header);
004 }
005 private int getIntHeader(String header)
006 {
007 return Integer.parseInt(getHeader(header));
008 }
這兩個(gè)方法將會(huì)在getFileSize和download中被調(diào)用。它們的功能是從響應(yīng)消息中根據(jù)字段字得到相應(yīng)的字段值。getHeader得到字符串形式的字段值,而getIntHeader得到整數(shù)型的字段值。
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方法的功能是得到下載文件的實(shí)際大小。首先在006行通過(guò)Content-Length得到了當(dāng)前響應(yīng)消息的實(shí)體內(nèi)容大小。然后在009行得到了Content-Range字段值所描述的文件的實(shí)際大小("""后面的值)。如果Content-Range字段不存在,則文件的實(shí)際大小就是Content-Length字段的值。如果Content-Length字段也不存在,則返回-1,表示文件實(shí)際大小無(wú)法確定。
002 int cacheSize) throws Exception
003 {
004 File file = new File(localFN);
005 long finishedSize = 0;
006 long fileSize = 0; // localFN所指的文件的實(shí)際大小
007 FileOutputStream fileOut = new FileOutputStream(localFN, true);
008 URL myUrl = new URL(url);
009 Socket socket = new Socket();
010 byte[] buffer = new byte[cacheSize]; // 下載數(shù)據(jù)的緩沖
011
012 if (file.exists())
013 finishedSize = file.length();
014
015 // 得到要下載的Web資源的端口號(hào),未提供,默認(rèn)是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 // 產(chǎn)生HTTP請(qǐng)求消息
022 generateHttpRequest(socket.getOutputStream(), myUrl.getHost(), myUrl
023 .getPath(), finishedSize);
024
025 InputStream inputStream = socket.getInputStream();
026 // 分析HTTP響應(yīng)消息頭
027 analyzeHttpHeader(inputStream, de);
028 fileSize = getFileSize(); // 得到下載文件的實(shí)際大小
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("不支持的響應(yīng)碼");
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方法是斷點(diǎn)續(xù)傳工具的核心方法。它有四個(gè)參數(shù):
1. DownloadEvent de
用于處理下載事件的接口。
2. String url
要下載文件的URL。
3. String localFN
要保存的本地文件名,可以用這個(gè)文件的大小來(lái)確定已經(jīng)下載了多少個(gè)字節(jié)。
4. int cacheSize
下載數(shù)據(jù)的緩沖區(qū)。也就是一次從服務(wù)器下載多個(gè)字節(jié)。這個(gè)值不宜太小,因?yàn)?,頻繁地從服務(wù)器下載數(shù)據(jù),會(huì)降低網(wǎng)絡(luò)的利用率。一般可以將這個(gè)值設(shè)為8192(8K)。
為了分析下載文件的url,在008行使用了URL類(lèi),這個(gè)類(lèi)在以后還會(huì)介紹,在這里只要知道使用這個(gè)類(lèi)可以將使用各種協(xié)議的url(包括HTTP和FTP協(xié)議)的各個(gè)部分分解,以便單獨(dú)使用其中的一部分。
第029行:根據(jù)文件的實(shí)際大小和已經(jīng)下載的字節(jié)數(shù)(finishedSize)來(lái)判斷是否文件是否已經(jīng)下載完成。當(dāng)文件的實(shí)際大小無(wú)法確定時(shí),也就是fileSize返回-1時(shí),不能下載。
第033行:如果文件已經(jīng)下載了一部分,并且返回的狀態(tài)碼仍是200(應(yīng)該是206),則表明服務(wù)器并不支持?jǐn)帱c(diǎn)續(xù)傳。當(dāng)然,這可以根據(jù)另一個(gè)字段Accept-Ranges來(lái)判斷。
第036行:由于本程序未考慮重定向(狀態(tài)碼是3xx)的情況,因此,在使用download時(shí),不要下載返回3xx狀態(tài)碼的Web資源。
第040 ? 048行:開(kāi)始下載文件。第046行調(diào)用DownloadEvent的percent方法來(lái)返回下載進(jìn)度。
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("下載緩沖區(qū)大小:" + 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行:實(shí)現(xiàn)DownloadEvent接口的NewDownloadEvent類(lèi)。用于在Main函數(shù)里接收相應(yīng)事件傳遞的數(shù)據(jù)。
第026 ? 065 行:下載工具的Main方法。在這個(gè)Main方法里,打開(kāi)下載資源列表文件,逐行下載相應(yīng)的Web資源。
測(cè)試
假設(shè)download.txt在當(dāng)前目錄中,內(nèi)容如下:
http://files.cnblogs.com/nokiaguy/designpatterns.rar designpatterns.rar 4096
http://files.cnblogs.com/nokiaguy/download.rar download.rar 8192
這兩個(gè)URL是在本機(jī)的Web服務(wù)器(如IIS)的虛擬目錄中的兩個(gè)文件,將它們下載在D盤(pán)根目錄。
運(yùn)行下面的命令:
運(yùn)行的結(jié)果如圖1所示。
下一篇:Java網(wǎng)絡(luò)編程從入門(mén)到精通(25):創(chuàng)建ServerSocket對(duì)象
《Android開(kāi)發(fā)完全講義(第2版)》(本書(shū)版權(quán)已輸出到臺(tái)灣)
http://product.dangdang.com/product.aspx?product_id=22741502
《Android高薪之路:Android程序員面試寶典 》http://book.360buy.com/10970314.html
新浪微博:http://t.sina.com.cn/androidguy 昵稱(chēng):李寧_Lining
posted on 2009-07-02 18:11 銀河使者 閱讀(4735) 評(píng)論(2) 編輯 收藏 所屬分類(lèi): java 、 原創(chuàng) 、網(wǎng)絡(luò)編程