JAVA 實現 HTTP 斷點續傳及原理

斷點續傳原理:
現在有一個文件需要我們進行下載,當我們下載了一部分的時候,出現情況了,比如:電腦死機、沒電、網絡中斷等等。 對於以上行爲,如果“下載”的行爲無法記錄本次下載的一個進度。那麼,當我們再次下載這個文件也就只能從頭來過。
所以,要實現讓一種斷開的行爲“續”起來的目的,關鍵就在於要有“介質”能夠記錄和讀取行爲出現”中斷”的這個節點的信息。
實際上這就是“斷點續傳”的基礎原理,用大白話說就是:我們要在下載行爲出現中斷的時候,記錄下中斷的位置信息,在新的下載行爲開始的時候,直接從記錄的這個位置開始下載內容,而不再從頭開始。

實現步驟:
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,能夠正常打開,傳輸成功!

發佈了83 篇原創文章 · 獲贊 333 · 訪問量 68萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章