相信大家都遇過,下載檔案最後剩一點點時出錯失敗,然後要重新下載整個檔案,體驗一定非常差吧,不過如果程式有支援檔案續傳功能,可以從斷線的地方繼續,就不用浪費時間重新下載,今天要和大家介紹檔案續傳的方式。
檔案續傳需要的 Request Header:
Range
: 告知伺服器要下載的檔案範圍,等號前為範圍的單位,通常是bytes,等號後為範圍的開始到結束,範圍從 0 開始計算,四種格式如下:Range: bytes=500-1000
: 從 500 byte 開始,到 1000 byte 結束。Range: bytes=500-
: 從 500 byte 開始,到檔案的最後結束。Range: bytes=-500
: 傳回倒數 500 個 byte 的內容,這裡和上面兩種比較不同。Range: bytes=500-1000, 1500-2000
: 可以指定多個範圍。
If-Range
: 確保續傳下載的過程中,這次下載的部分和上次下載的,這之間檔案沒有被變更過。
為非必要可以不加,但如果有 If-Range
就一定要配合 Range
使用,否則忽略 If-Range
。
可以使用 Last-Modified
時間驗證或 ETag
標記驗證,兩者選其一,但不可以兩者同時使用。If-Range: Wed, 18 Oct 2017 07:30:00 GMT
檔案續傳需要的 Reponse Header:
Accept-Ranges
: 請求範圍的單位。 Accept-Ranges: bytes
Content-Length
: 請求的內容長度,不是整個檔案的大小。 Content-Length: 1000
Content-Range
: 請求範圍在整個檔案中的位置。Content-Range: bytes 500-1000/3000
: 請求範圍從 500 byte 開始,到 1000 byte 結束,整個檔案大小 3000 byte。Last-Modified
: 檔案的最後修改時間,使用國際標準時間 GMT。Last-Modified: Wed, 18 Oct 2017 07:30:00 GMT
ETag
: 檔案的唯一標記,用來驗證檔案是否變更,類似 MD5 的作用。ETag: "33a64df5514abcd55bsb2a148795d9f6b989d4"
檔案續傳流程如下:
Range
返回狀態碼 200
,一般檔案下載。Range
從斷掉的地方繼續,返回狀態碼 206
,檔案續傳下載。If-Range
驗證發現檔案有變動,返回狀態碼 200
,重新下載檔案。Range
範圍錯誤,返回狀態碼 416
。程式碼:
public void ProcessRequest(HttpContext context)
{
var index = context.Request.Params["index"];
var fileName = "test.txt";
var filePath = context.Server.MapPath("~/File/" + fileName);
//檔案最後修改時間,格式 RFC1123
var lastModified = File.GetLastWriteTime(filePath).ToString("r");
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
var bufferSize = 102400; //緩衝區大小 100KB
var buffer = new byte[bufferSize]; //緩衝區
var outputLength = fs.Length; //檔案大小
var readLength = 0; //每次讀取大小
var sIndex = (long)0; //開始讀取位置
var eIndex = outputLength - 1; //結束讀取位置
var isPartialContent = false; //是否為續傳
if (context.Request.Headers["Range"] != null)
{
//判斷檔案最後修改時間是否和 If-Range 相同,相同代表檔案沒有被修改過
if (context.Request.Headers["If-Range"] == null ||
context.Request.Headers["If-Range"] == lastModified)
{
//取得要續傳的範圍
var range = context.Request.Headers["Range"];
var sRange = "";
var eRange = "";
//驗證續傳的範圍格式是否正確
var regex = new Regex(@"^[\s]*bytes=(([0-9]*)-([0-9]*))$");
if (regex.IsMatch(range))
{
var match = regex.Match(range);
sRange = match.Groups[2].Value;
eRange = match.Groups[3].Value;
}
//50-100 : 從第 50 個 byte 開始到第 100 個 byte
if (!string.IsNullOrEmpty(sRange) &&
!string.IsNullOrEmpty(eRange))
{
sIndex = long.Parse(sRange);
eIndex = long.Parse(eRange);
}
//50- : 從第 50 個 byte 開始到最後
if (!string.IsNullOrEmpty(sRange) &&
string.IsNullOrEmpty(eRange))
{
sIndex = long.Parse(sRange);
}
//-50 : 倒數 50 個 byte
if (string.IsNullOrEmpty(sRange) && !
string.IsNullOrEmpty(eRange))
{
sIndex = eIndex + 1 - long.Parse(sRange);
}
if (eIndex < 0 || sIndex > outputLength - 1 ||
sIndex > eIndex)
{
//Range 範圍不符
context.Response.StatusCode = 416;
return;
}
//是否為檔案續傳
isPartialContent = true;
}
}
context.Response.Clear();
context.Response.AddHeader("Accept-Ranges", "bytes");
context.Response.AppendHeader("Last-Modified", lastModified);
context.Response.AddHeader(
"Content-Length", $"{eIndex - sIndex + 1}");
context.Response.AddHeader(
"Content-Range", $" bytes {sIndex}-{eIndex}/{outputLength}");
context.Response.ContentType = "application/octet-stream";
context.Response.AddHeader(
"content-disposition", "attachment; filename=" + fileName);
if (isPartialContent) context.Response.StatusCode = 206;
try
{
var currentIndex = sIndex;
fs.Seek(currentIndex, SeekOrigin.Begin);
while (currentIndex <= eIndex &&
context.Response.IsClientConnected)
{
readLength =
(int)Math.Min(eIndex - currentIndex + 1, bufferSize);
fs.Read(buffer, 0, readLength);
context.Response.OutputStream.Write(buffer, 0, readLength);
context.Response.Flush();
currentIndex = currentIndex + readLength;
}
}
catch (Exception)
{
//傳輸過程中如果客戶端關閉連接,會拋出例外不處理
}
context.Response.End();
}
}
結果:
使用續傳軟體(IDM)的測試結果,支援多點續傳,中斷後也可以恢復下載。
下載過程中,我按了暫停然後去修改檔案,再恢復下載時有被 IDM 判斷出來檔案已經被變動,要求重新下載。
結語:
我沒有實作 Range
的第四種格式,指定多個範圍,因為不常用到,而且會增加程式閱讀的困難度,If-Range
的部分,我選用 Last-Modified
驗證,因為不想去弄 MD5 偷懶一下 XD。
參考文章:
在ASP.NET中支持断点续传下载大文件(ZT) (转)
HTTP 断点下载功能实现
HTTP断点续传(分块传输)
MDN Web Docs
相關文章:
[C#] ASP.NET 檔案下載(1) - POST 和 GET 觸發檔案下載
[C#] ASP.NET 檔案下載(2) - 大型檔案下載
[C#] ASP.NET 檔案下載(3) - 檔案續傳