C# 檔案下載之斷點續傳

sparkdev發表於2016-12-07

注意,本文所說的斷點續傳特指 HTTP 協議中的斷點續傳。本文主要聊聊思路和關鍵程式碼,更多細節請參考本文附帶的 demo。

工作原理

HTTP 協議中定義了一些請求/響應頭,通過組合使用這些頭資訊。我們可以在一次 HTTP 請求中只請求一個檔案中的一部分資料。這樣我們就可以把已經下載的資料存起來,下次只用請求剩餘的資料即可,當全部資料都下載到本地後再完成合並工作。

HTTP 協議指出,可以通過 HTTP 請求中的 Range 頭指定請求資料的範圍,Range 頭的使用也很簡單,只要指定下面的格式就可以了:

Range: bytes=500-999

它的意思是,只請求目標檔案的第 500 到第 999 這 500 個位元組。

比如我有一個1000 bytes 大小的檔案需要下載,第一次請求時不用指定 Range 頭,表示下載整個檔案。但在下載完第 499 個位元組後,下載被取消了。那麼在下一次請求下載同一個檔案時,只需要下載第 500 個位元組至第 999 個位元組的資料就可以了。原理看上去很簡單,但我們需要考慮下面幾個問題:

1.    是不是所有的 web 伺服器都支援 Range 頭?
2.    多次請求之間可能會間隔很長的時間,伺服器上的檔案發生了變化怎麼辦?
3.    如何儲存下載的部分資料和相關資訊?
4.    當我們通過位元組操作把一個檔案拼成原始大小後,如何驗證它和原始檔一模一樣?

下面我們就帶著這些問題去探究斷點續傳的一些細節。

檢查伺服器端對斷點續傳的支援

在伺服器響應我們的請求時,會在響應頭中通過 Accept-Ranges 指明是否接受請求一個資源的一部分資料。但這裡似乎有個小小的陷阱,就是不同的伺服器可能返回不同的值來指明自己能夠接受部分資源的請求。貌似比較統一的方法是,當伺服器不支援請求部分資料時,都會返回 Accept-Ranges: none,我們只要判斷這個返回值是不是等於 none 就行了。程式碼如下:

private static bool IsAcceptRanges(WebResponse res)
{
    if (res.Headers["Accept-Ranges"] != null)
    {
        string s = res.Headers["Accept-Ranges"];
        if (s == "none")
        {
            return false;
        }
    }
    return true;
}

檢查伺服器端檔案是否變化

當我們下載了一個檔案的一部分之後,可能馬上就會接著下載,也可能會過一段時間再下載,也可能永遠不會再接著下載了…
這裡的問題是,當下次要接著下載時,如何確定伺服器上的檔案還是當初下載了一半的那個檔案。如果伺服器上的檔案已經更新了,那無論如何都需要重新從頭開始下載。只有在伺服器上的檔案沒有發生變化的情況下,斷點續傳才有意義。
對於這個問題,HTTP 響應頭為我們提供了不同的選擇。ETag 和 Last-Modified 都能完成任務。

先看 ETag:

The ETag response-header field provides the current value of the entity tag for the requested variant. (引自RFC2616 14.19 ETag)
簡單點說 ETag 就是一個標識當前請求內容的字串,當請求的資源發生變化後,對應的 ETag 也會變化。好了,最簡單的辦法是第一次請求時,把響應頭中的 ETag 存下來,下次請求時做比較。程式碼如下:

string newEtag = GetEtag(response);
// tempFileName指已經下載到本地的部分檔案內容
// tempFileInfoName指儲存了Etag內容的臨時檔案
if (File.Exists(tempFileName) && File.Exists(tempFileInfoName))
{
    string oldEtag = File.ReadAllText(tempFileInfoName);
    if (!string.IsNullOrEmpty(oldEtag) && !string.IsNullOrEmpty(newEtag) && newEtag == oldEtag)
    {
    // Etag沒有變化,可以斷點續傳
        resumeDowload = true;
    }
}
else
{
    if (!string.IsNullOrEmpty(newEtag))
    {
        File.WriteAllText(tempFileInfoName, newEtag);
    }
}
private static string GetEtag(WebResponse res)
{
    if (res.Headers["ETag"] != null)
    {
        return res.Headers["ETag"];
    }
    return null;
}

再來看看 Last-Modified:

The Last-Modified entity-header field indicates the date and time at which the origin server believes the variant was last modified. (引自RFC2616 14.29 Last-Modified)
Last-Modified 就是所請求的資源在伺服器上的最後一次修改時間。使用方法和 ETag 大體相同。

個人感覺使用 ETag 和 Last-Modified 中的任何一個都能達到我們的目的。但是你也可以兩個都用,做 double check,誰知道web伺服器的實現是不是嚴格遵循了 HTTP 協議!

儲存中間結果

這裡主要就是用 C# 進行檔案操作。大體思路是如果有未下載完的檔案,就把新下載的位元組新增到檔案的末尾,不再囉嗦,有興趣的同學請直接看 demo 程式碼。

驗證檔案

在斷點續傳的過程中,我們以 byte 為單位下載、合併檔案,如果整個過程中稍有沒有處理好的異常,可能最後得到的檔案就和原始檔不太一樣。因此最好是能夠對下載好的檔案進行一次校驗。可這也是最難、最不容易實現的。因為它需要伺服器端的支援,比如伺服器端在提供一個可下載檔案的同時提供該檔案的 MD5 hash。當然,如果伺服器端也是我們自己建立的,我們就可以去實現它。但我們又怎麼能夠要求現存的 web 伺服器都提供這樣的功能呢!

Demo 下載

相關文章