就是下載文件時,沒必要重頭開始下載,而是從指定的位置繼續下載,這樣的功能就作斷點續傳下載。斷點續傳的理解能夠分爲兩部分:一部分是斷點,一部分是續傳下載。斷點的由來是在下載過程當中,將一個下載文件分紅了多個部分,同時進行多個部分一塊兒的下載,當某個時間點,任務被暫停了或因網絡緣由斷網、或停電、程序閃退或退出等等影響,此時下載中斷的位置就是斷點了。續傳就是當一個未完成的下載任務再次開始時,會從上次的斷點繼續傳送下載。固然,在實際的業務開發中,就是把一個大文件事先分紅多個小片斷返回給前端。
php
PHP支持斷點續傳,主要依靠HTTP協議中 header HTTP_RANGE實現。HTTP斷點續傳原理Http頭 Range、Content-Range()HTTP頭中通常斷點下載時纔用到Range和Content-Range實體頭,Range用戶請求頭中,指定第一個字節的位置和最後一個字節的位置,如(Range:200-300)Content-Range用於響應頭請求下載整個文件。
不使用斷點續傳html
get /down.zip http/1.1 accept: image/gif,image/x-xbitmap,image/jpeg,image/pjpeg,application/vnd.ms-excel,application/msword,application/vnd.ms-powerpoint accept-language:zh-cn accept-encoding:gzip,deflate user-agent:mozilla/4.0(compatible;msie 5.01;windows nt 5.0) connection:keep-alive
服務器收到請求後,按要求尋找請求的文件,提交文件的信息,而後返回給瀏覽器,返回信息以下:前端
HTTP/1.1 200 OK content - length = 106788888 accept - ranges = bytes date=mon, 30 apr 2021 12:12:11 gmt etag=w/「02ca57e173c11:95b」 content - type = application/octet - stream server = microsoft - iis /5.0 last-modified = mon, 30 apr 2021 12:12:11 gmt
使用斷點續傳windows
GET /down.zip HTTP/1.0 User - Agent : NetFox RANGE: bytes = 2000070- Accept:text/html,image/gif,image/jpeg,*;q=.2,*/*;q=.2
多了這麼一行Range:bytes = 2000070-
這一行的意思就是告訴服務器down.zip這個文件從2000070字節開始傳,前面的字節不用傳了。
Range的完整格式是:瀏覽器
Range:bytes = startOffset - targetOffset/sum [表示從startOffset讀取,一直讀取到targetOffset位置,讀取總數爲sum] Range:bytes = startOffset - targetOffset [字節總數也能夠去掉]
服務器收到這個請求後,返回的信息以下:服務器
HTTP/1.1 206 Partial Content content - length = 106788888 content - range = bytes 2000070 - 106788888 / 106788889 date = mon, 30 apr 2021 12:55:20 gmt etag = w/「02ca57e173c11:95b」 content - type = application / octet - stream server = microsoft - iis / 5.0 last - modified = mon, 30 apr 2021 12:55:20 gmt
和前面服務器返回的信息比較一下,就會發現增長了一行:網絡
Content - Range = bytes 2000070 - 106788888 / 106788889
返回的代碼也改成206了,而再也不是200了。app
HTTP/1.1 206 Partial Content
在實際場景中,會出現一種狀況,即在終端發起續傳請求時,URL對應的文件內容在服務端已經發生了變化,此時續傳的數據確定是錯誤的。如何解決這個問題呢?顯然此時須要有一個標識文件惟一性的方法。
在 RFC2616 中也有相應的定義,好比實現 Last-Modified 來標識文件的最後修改時間,這樣既可判斷出續傳文件時是否已經發生過改動。同時 FC2616 中還定義有一個ETag 的頭,可使用 ETag 頭來放置文件的惟一標識,好比文件的MD5值。
終端在發起續傳請求時應該在HTTP頭中申明If-Match 或者 If-Modified-Since 字段,幫助服務端判別文件變化。
另外RF2616中同時定義有一個If-Range頭,終端若是在續傳是使用If-Range。If-Range中的內容能夠爲最初收到的ETag頭或是Last-Modified中的最後修改時候。服務端在收到續傳請求時,經過If-Range中的內容進行校驗,校驗一致時返回206的續傳回應,不一致時服務端則返回200迴應,迴應的內容爲新的文件的所有數據。
ide
If-Modified-Since,和 Last-Modified 同樣都是用於記錄頁面最後修改時間的 HTTP 頭信息,只是 Last-Modified 是由服務器往客戶端發送的 HTTP 頭,而 If-Modified-Since 則是由客戶端往服務器發送的頭,能夠看到,再次請求本地存在的 cache 頁面時,客戶端會經過 If-Modified-Since 頭將先前服務器端發過來的 Last-Modified 最後修改時間戳發送回去,這是爲了讓服務器端進行驗證,經過這個時間戳判斷客戶端的頁面是不是最新的,若是不是最新的,則返回新的內容,若是是最新的,則返回 304 告訴客戶端其本地 cache 的頁面或文件是最新的,因而客戶端就能夠直接從本地加載頁面了,這樣在網絡上傳輸的數據就會大大減小,同時也減輕了服務器的負擔。
測試
Etag(Etity Tags)主要爲了解決 Last-Modified 沒法解決的一些問題。
1. 一些文件也許會週期性的更改,可是內容並不改變(僅改變修改時間),這時候咱們並不但願客戶端認爲這個文件被修改了,而從新 GET 。
2.某些文件修改很是頻繁,例如:在秒如下的時間內進行修改(1s內修改了N次),If-Modified-Since 能檢查到的粒度是 s 級的,這種修改沒法判斷(或者說 UNIX 記錄 MTIME 只能精確到秒)。
3.某些服務器不能精確的獲得文件的最後修改時間。
爲此,HTTP/1.1 引入了 Etag。Etag 僅僅是一個和文件相關的標記,能夠是一個版本標記,例如:v1.0.0;或者說「627-45235gfd56250」這麼一串看起來很神祕的編碼。可是 HTTP/1.1 標準並無規定 Etag 的內容是什麼或者說要怎麼實現,惟一規定的是 Etag 須要放在 「」 內。
用於判斷實體是否發生改變,若是實體未改變,服務器發送客戶端丟失的部分,不然發送整個實體。
通常格式:
If-Range:Etag | HTTP-Date
也就是說,If-Range 可使用 Etag 或者 Last-Modified 返回的值。當沒有 ETage 卻有 Last-modified 時,能夠把 Last-modified 做爲 If-Range 字段的值。
例如:
If-Range:Etag | HTTP-Date
也就是說,If-Range 可使用 Etag 或者 Last-Modified 返回的值。當沒有 ETag 卻有 Last-modified時,能夠把 Last-modified 做爲 If-Range 字段的值。
例如:
If-Range:「627-45235gfd56250」 If-Range:30 apr 2021 12:55:20 gmt
If-Range 必須與 Range 配套使用。若是請求報文中沒有 Range,那麼 If-Range 就會被忽略。若是服務器不支持 If-Range,那麼 Range 也會被忽略。
若是請求報文中的 Etag 與服務器目標內容的 Etag 相等,即沒有發生變化,那麼應答報文的狀態碼爲206。若是服務器目標內容發生了變化,那麼應答報文的狀態碼爲200.
用於校驗的其餘 HTTP 頭信息:If-Match/If-None-Match、If-Modified-Since/If-Unmodified-Since。
Etag 由服務器端生成,客戶端經過 If-Range 條件判斷請求來驗證資源是否修改。請求一個文件的流程以下:
第一次請求:
1.客戶端發起 HTTP GET 請求一個文件。
2.服務器處理請求,返回文件內容以及相應的 Header,其中包括 Etag (例如:627-45235gfd56250)(假設服務器支持 Etag 生成並已開啓了 Etag)狀態碼爲200。
第二次請求(斷點續傳):
1.客戶端發起 HTTP GET 請求一個文件,同時發送 If-Range (該頭的內容就是第一次請求時服務器返回的 Etag:627-45235gfd56250)。
2.服務器判斷接收到的 Etag 和計算出來的 Etag 是否匹配,若是匹配,那麼響應的狀態碼爲206;不然,狀態碼爲200。
<?php /* php下載類,支持斷點續傳 download: 下載文件 setSpeed: 設置下載速度 getRange: 獲取header中Range */ class FileDownload{ private $_speed = 512; //下載速度 /** 下載 * @ param String $file 要下載的文件路徑 * @ param String $name 文件名稱,爲空則與下載的文件名稱同樣 * @ param boolean $reload 是否開啓斷點續傳 */ public function download($file, $name=' ', $reload=false){ if(file_exists($file)){ if($name==' '){ $name = basename($file); } $header_array = get_headers($file, true); //下載本地文件,獲取文件大小 if(!$header_array){ $file_size = filesize($file); }else{ $file_size = $header_array['Content-Length']; } $ranges = $this->getRange($file_size); $ua = $_SERVER['HTTP_USER_AGENT'];//判斷是什麼類型瀏覽器 header('cache-control:public'); header('content-type:application/octet-stream'); header('content-disposition:attachment; filename='.$name); $encoded_filename = urlencode($name); $encoded_filename = str_replace("+","%20",$encoded_filename); //解決下載文件名亂碼 if(preg_match("/MSIE/",$ua) || preg_match("/Trident/", $ua)){ header('Content-Disposition:attachment; filename=" ' .$encoded_filename . ' " ') }else if(preg_match("/Firefox", $ua)) { header('Content-Disposition: attachment; filename*="utf8\ '\ ' ' . $name . ' " '); }else if(preg_match("/Chrome/", $ua)) { header('Content-Disposition: attachment; filename=" ' . $encoded_filename . ' " '); } else{ header('Content-Disposition: attachment; filename=" '.); } if($reload && $ranges != null){ //使用續傳 header('HTTP/1.1 206 Partial Content' ); header('Accept-Ranges:bytes' ); //剩餘長度 header(sprintf('content-length:%u',$ranges['end']-$ranges['start'])); //range信息 header(sprintf('content-range:bytes %s-%s/%s', $ranges['start'], $ranges['end'], $file_size)); //fp指針跳到斷點位置 fseek($fp, sprintf('%u', $ranges['start'])); }else{ header('HTTP/1.1 200 OK'); header('content-length:'.$file_size); } while(!feof($fp)){ echo fread($fp, round($this->_speed*1024,0)); ob_flush(); //sleep(1); //用於測試,減慢下載速度 } ($fp!=null) && fclose($fp); }else{ return ' '; } } /** 設置下載速度 * @ param int $speed */ public function setSpeed($speed){ if(is_numberic($speed) && $speed > 16 && $speed < 4096){ $this->_speed = $speed; } } /** 獲取header range信息 * @ param int $file_size 文件大小 * @ return Array */ private function getRange($file_size){ if(isset($_SERVER['HTTP_RANGE']) && !empty($_SERVER['HTTP_RANGE'])){ $range = $_SERVER['HTTP_RANGE']; $range = preg_replace('/[\s|,].*/', ' ', $range); $range = explode('-', substr($range, 6)); if(count($range) < 2){ $range[1] = file_size; } $range = array_combine(array('start','end'), $range); if(empty($range['start'])) { $range['start'] = 0; } if(empty($range['end'])) { $range['end'] = $file_size; } return $range; } return null; } } $file = 'down.zip'; $name = time().'.zip'; $obj = new FileDownload(); $flag = $obj->download($file, $name); //$flag = $obj->download($file, $name, true); //斷點續傳 if(!$flag){ echo 'file not exists'; } ?>