最近在家要下載一個比較大的鏡像文件,由於網絡太差,每次都是下載到中間就停了,文件就下載失敗。我用的是 chrome 自帶的下載功能,就這樣重試了4-5 次,每次都以失敗了結。可恨的是,有時候下載到 80% - 90%,失敗了還要從頭重來。chrome
後來我就想到 wget 命令行,上網搜了一下,發現 wget 是自帶斷點下載功能的。也就是說,若是中間鏈接斷開,能夠直接從已經下載好的地方繼續開始。而後,直接使用 wget 搞定了任務。服務器
若是以前已經有了一個下載部分的文件,也可使用 wget 的 -c 命令,來繼續下載。 能夠參考 nixCraft 上一篇文章 Wget: Resume Broken Downloadmarkdown
按說完事以後,我也就該放心啦。文件也下載完了,你還有啥可惦記的呢?但是我內心一直有個疙瘩:HTTP 協議不是無狀態的嗎?重發的請求,是怎麼告訴服務器端,我只要某一段數據的呢?應用程序有事怎麼把不一樣的數據接起來的呢?網絡
而後,我就繼續搜索,發現 HTTP 斷點續傳祕密所在:Range
頭部字段。工具
斷點下載,通俗一點說,就是客戶端告訴服務器端:我已經下載了前面長度爲 n 的內容, 請從 n+1 個數據給我傳過來,不用從 0 開始從新傳。url
在 HTTP 1.1
版本中,有個對應的實體頭部作這件事情,那就是 Range
。Range
指定要請求實體(entity)的範圍,和多數的程序規範同樣,它也是從 0 開始計數的。好比前面已經下載了 500 bytes 的內容,要請求 500 bytes 之後的內容,只要在 HTTP 請求的頭部加上 Range: bytes=500-
就能夠啦。spa
Range
有幾種不一樣的方式來限定範圍:命令行
500-900
:指定開始到結束這一段的長度,記住 Range 是從 0 計數 的,因此這個是要求服務器從 501 字節開始傳,一直到 901 字節結束。這個通常用來特別大的文件分片傳輸,好比視頻。500-
:從 501 bytes 以後開始傳,一直傳到最後。這個就比較適合用於斷點續傳,客戶端已經下載了 500 字節的內容,麻煩把後面的傳過來-500
:這個須要注意啦,若是範圍沒有指定開始位置,就是要服務器端傳遞倒數 500 字節的內容。而不是從 0 開始的 500 字節。。500-900, 1000-2000
:也能夠同時指定多個範圍的內容,這種狀況不是很常見客戶端發出這樣的請求,也要服務器端響應和支持這個功能。前面也提到過,這個實體頭部是 HTTP 1.1 版本才添加的,因此有些 HTTP 服務端使用比較老的版本可能不支持。code
若是服務端支持的話,那麼這個響應裏也要帶一個 content-range 的字段。好比客戶端使用的是 Range: bytes=1024-,那麼對應的服務端返回頭部會包括相似 content-range: bytes 1024-439714/439715 的內容。這個意思就是說,我知道啦,我會從只傳遞 1024 - 439714 範圍的內容,而整個文件的大小是 439715 字節。並且,這個時候響應的 status code 必定是 206,關於 206 的解釋能夠查看 w3 官網上的說明。 。 若是真是這樣也就皆大歡喜啦,可是可能出現兩種異常狀況:orm
服務端返回的響應頭部沒有 content-range
字段,代表服務端忽略了請求裏 range
字段。多是由於服務端不支持這個功能,那麼也沒辦法,只能從頭開始下載。 注:目前大多數 HTTP 都支持這個功能,因此這個狀況也不多見
服務端返回的響應頭部包含了 content-range
,但仍是從 0 開始從新下載的。這個就是服務端實現比較嚴格,考慮到了這個文件可能會被修改的狀況。
什麼意思?考慮一下這個狀況:昨天我下載某個文件,下了一半,今天使用 wget -c
接着下。但問題是:中間某個時間點,這個文件被更新了!URL 沒變,可是文件內容變啦。若是我接着日後下,而後把以前的內容和新下的內容和在一塊兒,極可能就不是一個正常的文件啦,沒有辦法打開。這個狀況比從頭開始下載還要糟糕:辛辛苦苦下載完,結果是不能用的。
若是服務端要下載的文件,或者其餘資源是可變的。就要有一個辦法來標識文件或者資源的惟一性,就是說我以前下載的究竟是哪一個版本的,和如今版本是否是一致的。HTTP 協議裏有兩個辦法能夠來作這件事,固然也就是兩個頭部字段:ETag
和 Last-Modified
,都定義在 RFC2616。
ETag
: 你能夠把 ETag 比做文件的 MD5,用來惟一標識一個文件,它是一串字符Last-Modified
:望名思意,就是這個文件上次修改的時間若是服務端比較嚴格,會檢查你此次要下載的文件和上次下載的有沒有變化,那麼在第一次下載的時候,它必定會提供 ETag
或者 Last-Modified
兩個字段中的至少一個。那麼斷點下載的請求,只要把任意一個放到 If-Range 字段裏傳過去就能夠啦(If-Range
只能和 Range
一塊兒使用,不然會被服務端忽略)。
若是兩次下載的是同一個文件,就會返回 206,從後開始續傳;不然就會返回 200,表示文件變了,要重頭開始下載。
若是客戶端發送的 range 範圍有錯誤,會返回 416,而且 Content-Range
字段形如 bytes */439715
,表示提供的範圍有誤,文件總大小是 439715
。
知道上面的原理,實現一個本身的斷點下載程序其實很簡單。
這段代碼實現的是 wget -c 的功能,能夠實現的效果是:
若是要使用 If-Range
就要把第一次請求的數據存起來,爲了簡單起見,咱們不會實現這個功能。
import os
import sys
import requests
def file_size(filename):
return os.stat(filename).st_size
def download(url, chunk_size=65535):
downloaded = 0 # How many data already downloaded.
filename = url.split('/')[-1] # Use the last part of url as filename
if os.path.isfile(filename):
downloaded = file_size(filename)
print("File already exists. Send resume request after {} bytes".format(
downloaded))
# Update request header to add `Range`
headers = {}
if downloaded:
headers['Range'] = 'bytes={}-'.format(downloaded)
res = requests.get(url, headers=headers, stream=True, timeout=15)
mode = 'w+'
content_len = int(res.headers.get('content-length'))
print("{} bytes to download.".format(content_len))
# Check if server supports range feature, and works as expected.
if res.status_code == 206:
# Content range is in format `bytes 327675-43968289/43968290`, check
# if it starts from where we requested.
content_range = res.headers.get('content-range')
# If file is already downloaded, it will reutrn `bytes */43968290`.
if content_range and \
int(content_range.split(' ')[-1].split('-')[0]) == downloaded:
mode = 'a+'
if res.status_code == 416:
print("File download already complete.")
return
with open(filename, mode) as fd:
for chunk in res.iter_content(chunk_size):
fd.write(chunk)
downloaded += len(chunk)
print("{} bytes downloaded.".format(downloaded))
print("Download complete.")
if __name__ == '__main__':
url = 'http://dldir1.qq.com/qqfile/QQforMac/QQ_V4.0.4.dmg'
url = sys.argv[1] if len(sys.argv) == 2 else url
download(url)
複製代碼
咱們使用了 requests 庫來實現 HTTP 請求,代碼實現起來也比較簡單,須要說明的地方都已經添加了註釋。
稍微修改一下,就能封裝成一個還不錯的下載工具。