在前面的文章曾討論了HTTP消息頭的三個和斷點繼傳有關的字段一個是請求消息的字段Range另兩個是響應消息字段AcceptRanges和ContentRange其中AcceptRanges用來斷定Web服務器是否支持斷點繼傳功能在這裡為了演示如何實現斷點繼傳功能假設Web服務器支持這個功能因此我們只使用Range和ContentRange來完成一個斷點繼傳工具的開發
● 要實現一個什麼樣的斷點續傳工具?
這個斷點續工具是一個單線程的下載工具它通過參數傳入一個文本文件這個文件的格式如下
/jpg d:\okjpg
/jpg d:\okjpg
/jpg d:\okjpg
這個文本文件的每一行是一個下載項這個下載項分為三部分
● 要下載的Web資源的URL
● 要保存的本地文件名
● 下載的緩沖區大小(單位是字節)
使用至少一個空格來分隔這三部分這個下載工具逐個下載這些文件在這些文件全部下載完後程序退出
● 斷點續傳的工作原理
斷點續傳顧名思義就是一個文件下載了一部分後由於服務器或客戶端的原因當前的網絡連接中斷了在中斷網絡連接後用戶還可以再次建立網絡連接來繼續下載這個文件還沒有下完的部分
要想實現單線程斷點續傳必須在客戶斷保存兩個數據
已經下載的字節數
下載文件的URL
一但重新建立網絡連接後就可以利用這兩個數據接著未下載完的文件繼續下載在本下載工具中第一種數據就是文件已經下載的字節數而第二個數據在上述的下載文件中保存
在繼續下載時檢測已經下載的字節數假設已經下載了個字節那麼HTTP請求消息頭的Range字段被設為如下形式
Range: bytes=
HTTP響應消息頭的ContentRange字段被設為如下的形式
ContentRange: bytes /
● 實現斷點續傳下載工具
一個斷點續傳下載程序可按如下幾步實現
輸入要下載文件的URL和要保存的本地文件名並通過Socket類連接到這個URL
所指的服務器上
在客戶端根據下載文件的URL和這個本地文件生成HTTP請求消息在生成請求
消息時分為兩種情況
()第一次下載這個文件按正常情況生成請求消息也就是說生成不包含Range
字段的請求消息
()以前下載過這次是接著下載這個文件這就進入了斷點續傳程序在這種情況生成的HTTP請求消息中必須包含Range字段由於是單線程下載因此這個已經下載了一部分的文件的大小就是Range的值假設當前文件的大小是個字節那麼將Range設成如下的值
Rangebytes=
向服務器發送HTTP請求消息
接收服務器返回的HTTP響應消息
處理HTTP響應消息在本程序中需要從響應消息中得到下載文件的總字節數如
果是第一次下載也就是說響應消息中不包含ContentRange字段時這個總字節數也就是ContentLength字段的值如果響應消息中不包含ContentLength字段則這個總字節數無法確定這就是為什麼使用下載工具下載一些文件時沒有文件大小和下載進度的原因如果響應消息中包含ContentRange字段總字節數就是ContentRangebytes mn/k中的k如ContentRange的值為
ContentRangebytes /
則總字節數為由於本程序使用的Range值類型是得到從某個字節開始往後的所有字節因此當前的響應消息中的ContentRange總是能返回還有多少個字節未下載如上面的例子未下載的字節數為+=
開始下載文件並計算下載進度(百分比形式)如果網絡連接斷開時文件仍未下載完重新執行第一步也果文件已經下載完退出程序
分析以上六個步驟得知有四個主要的功能需要實現
生成HTTP請求消息並將其發送到服務器這個功能由generateHttpRequest方法來完成
分析HTTP響應消息頭這個功能由analyzeHttpHeader方法來完成
得到下載文件的實際大小這個功能由getFileSize方法來完成
下載文件這個功能由download方法來完成
以上四個方法均被包含在這個斷點續傳工具的核心類HttpDownloadjava中在給出HttpDownload類的實現之前先給出一個接口DownloadEvent接口從這個接口的名字就可以看出它是用來處理下載過程中的事件的下面是這個接口的實現代碼
package download;
public interface DownloadEvent
{
void percent(long n); // 下載進度
void state(String s); // 連接過程中的狀態切換
void viewHttpHeaders(String s); // 枚舉每一個響應消息字段
}
從上面的代碼可以看出DownloadEvent接口中有三個事件方法在以後的主函數中將實現這個接口來向控制台輸出相應的信息下面給出了HttpDownload類的主體框架代碼
package download;
import *;
import javaio*;
import javautil*;
public class HttpDownload
{
private HashMap httpHeaders = new HashMap();
private String stateCode;
// generateHttpRequest方法
/* ananlyzeHttpHeader方法
*
* addHeaderToMap方法
*
* analyzeFirstLine方法
*/
// getFileSize方法
// download方法
/* getHeader方法
*
* getIntHeader方法
*/
}
上面的代碼只是HttpDownload類的框架代碼其中的方法並未直正實現我們可以從中看出第和行就是上述的四個主要的方法在和行的addHeaderToMap和analyzeFirstLine方法將在analyzeHttpHeader方法中用到而和行的getHeader和getIntHeader方法在getFileSize和download方法都會用到上述的八個方法的實現都會在後面給出
private void generateHttpRequest(OutputStream out String host
String path long startPos) throws IOException
{
OutputStreamWriter writer = new OutputStreamWriter(out);
writerwrite(GET + path + HTTP/\r\n);
writerwrite(Host: + host + \r\n);
writerwrite(Accept: */*\r\n);
writerwrite(UserAgent: My First Http Download\r\n);
if (startPos > ) // 如果是斷點續傳加入Range字段
writerwrite(Range: bytes= + StringvalueOf(startPos) + \r\n);
writerwrite(Connection: close\r\n\r\n);
writerflush();
}
這個方法有四個參數
OutputStream out
使用Socket對象的getOutputStream方法得到的輸出流
String host
下載文件所在的服務器的域名或IP
String path
下載文件在服務器上的路徑也就跟在GET方法後面的部分
long startPos
從文件的startPos位置開始下載如果startPos為則不生成Range字段
private void analyzeHttpHeader(InputStream inputStream DownloadEvent de)
throws Exception
{
String s = ;
byte b = ;
while (true)
{
b = (byte) inputStreamread();
if (b == \r)
{
b = (byte) inputStreamread();
if (b == \n)
{
if (sequals())
break;
deviewHttpHeaders(s);
addHeaderToMap(s);
s = ;
}
}
else
s += (char) b;
}
}
private void analyzeFirstLine(String s)
{
String[] ss = ssplit([ ]+);
if (sslength > )
stateCode = ss[];
}
private void addHeaderToMap(String s)
{
int index = sindexOf(:);
if (index > )
(ssubstring( index) ssubstring(index + ) trim());
else
analyzeFirstLine(s);
}
第 ;行analyzeHttpHeader方法的實現這個方法有兩個參數其中inputStream是用Socket對象的getInputStream方法得到的輸入流這個方法是直接使用字節流來分析的HTTP響應頭(主要是因為下載的文件不一定是文本文件因此都統一使用字節流來分析和下載)每兩個rn之間的就是一個字段和字段值對在行調用了DownloadEvent接口的viewHttpHeaders事件方法來枚舉每一個響應頭字段
第 ;行analyzeFirstLine方法的實現這個方法的功能是分析響應消息頭的第一行並從中得到狀態碼後將其保存在stateCode變量中這個方法的參數s就是響應消息頭的第一行
第 ;行addHeaderToMap方法的實現這個方法的功能是將每一個響應請求消息字段和字段值加到在HttpDownload類中定義的httpHeaders哈希映射中在第行查找每一行消息頭是否包含:如果包含:這一行必是消息頭的第一行因此在第行調用了analyzeFirstLine方法從第一行得到響應狀態碼
private String getHeader(String header)
{
return (String) (header);
}
private int getIntHeader(String header)
{
return IntegerparseInt(getHeader(header));
}
這兩個方法將會在getFileSize和download中被調用它們的功能是從響應消息中根據字段字得到相應的字段值getHeader得到字符串形式的字段值而getIntHeader得到整數型的字段值
public long getFileSize()
{
long length = ;
try
{
length = getIntHeader(ContentLength);
String[] ss = getHeader(ContentRange)split([/]);
if (sslength > )
length = IntegerparseInt(ss[]);
else
length = ;
}
catch (Exception e)
{
}
return length;
}
getFileSize方法的功能是得到下載文件的實際大小首先在行通過ContentLength得到了當前響應消息的實體內容大小然後在行得到了ContentRange字段值所描述的文件的實際大小(後面的值)如果ContentRange字段不存在則文件的實際大小就是ContentLength字段的值如果ContentLength字段也不存在則返回表示文件實際大小無法確定
public void download(DownloadEvent de String url String localFN
int cacheSize) throws Exception
{
File file = new File(localFN);
long finishedSize = ;
long fileSize = ; // localFN所指的文件的實際大小
FileOutputStream fileOut = new FileOutputStream(localFN true);
URL myUrl = new URL(url);
Socket socket = new Socket();
byte[] buffer = new byte[cacheSize]; // 下載數據的緩沖
if (fileexists())
finishedSize = filelength();
// 得到要下載的Web資源的端口號未提供默認是
int port = (myUrlgetPort() == ) ? : myUrlgetPort();
destate(正在連接 + myUrlgetHost() + : + StringvalueOf(port));
nnect(new InetSocketAddress(myUrlgetHost() port) );
destate(連接成功!);
// 產生HTTP請求消息
generateHttpRequest(socketgetOutputStream() myUrlgetHost() myUrl
getPath() finishedSize);
InputStream inputStream = socketgetInputStream();
// 分析HTTP響應消息頭
analyzeHttpHeader(inputStream de);
fileSize = getFileSize(); // 得到下載文件的實際大小
if (finishedSize >= fileSize)
return;
else
{
if (finishedSize > && stateCodeequals())
return;
}
if (stateCodecharAt() != )
throw new Exception(不支持的響應碼);
int n = ;
long m = finishedSize;
while ((n = inputStreamread(buffer)) != )
{
fileOutwrite(buffer n);
m += n;
if (fileSize != )
{
depercent(m * / fileSize);
}
}
fileOutclose();
socketclose();
}
download方法是斷點續傳工具的核心方法它有四個參數
DownloadEvent de
用於處理下載事件的接口
String url
要下載文件的URL
String localFN
要保存的本地文件名可以用這個文件的大小來確定已經下載了多少個字節
int cacheSize
下載數據的緩沖區也就是一次從服務器下載多個字節這個值不宜太小因為頻繁地從服務器下載數據會降低網絡的利用率一般可以將這個值設為(K)
為了分析下載文件的url在行使用了URL類這個類在以後還會介紹在這裡只要知道使用這個類可以將使用各種協議的url(包括HTTP和FTP協議)的各個部分分解以便單獨使用其中的一部分
第行根據文件的實際大小和已經下載的字節數(finishedSize)來判斷是否文件是否已經下載完成當文件的實際大小無法確定時也就是fileSize返回時不能下載
第行如果文件已經下載了一部分並且返回的狀態碼仍是(應該是)則表明服務器並不支持斷點續傳當然這可以根據另一個字段AcceptRanges來判斷
第行由於本程序未考慮重定向(狀態碼是xx)的情況因此在使用download時不要下載返回xx狀態碼的Web資源
第 ;行開始下載文件第行調用DownloadEvent的percent方法來返回下載進度
package download;
import javaio*;
class NewProgress implements DownloadEvent
{
private long oldPercent = ;
public void percent(long n)
{
if (n > oldPercent)
{
Systemoutprint([ + StringvalueOf(n) + %]);
oldPercent = n;
}
}
public void state(String s)
{
Systemoutprintln(s);
}
public void viewHttpHeaders(String s)
{
Systemoutprintln(s);
}
}
public class Main
{
public static void main(String[] args) throws Exception
{
DownloadEvent progress = new NewProgress();
if (argslength < )
{
Systemoutprintln(用法java class 下載文件名);
return;
}
FileInputStream fis = new FileInputStream(args[]);
BufferedReader fileReader = new BufferedReader(new InputStreamReader(
fis));
String s = ;
String[] ss;
while ((s = fileReaderreadLine()) != null)
{
try
{
ss = ssplit([ ]+);
if (sslength > )
{
Systemoutprintln(\r\n);
Systemoutprintln(正在下載: + ss[]);
Systemoutprintln(文件保存位置: + ss[]);
Systemoutprintln(下載緩沖區大小: + ss[]);
Systemoutprintln();
HttpDownload httpDownload = new HttpDownload();
(new NewProgress() ss[] ss[]
IntegerparseInt(ss[]));
}
}
catch (Exception e)
{
Systemoutprintln(egetMessage());
}
}
fileReaderclose();
}
}
第 ;行實現DownloadEvent接口的NewDownloadEvent類用於在Main函數裡接收相應事件傳遞的數據
第 ; 行下載工具的Main方法在這個Main方法裡打開下載資源列表文件逐行下載相應的Web資源
測試
假設downloadtxt在當前目錄中內容如下
;HttpSimulatorrar
;designpatternsrar
downloadrar
這兩個URL是在本機的Web服務器(如IIS)的虛擬目錄中的兩個文件將它們下載在D盤根目錄
運行下面的命令
java downloadMain downloadtxt
運行的結果如圖所示
![](http://tw.wingwit.com/Article/UploadFiles/201311/2013112319284548.jpg)
圖
From:http://tw.wingwit.com/Article/program/Java/hx/201311/26939.html