這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
大家都用過迅雷等下載工具,特點就是支援並發下載,斷點續傳。我們這裡不介紹它,這個比較複雜了,逼人也不懂。本文只介紹狹義上的簡易的斷點續傳和狹義上的多線程下載。跟之前一樣,旨在研究原理,實際生活中基本沒啥用,實測下來多線程下載比單線程下載還慢。。。太丟人了。
主要講三個方面,如何HTTP的並發下載、通過Golang進行多協程開發、如何斷點續傳。
HTTP的並發下載
想要並發下載,就是把下載內容分塊,然後並行下載這些塊。這就要求伺服器能夠支援分塊擷取資料。大迅雷、電驢這種都有自己的協議,thunder://
這種,我們只研究原理,就說說HTTP協議對於並發的支援。
|
HTTP頭 |
|
對應值 |
|
含義 |
|
|
Content-Length |
|
14247 |
|
HTTP響應的Body大小,下載的時候,Body就是檔案,也可以認為是檔案大小,單位是位元 |
|
|
Content-Disposition |
|
inline; filename=”bryce.jpg” |
|
是MIME協議的擴充,MIME協議指示MIME使用者代理程式如何顯示附加的檔案。當瀏覽器接收到頭時,它會啟用檔案下載。這裡還包含了檔案名稱 |
|
|
Accept-Ranges |
|
bytes |
|
允許用戶端以bytes的形式擷取檔案 |
|
|
Range |
|
bytes=0-511 |
|
分塊擷取資料,這裡表示擷取第0到第511的資料,共512位元組 |
|
如果要下載一個檔案,想知道這些檔案的資訊,例如檔案名稱、檔案大小、是否支援並發下載、檔案類型都可以從響應的頭裡面擷取。如何在下載前獲得到這些內容而不是下載中擷取,可以用HTTP提供的HEAD方法。HEAD方法只響應HTTP的頭部分,不包含Body部分。
req, err := http.NewRequest("HEAD", get.Url, nil)resp, err := get.GetClient.Do(req)
擷取檔案類型、檔案名稱等參數。HTTP從Url到Head在到Body,你都可以認為是字串,也確實是字串,但是解析的時候不要自己以字串的方式處理,要不噁心死你。Url的解析大Golang有net/url
包支援,MIME有mime
包支援,這都是原生包,別的語言必然也支援。
get.ContentLength = int(resp.ContentLength)get.MediaType, get.MediaParams, _ = mime.ParseMediaType(get.Header.Get("Content-Disposition"))log.Printf("Get %s MediaType:%s, Filename:%s, Length %d.\n", get.Url, get.MediaType, get.MediaParams["filename"], get.ContentLength)
輸出
2015/07/02 09:56:47 Get http://7b1h1l.com1.z0.glb.clouddn.com/bryce.jpg MediaType:inline, Filename:bryce.jpg, Length 14247.
如果回應標頭裡面還包含了Accept-Ranges
,就說明伺服器支援分塊擷取:
if get.Header.Get("Accept-Ranges") != "" {log.Printf("Server %s support Range by %s.\n", get.Header.Get("Server"), get.Header.Get("Accept-Ranges"))} else {log.Printf("Server %s doesn't support Range.\n", get.Header.Get("Server"))}
分模組下載,建立N個臨時檔案,我的命名規則是加個分塊區間的尾碼,例如bryce.jpg.0-512,這樣可以省掉一個設定檔(主要是我懶的寫)。將下載好的塊存入臨時檔案裡面,最後都下載完之後統一存入最終的檔案裡面。分塊下載加個Range
頭就可以了。
range_i := fmt.Sprintf("%d-%d", get.DownloadRange[i][0], get.DownloadRange[i][1])log.Printf("Download #%d bytes %s.\n", i, range_i)defer get.TempFiles[i].Close()req, err := http.NewRequest("GET", get.Url, nil)req.Header.Set("Range", "bytes="+range_i)resp, err := get.GetClient.Do(req)defer resp.Body.Close()
最後將下載好的保持到檔案裡。這裡是等這個塊都下載完之後再寫入硬碟,下載完之後都是保持在記憶體裡面。
cnt, err := io.Copy(get.TempFiles[i], resp.Body)
多線程開發
並發下載的時候,要啟動N個協程,主線程這時需要阻塞,等待這N個協程下載完畢。先開始想用channel
自己寫,不是特別會。。。用sync
包輔助實現,它支援WaitGroup
,正好可以解決我這裡的問題。
在主線程裡面啟動N個協程,Add
方法可以理解成增加一個任務,任務計數器加一;Wait
方法用於阻塞,指導所有任務完成。
for i, _ := range get.DownloadRange {get.WG.Add(1)go get.Download(i)}get.WG.Wait()
在下載協程增加Done
函數,協程結束之後通知任務完成,任務計數器減一。
defer get.WG.Done()
斷點續傳
這塊最簡單,如果任務下載暫停了,就是傳輸的內容不足。第一步建立臨時檔案的檔案名稱尾碼有派上用場了,讀取到塊應有的大小之後,檢查塊實際大小。通過檔案的os.FileInfo
就能擷取到檔案相關屬性資訊。這樣再下載的時候就增加一個位移量,跳過已經下載好的內容。
for i := 0; i < len(get.DownloadRange); i++ {range_i := fmt.Sprintf("%d-%d", get.DownloadRange[i][0], get.DownloadRange[i][1])temp_file, err := os.OpenFile(get.FilePath+"."+range_i, os.O_RDONLY|os.O_APPEND, 0)if err != nil {temp_file, _ = os.Create(get.FilePath + "." + range_i)} else {fi, err := temp_file.Stat()if err == nil {get.DownloadRange[i][0] += int(fi.Size())}}get.TempFiles = append(get.TempFiles, temp_file)}
大概簡單的原理就是這些,前面說了,比項目無法用於實際用途,原因如下:
- 需要有一個線程池來並發下載,目前的設計在下載大檔案時會導致並發數過大。已經完成下載的線程還能繼續下載未完成的塊,這個又涉及到下任務的動態分配和動態拆分;
- 並發下載時的塊大小,這個值也很講究,一般4096和位元組是一個記憶體塊單位,以此數位倍數下載比較節約記憶體,目前我這裡的塊大小是根據並發數計算的,比較水;
- 前面提到了,這是狹義的多線程下載,前提是伺服器必須支援Range,否則還是無法並發擷取資料,實際在做的時候最起碼是會有幾台下載完的伺服器,這樣自己的伺服器就能開發支援分塊取資料的協議來支援並發下載,市面上的也都是這麼幹;
- 還有下載進度條,就是類似於
wget
命令在控制台展示進度條和下載速度的日誌,這個不太會寫,查了查,據說可以通過fmt.Println("abc\rcde")
實現,\r
表示斷行符號符,可以回到行首,我沒試過,大家可以試試。
本文所涉及到的完整源碼請參考。
參考文獻
- http協議 檔案下載原理及多線程斷點續傳 - zhuhuiby
- Package sync - The Go Programming Language
- How to update command line output? - stackoverflow
原文連結:Golang實現多線程並發下載,轉載請註明來源!