PHP實現大文件斷點下載

什麼是斷點續傳下載?

 就是下載文件時,沒必要重頭開始下載,而是從指定的位置繼續下載,這樣的功能就作斷點續傳下載。斷點續傳的理解能夠分爲兩部分:一部分是斷點,一部分是續傳下載。斷點的由來是在下載過程當中,將一個下載文件分紅了多個部分,同時進行多個部分一塊兒的下載,當某個時間點,任務被暫停了或因網絡緣由斷網、或停電、程序閃退或退出等等影響,此時下載中斷的位置就是斷點了。續傳就是當一個未完成的下載任務再次開始時,會從上次的斷點繼續傳送下載。固然,在實際的業務開發中,就是把一個大文件事先分紅多個小片斷返回給前端。
php

簡單介紹下HTTP斷點續傳原理

  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

Last-Modified

 If-Modified-Since,和 Last-Modified 同樣都是用於記錄頁面最後修改時間的 HTTP 頭信息,只是 Last-Modified 是由服務器往客戶端發送的 HTTP 頭,而 If-Modified-Since 則是由客戶端往服務器發送的頭,能夠看到,再次請求本地存在的 cache 頁面時,客戶端會經過 If-Modified-Since 頭將先前服務器端發過來的 Last-Modified 最後修改時間戳發送回去,這是爲了讓服務器端進行驗證,經過這個時間戳判斷客戶端的頁面是不是最新的,若是不是最新的,則返回新的內容,若是是最新的,則返回 304 告訴客戶端其本地 cache 的頁面或文件是最新的,因而客戶端就能夠直接從本地加載頁面了,這樣在網絡上傳輸的數據就會大大減小,同時也減輕了服務器的負擔。
測試

Etag

 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

用於判斷實體是否發生改變,若是實體未改變,服務器發送客戶端丟失的部分,不然發送整個實體。
通常格式:

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';
}

?>
相關文章
相關標籤/搜索