看到一篇多線程下載的文章,這裏把本身的理解寫一篇多線程下載的文章。python
咱們訪問http://192.168.10.7/a.jpg時是get請求,response的head包含Content-Length: 37694緩存
這個就是a.jpg文件的大小多線程
抓包的話,server端是發送多個數據包(PDU)和一個文件信息,而後拼裝成了a.jpg圖片:app
,部分截圖。測試
若是我用requests.head("http://192.168.10.7/a.jpg")時,server端只返回文件信息,而不會發送文件數據。編碼
response = requests.head(self.url) print(response.headers) # {'Keep-Alive': 'timeout=5, max=100', 'Accept-Ranges': 'bytes', 'Date': 'Sat, 18 Feb 2017 02:56:08 GMT', 'ETag': '"933e-548c4b0beff53"', 'Content-Type': 'image/jpeg', 'Content-Length': '37694', 'Last-Modified': 'S at, 18 Feb 2017 02:21:39 GMT', 'Connection': 'Keep-Alive', 'Server': 'Apache/2.4.18 (Ubuntu)'}
文件a.jpg大小是37964字節url
保存a.jpg文件後查看文件大小也是spa
好了,咱們知道文件大小了的話,那如何多線程下載了?線程
假如咱們用3個線程去下載a.jpg,那麼咱們會用線程1去下載1260x10=12600字節,線程2下載12601-25200字節,以此類推,還不夠就用線程1再去下載。設計
可是get請求不是會直接下載a.jpg文件了?怎麼只獲取一部分文件的數據了?
咱們能夠在get請求的head部分加入「Range: bytes=0-12599」, 先測試下
# res.text 是將get獲取的byte類型數據自動編碼,是str類型, res.content是原始的byte類型數據
# 因此下面是直接write(res.content)
headers = {"Range":"bytes=0-12599"} res = requests.get(self.url,headers=headers) # res.text 是將get獲取的byte類型數據自動編碼,是str類型, res.content是原始的byte類型數據 # 因此下面是直接write(res.content) with open(self.filename,'wb') as f: f.write(res.content)
而後能夠看到下載獲取的一部分圖片:
咱們再獲取下一部分數據,
headers = {"Range":"bytes=12600-25199"} res = requests.get(self.url,headers=headers) # res.text 是將get獲取的byte類型數據自動編碼,是str類型, res.content是原始的byte類型數據 # 因此下面是直接write(res.content) with open(self.filename,'ab+') as f: print(f.tell()) f.write(res.content)
能夠看到文件:
咱們知道:
r或rt 默認模式,文本模式讀 rb 二進制文件 w或wt 文本模式寫,打開前文件存儲被清空 wb 二進制寫,文件存儲一樣被清空 a 追加模式,只能寫在文件末尾 a+ 可讀寫模式,寫只能寫在文件末尾 w+ 可讀寫,與a+的區別是要清空文件內容 r+ 可讀寫,與a+的區別是能夠寫到文件任何位置
若是是多線程的而下載的話,咱們用open('file','rb+'),我先用這種模式繼續上面下載文件,上面下載到了25199字節,
那此次我從26000開始下載,f.seek(26000)後開始保存下載的文件,看文件是否能保存,看到的文件是否會中間出現空白:
headers = {"Range":"bytes=26000-37694"} res = requests.get(self.url,headers=headers) # res.text 是將get獲取的byte類型數據自動編碼,是str類型, res.content是原始的byte類型數據 # 因此下面是直接write(res.content) with open(self.filename,'rb+') as f: f.seek(26000) f.write(res.content)
下載後的文件:
這個,可能圖片顯示可能跟咱們想象的不同,可是rb+確定是能夠從任意位置讀寫的。
還介紹一個知識點,可能在本身測試的時候用的到,就是:
f.truncate(n): 從文件的首行首字符開始截斷,截斷文件爲n個字符;無n表示從當前位置起截斷;截斷以後n後面的全部字符被刪除。
好了,如今咱們開始使用多線程下載文件:
設計思路是:
一、每一個線程下載一部分數據
二、每一個線程用rb+模式打開文件
三、每一個線程下載數據後,用f.seek()到相應的位置,而後再寫數據。
直接f=open(),再多線程f.write()時會出現文件寫錯誤。
咱們能夠用os.dup()複製文件符合os.fsopen(fd,mode,buffer)來打開處理文件。
os.dup()和os.fdopen()的好處我的理解是os.dup()複製文件句柄,os.fdopen()先寫緩存,具體官方文檔還有待查證。
代碼:
版本 python3,
pip install requests
下面代碼能夠拿來直接跑
#! -coding:utf8 -*- import threading,sys import requests import time import os class MulThreadDownload(threading.Thread): def __init__(self,url,startpos,endpos,f): super(MulThreadDownload,self).__init__() self.url = url self.startpos = startpos self.endpos = endpos self.fd = f def download(self): print("start thread:%s at %s" % (self.getName(), time.time())) headers = {"Range":"bytes=%s-%s"%(self.startpos,self.endpos)} res = requests.get(self.url,headers=headers) # res.text 是將get獲取的byte類型數據自動編碼,是str類型, res.content是原始的byte類型數據 # 因此下面是直接write(res.content) self.fd.seek(self.startpos) self.fd.write(res.content) print("stop thread:%s at %s" % (self.getName(), time.time())) # f.close() def run(self): self.download() if __name__ == "__main__": url = sys.argv[1] #獲取文件的大小和文件名 filename = url.split('/')[-1] filesize = int(requests.head(url).headers['Content-Length']) print("%s filesize:%s"%(filename,filesize)) #線程數 threadnum = 3 #信號量,同時只容許3個線程運行 threading.BoundedSemaphore(threadnum) # 默認3線程如今,也能夠經過傳參的方式設置線程數 step = filesize // threadnum mtd_list = [] start = 0 end = -1 # 請空並生成文件 tempf = open(filename,'w') tempf.close() # rb+ ,二進制打開,可任意位置讀寫 with open(filename,'rb+') as f: fileno = f.fileno() # 若是文件大小爲11字節,那就是獲取文件0-10的位置的數據。若是end = 10,說明數據已經獲取完了。 while end < filesize -1: start = end +1 end = start + step -1 if end > filesize: end = filesize # print("start:%s, end:%s"%(start,end)) # 複製文件句柄 dup = os.dup(fileno) # print(dup) # 打開文件 fd = os.fdopen(dup,'rb+',-1) # print(fd) t = MulThreadDownload(url,start,end,fd) t.start() mtd_list.append(t) for i in mtd_list: i.join()
執行結果:
python multiprocess_download.py http://192.168.10.7/of.tar.gz of.tar.gz filesize:36578022 start thread:Thread-1 at 1487405833.7353075 start thread:Thread-2 at 1487405833.736311 start thread:Thread-3 at 1487405833.7378094 stop thread:Thread-1 at 1487405836.9561603 stop thread:Thread-3 at 1487405837.0016065 stop thread:Thread-2 at 1487405837.0116146
屢次測試,下載後的文件均可以正常打開。
若是有多個站點有of.tar.gz文件,那更能夠體現多線程下載的體驗。
根據上面的理論,咱們應該能夠作一個相似p2p的下載,好比10臺機器,每臺啓動一個agent,每一個agent給server上報本身目錄下的文件信息,當有一個agent有下載文件時,會去server查詢哪些agent有這個文件,而後計算去哪些agent下載哪段數據。