現在有一個文件需要我們進行下載,當我們下載了一部分的時候,出現情況了,比如:電腦死機、沒電、網絡中斷等等。 對於以上行爲,如果“下載”的行爲無法記錄本次下載的一個進度。那麼,當我們再次下載這個文件也就只能從頭來過。
所以,要實現讓一種斷開的行爲“續”起來的目的,關鍵就在於要有“介質”能夠記錄和讀取行爲出現”中斷”的這個節點的信息。
實際上這就是“斷點續傳”的基礎原理,用大白話說就是:我們要在下載行爲出現中斷的時候,記錄下中斷的位置信息,在新的下載行爲開始的時候,直接從記錄的這個位置開始下載內容,而不再從頭開始。
實現步驟:
1、當“上傳(下載)的行爲”出現中斷,我們需要記錄本次上傳(下載)的位置(position)。
2、當“續”這一行爲開始,我們直接跳轉到postion處繼續上傳(下載)的行爲。
實現方式:
要從文件已經下載的地方開始繼續下載,需要在客戶端瀏覽器傳給 Web 服務器的時候要多加一條信息(告訴服務器要從哪裏開始下載),如下面要求從 2000070 字節開始。
GET /abc.exe HTTP/1.0
User-Agent: NetFox
RANGE: bytes=2000070-
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
這裏多了一行 RANGE: bytes=2000070-
這一行的意思就是告訴服務器 abc.exe 穿上文件從 2000070 字節開始傳,前面的字節不用傳了。 服務器收到這個請求以後,返回的信息如下:
206
Content-Length=106786028
Content-Range=bytes 2000070-106786027/106786028
Date=Mon, 30 Apr 2001 12:55:20 GMT
ETag=W/"02ca57e173c11:95b"
Content-Type=application/octet-stream
Server=Microsoft-IIS/5.0
Last-Modified=Mon, 30 Apr 2001 12:55:20 GMT
返回的信息增加了一行: Content-Range=bytes 2000070-106786027/106786028
返回的代碼也變成 206 ,而不再是 200 了。知道了這些原理,就可以進行斷點續傳的編程了。
關鍵點:
1、用什麼方法實現提交 RANGE: bytes=2000070-。
用最原始的 Socket 是肯定能完成的,那樣太費事了,Java 的 net 包中已經提供了這種功能。代碼如下:
URL url = new URL("http://www.sjtu.edu.cn/abc.exe");
HttpURLConnection httpConnection = (HttpURLConnection)url.openConnection();
// 設置 User-Agent
httpConnection.setRequestProperty("User-Agent","NetFox");
// 設置斷點續傳的開始位置
httpConnection.setRequestProperty("RANGE","bytes=2000070");
// 獲得輸入流
InputStream input = httpConnection.getInputStream();
從輸入流中取出的字節流就是 abc.exe 文件從 2000070 開始的字節流。 其實斷點續傳用 Java 實現起來還是很簡單的,接下來要做的事就是怎麼保存獲得的流到文件中去。
2、保存文件採用的方法。
我採用的是 IO 包中的 RandAccessFile 類。 API文檔中對該類的說明:
此類的實例支持對隨機訪問文件的讀取和寫入。隨機訪問文件的行爲類似存儲在文件系統中的一個大型 byte 數組。
如果隨機訪問文件以讀取/寫入模式創建,則輸出操作也可用;輸出操作從文件指針開始寫入字節,並隨着對字節的寫入而前移此文件指針。
寫入隱含數組的當前末尾之後的輸出操作導致該數組擴展。該文件指針可以通過 getFilePointer 方法讀取,並通過 seek 方法設置。
操作相當簡單,假設從 2000070 處開始保存文件,代碼如下:
RandomAccess oSavedFile = new RandomAccessFile("down.zip","rw");
long nPos = 2000070;
// 定位文件指針到 nPos 位置
oSavedFile.seek(nPos);
byte[] b = new byte[1024];
int nRead;
// 從輸入流中讀入字節流,然後寫到文件中
while((nRead=input.read(b,0,1024)) > 0)
{
oSavedFile.write(b,0,nRead);
}
接下來要做的就是整合成一個完整的程序了。包括一系列的線程控制等等。
斷點續傳完整實現:
主要用了 6 個類,包括一個測試類:
SiteFileFetch.java 負責整個文件的抓取,控制內部線程 (FileSplitterFetch 類 )。
FileSplitterFetch.java 負責部分文件的抓取。
FileAccess.java 負責文件的存儲。
SiteInfoBean.java 要抓取的文件的信息,如文件保存的目錄,名字,抓取文件的 URL 等。
Utility.java 工具類,放一些簡單的方法。
TestMethod.java 測試類。
SiteFileFetch.java
package com.bocom.sb.http.file;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
/**
*
* 負責整個文件的抓取,控制內部線程(FileSplitterFetch類)
* @author xiaoming
* @since 2017-8-29
*/
public class SiteFileFetch extends Thread {
// 文件信息Bean
SiteInfoBean siteInfoBean = null;
// 開始位置
long[] nStartPos;
// 結束位置
long[] nEndPos;
// 子線程對象
FileSplitterFetch[] fileSplitterFetch;
// 文件長度
long nFileLength;
// 是否第一次取文件
boolean bFirst = true;
// 停止標誌
boolean bStop = false;
// 文件下載的臨時信息
File tmpFile;
// 輸出到文件的輸出流
DataOutputStream output;
public SiteFileFetch(SiteInfoBean bean) throws IOException {
siteInfoBean = bean;
// tmpFile = File.createTempFile ("zhong","1111",new
// File(bean.getSFilePath()));
tmpFile = new File(bean.getSFilePath() + File.separator + bean.getSFileName() + ".info");
if (tmpFile.exists()) {
bFirst = false;
read_nPos();
} else {
nStartPos = new long[bean.getNSplitter()];
nEndPos = new long[bean.getNSplitter()];
}
}
public void run() {
// 獲得文件長度
// 分割文件
// 實例FileSplitterFetch
// 啓動FileSplitterFetch線程
// 等待子線程返回
try {
if (bFirst) {
nFileLength = getFileSize();
if (nFileLength == -1) {
System.err.println("File Length is not known!");
} else if (nFileLength == -2) {
System.err.println("File is not access!");
} else {
for (int i = 0; i < nStartPos.length; i++) {
nStartPos[i] = (long) (i * (nFileLength / nStartPos.length));
}
for (int i = 0; i < nEndPos.length - 1; i++) {
nEndPos[i] = nStartPos[i + 1];
}
nEndPos[nEndPos.length - 1] = nFileLength;
}
}
// 啓動子線程
fileSplitterFetch = new FileSplitterFetch[nStartPos.length];
for (int i = 0; i < nStartPos.length; i++) {
fileSplitterFetch[i] = new FileSplitterFetch(siteInfoBean.getSSiteURL(),
siteInfoBean.getSFilePath() + File.separator + siteInfoBean.getSFileName(), nStartPos[i],
nEndPos[i], i);
Utility.log("Thread " + i + " , nStartPos = " + nStartPos[i] + ", nEndPos = " + nEndPos[i]);
fileSplitterFetch[i].start();
}
// fileSplitterFetch[nPos.length-1] = new
// FileSplitterFetch(siteInfoBean.getSSiteURL(),
// siteInfoBean.getSFilePath() + File.separator +
// siteInfoBean.getSFileName(),nPos[nPos.length-1],nFileLength,nPos.length-1);
// Utility.log("Thread " + (nPos.length-1) + " , nStartPos = " +
// nPos[nPos.length-1] + ", nEndPos = " + nFileLength);
// fileSplitterFetch[nPos.length-1].start();
// 等待子線程結束
// int count = 0;
// 是否結束while循環
boolean breakWhile = false;
while (!bStop) {
write_nPos();
Utility.sleep(500);
breakWhile = true;
for (int i = 0; i < nStartPos.length; i++) {
if (!fileSplitterFetch[i].bDownOver) {
breakWhile = false;
break;
}
}
if (breakWhile)
break;
// count++;
// if(count>4)
// siteStop();
}
System.err.println("文件下載結束!");
} catch (Exception e) {
e.printStackTrace();
}
} // 獲得文件長度
public long getFileSize() {
int nFileLength = -1;
try {
URL url = new URL(siteInfoBean.getSSiteURL());
HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
httpConnection.setRequestProperty("User-Agent", "NetFox");
int responseCode = httpConnection.getResponseCode();
if (responseCode >= 400) {
processErrorCode(responseCode);
return -2; // -2 represent access is error
}
String sHeader;
for (int i = 1;; i++) {
// DataInputStream in = new
// DataInputStream(httpConnection.getInputStream ());
// Utility.log(in.readLine());
sHeader = httpConnection.getHeaderFieldKey(i);
if (sHeader != null) {
if (sHeader.equals("Content-Length")) {
nFileLength = Integer.parseInt(httpConnection.getHeaderField(sHeader));
break;
}
} else
break;
}
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
Utility.log(nFileLength);
return nFileLength;
}
// 保存下載信息(文件指針位置)
private void write_nPos() {
try {
output = new DataOutputStream(new FileOutputStream(tmpFile));
output.writeInt(nStartPos.length);
for (int i = 0; i < nStartPos.length; i++) {
// output.writeLong(nPos[i]);
output.writeLong(fileSplitterFetch[i].nStartPos);
output.writeLong(fileSplitterFetch[i].nEndPos);
}
output.close();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
// 讀取保存的下載信息(文件指針位置)
private void read_nPos() {
try {
DataInputStream input = new DataInputStream(new FileInputStream(tmpFile));
int nCount = input.readInt();
nStartPos = new long[nCount];
nEndPos = new long[nCount];
for (int i = 0; i < nStartPos.length; i++) {
nStartPos[i] = input.readLong();
nEndPos[i] = input.readLong();
}
input.close();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
private void processErrorCode(int nErrorCode) {
System.err.println("Error Code : " + nErrorCode);
}
// 停止文件下載
public void siteStop() {
bStop = true;
for (int i = 0; i < nStartPos.length; i++)
fileSplitterFetch[i].splitterStop();
}
}
FileSplitterFetch.java
package com.bocom.sb.http.file;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
/**
*
* 負責部分文件的抓取
* @author xiaoming
* @since 2017-8-29
*/
public class FileSplitterFetch extends Thread {
// File URL
String sURL;
// File Snippet Start Position
long nStartPos;
// File Snippet End Position
long nEndPos;
// Thread's ID
int nThreadID;
// Downing is over
boolean bDownOver = false;
// Stop identical
boolean bStop = false;
// File Access interface
FileAccess fileAccessI = null;
public FileSplitterFetch(String sURL, String sName, long nStart, long nEnd, int id) throws IOException {
this.sURL = sURL;
this.nStartPos = nStart;
this.nEndPos = nEnd;
nThreadID = id;
fileAccessI = new FileAccess(sName, nStartPos);
}
public void run() {
while (nStartPos < nEndPos && !bStop) {
try {
URL url = new URL(sURL);
HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
httpConnection.setRequestProperty("User-Agent", "NetFox");
String sProperty = "bytes=" + nStartPos + "-";
httpConnection.setRequestProperty("RANGE", sProperty);
Utility.log(sProperty);
InputStream input = httpConnection.getInputStream();
// logResponseHead(httpConnection);
byte[] b = new byte[1024];
int nRead;
while ((nRead = input.read(b, 0, 1024)) > 0 && nStartPos < nEndPos && !bStop) {
nStartPos += fileAccessI.write(b, 0, nRead);
// if(nThreadID == 1)
// Utility.log("nStartPos = " + nStartPos + ", nEndPos = " +
// nEndPos);
}
Utility.log("Thread " + nThreadID + " is over!");
bDownOver = true;
// nPos = fileAccessI.write (b,0,nRead);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 打印迴應的頭信息
public void logResponseHead(HttpURLConnection con) {
for (int i = 1;; i++) {
String header = con.getHeaderFieldKey(i);
if (header != null)
// responseHeaders.put(header,httpConnection.getHeaderField(header));
Utility.log(header + " : " + con.getHeaderField(header));
else
break;
}
}
public void splitterStop() {
bStop = true;
}
}
FileAccess.java
package com.bocom.sb.http.file;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.Serializable;
/**
*
* 負責文件的存儲
* @author xiaoming
* @since 2017-8-29
*
*/
public class FileAccess implements Serializable {
private static final long serialVersionUID = -6335788938054788024L;
RandomAccessFile oSavedFile;
long nPos;
public FileAccess() throws IOException {
this("", 0);
}
public FileAccess(String sName, long nPos) throws IOException {
oSavedFile = new RandomAccessFile(sName, "rw");
this.nPos = nPos;
oSavedFile.seek(nPos);
}
public synchronized int write(byte[] b, int nStart, int nLen) {
int n = -1;
try {
oSavedFile.write(b, nStart, nLen);
n = nLen;
} catch (IOException e) {
e.printStackTrace();
}
return n;
}
}
SiteInfoBean.java
package com.bocom.sb.http.file;
/**
*
* 要抓取的文件的信息,如文件保存的目錄,名字,抓取文件的URL等
* @author xiaoming
* @since 2017-8-29
*/
public class SiteInfoBean
{
// Site's URL
private String sSiteURL;
// Saved File's Path
private String sFilePath;
// Saved File's Name
private String sFileName;
// Count of Splited Downloading File
private int nSplitter;
public SiteInfoBean()
{
// default value of nSplitter is 5
this("", "", "", 5);
}
public SiteInfoBean(String sURL, String sPath, String sName, int nSpiltter)
{
sSiteURL = sURL;
sFilePath = sPath;
sFileName = sName;
this.nSplitter = nSpiltter;
}
public String getSSiteURL()
{
return sSiteURL;
}
public void setSSiteURL(String value)
{
sSiteURL = value;
}
public String getSFilePath()
{
return sFilePath;
}
public void setSFilePath(String value)
{
sFilePath = value;
}
public String getSFileName()
{
return sFileName;
}
public void setSFileName(String value)
{
sFileName = value;
}
public int getNSplitter()
{
return nSplitter;
}
public void setNSplitter(int nCount)
{
nSplitter = nCount;
}
}
Utility.java
package com.bocom.sb.http.file;
/**
*
* 工具類,日誌處理,線程等待
* @author xiaoming
* @since 2017-8-29
*/
public class Utility {
public Utility() {
}
public static void sleep(int nSecond) {
try {
Thread.sleep(nSecond);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void log(String sMsg) {
System.err.println(sMsg);
}
public static void log(int sMsg) {
System.err.println(sMsg);
}
}
TestMethod.java
package com.bocom.sb.http.file;
/**
*
* 測試類
* @since 2017-8-29
* @author xiaoming
*
*/
public class TestMethod {
public TestMethod() {
try {
//從http://localhost:8080/abc.exe取文件,存到D:\\temp,命名爲abc.exe,開啓5個線程
SiteInfoBean bean = new SiteInfoBean("http://localhost:8080/file/abc.exe", "D:\\temp",
"abc.exe", 5);
SiteFileFetch fileFetch = new SiteFileFetch(bean);
fileFetch.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
Spring boot 設置一個對外接口,調用斷點續傳,或在TestMethod下直接寫個main方法進行測試。
@RequestMapping("download")
public void download(){
new TestMethod();
}
abc.exe存放在 src/main/resources/static/file/ 下面。
訪問方式:
http://127.0.0.1:8080/spring/download
問題:
項目啓動後,下載時報錯:Java sockets - java.net.ConnectException: Connection refused: connect
在Stack Overflow上搜到一個非常精簡有用的答案:
Have a look at the answers to: java.net.ConnectException: Connection refused
My first suspicion however would be a firewall issue.....
解決方法:關了你的防火牆...
到此,斷點續傳已實現完成,在測試時準備一個大一些的文件,在還沒有下載完成的時候,停掉服務器,這時會發現文件僅下載了一部分,然後啓動服務器,再次下載,程序會從上次中斷的位置繼續下載,等待下載完成,打開abc.exe,能夠正常打開,傳輸成功!