使用 Go 語(yǔ)言開(kāi)發(fā)一個(gè)并發(fā)文件下載器
本文轉(zhuǎn)載自微信公眾號(hào)「Golang來(lái)啦」,作者Seekload。轉(zhuǎn)載本文請(qǐng)聯(lián)系Golang來(lái)啦公眾號(hào)。
今天給大家分享一個(gè)實(shí)戰(zhàn)項(xiàng)目,涉及到的知識(shí)點(diǎn)還挺多,文末也有源碼地址!!
原文如下:
Go 語(yǔ)言是一門(mén)了不起的語(yǔ)言,盡管它非常簡(jiǎn)單,與 Koltin 和 Scala 等其他現(xiàn)代語(yǔ)言相比,它的功能很少,但它具有強(qiáng)大的并發(fā)能力。這篇文章,我們將會(huì)看到使用 Go 語(yǔ)言如何編寫(xiě)一個(gè)完整的并發(fā)文件下載器。完整的代碼在這里[1]。
檢查服務(wù)器是否支持并發(fā)下載
如何之前使用過(guò)類(lèi)似 IDM 的下載工具,你可能會(huì)注意到它支持并發(fā)下載文件。
可以看到下載文件的時(shí)候啟動(dòng)了 8 個(gè)進(jìn)程。
實(shí)現(xiàn)并發(fā)下載,我們必須確保服務(wù)器支持范圍請(qǐng)求。怎么確認(rèn)呢?我們可以發(fā)送 HEAD 請(qǐng)求,如果響應(yīng)頭的 Accept-Ranges 返回的值是 bytes,我們就能確定服務(wù)器支持此功能。
- res, err := http.Head("http://some.domain/some.file")
- if err != nil {
- log.Fatal(err)
- }
- if res.StatusCode == http.StatusOK && res.Header.Get("Accept-Ranges") == "bytes" {
- // Yeh, server supports partial request
- }
如何下載文件的其中一部分
設(shè)想服務(wù)器支持范圍請(qǐng)求,我們知道文件大小是 4000 字節(jié)(文件大小從響應(yīng)頭的 Content-Length 獲取)。要僅下載 2000 到 3000 字節(jié)的文件的一部分,我們可以發(fā)送 HTTP GET 請(qǐng)求,并在 header 頭設(shè)置 Range 參數(shù):
- curl -X GET -H "Range: bytes=2000-3000" -o OUTPUT_FILE http://some.domain/some.file
實(shí)現(xiàn)相同功能的代碼如下:
- req, err := http.NewRequest("GET", "http://some.domain/some.file", nil)
- if err != nil {
- log.Fatal(err)
- }
- rangeStart := 2000
- rangeStop := 3000
- req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", rangeStart, rangeStop))
- // make a request
- res, err := http.DefaultClient.Do(req)
將響應(yīng)保存在文件中
為了支持?jǐn)帱c(diǎn)續(xù)傳功能,我們不會(huì)將請(qǐng)求響應(yīng)保存在內(nèi)存里,而是會(huì)持久化在文件中。舉個(gè)例子,如果我們把并發(fā)級(jí)別設(shè)置成 4,在輸出目錄將會(huì)有 4 個(gè)臨時(shí)文件。下面的代碼,我們只是簡(jiǎn)單地讀取 HTTP 響應(yīng)體并將它寫(xiě)入一個(gè)文件中:
- f, err := os.OpenFile(outputPath, flags, 0644)
- if err != nil {
- log.Fatal(err)
- }
- defer f.Close()
- _, err = io.Copy(f, res.Body)
暫停下載
不知道大家注意到?jīng)]有,上面代碼有個(gè)問(wèn)題,使用時(shí)不支持 CTRL+C 暫停下載。如果下載的文件過(guò)大,或者網(wǎng)絡(luò)慢,下載需要花費(fèi)很長(zhǎng)時(shí)間。因?yàn)?io.Copy 復(fù)制文件時(shí)遇到 EOF 或者發(fā)生錯(cuò)誤才結(jié)束。為了解決這個(gè)問(wèn)題,我們使用 io.CopyN 和 cancel channel 組合:
- // copy to output file
- for {
- select {
- case <- context.Done():
- // user canceled the download
- return
- default:
- _, err = io.CopyN(f, res.Body, BUFFER_SIZE))
- if err != nil {
- if err == io.EOF {
- return
- } else {
- log.Fatal(err)
- }
- }
- }
- }
其他功能參見(jiàn)完整源代碼
這篇文章只提到了代碼中最重要的部分,但是通過(guò)閱讀代碼你可以了解其他功能是怎么實(shí)現(xiàn)的,比如:進(jìn)度條的工作方式、如何使用 sync 包實(shí)現(xiàn)部分下載的同步、如何合并臨時(shí)文件以及如何實(shí)現(xiàn)恢復(fù)功能等。所以可以通過(guò)閱讀倉(cāng)庫(kù)代碼[2]獲取更多信息。
參考資料
[1]這里: https://github.com/mostafa-asg/go-dl
[2]倉(cāng)庫(kù)代碼: https://github.com/mostafa-asg/go-dl
via:
https://returnfn.com/lets-build-a-concurrent-file-downloader-in-go
作者:Mostafa Asgari