php文件下載限速,文件斷點續傳,多線程下載文件原理解析

  • 2019 年 12 月 19 日
  • 筆記

文件下載限速

首先,我們寫一段使用php輸出文件給瀏覽器下載的代碼

<?php  /**   * Created by PhpStorm.   * User: tioncico   * Date: 19-2-4   * Time: 下午4:30   */  $filePath = './hyxd.zip';//文件  $fp=fopen($filePath,"r");    //取得文件大小  $fileSize=filesize($filePath);    header("Content-type:application/octet-stream");//設定header頭為下載  header("Accept-Ranges:bytes");  header("Accept-Length:".$fileSize);//響應大小  header("Content-Disposition: attachment; filename=testNaame");//文件名    $buffer=1024;  $bufferCount=0;    while(!feof($fp)&&$fileSize-$bufferCount>0){//循環讀取文件數據      $data=fread($fp,$buffer);      $bufferCount+=$buffer;      echo $data;//輸出文件  }    fclose($fp);

可以看出,php實現瀏覽器下載文件,主要是靠header頭的支持以及echo 文件數據,那麼,該如何限制速度呢?可以通過限制輸出頻率嗎?例如每次讀取1024之後,就進行一次sleep?

<?php  /**   * Created by PhpStorm.   * User: tioncico   * Date: 19-2-4   * Time: 下午4:30   */  $filePath = './hyxd.zip';//文件  $fp=fopen($filePath,"r");    //取得文件大小  $fileSize=filesize($filePath);    header("Content-type:application/octet-stream");//設定header頭為下載  header("Accept-Ranges:bytes");  header("Accept-Length:".$fileSize);//響應大小  header("Content-Disposition: attachment; filename=testName");//文件名    $buffer=1024;  $bufferCount=0;    while(!feof($fp)&&$fileSize-$bufferCount>0){//循環讀取文件數據      $data=fread($fp,$buffer);      $bufferCount+=$buffer;      echo $data;//輸出文件      sleep(1);//增加了一個sleep  }    fclose($fp);

但是通過瀏覽器訪問,我們發現是不行的,甚至造成了瀏覽器只有在n秒之後才會出現下載確認框,是哪裡出了問題呢?

其實,這是因為php的buffer引起的,php buffer緩衝區,會使php不會馬上輸出數據,而是需要等緩衝區滿之後才會響應到web服務器,通過web服務器再響應到瀏覽器中,詳細請看:關於php的buffer(緩衝區)

那該怎麼改呢?其實很簡單,只需要使用ob系列函數就可解決:

<?php  /**   * Created by PhpStorm.   * User: tioncico   * Date: 19-2-4   * Time: 下午4:30   */  $filePath = './hyxd.zip';//文件  $fp=fopen($filePath,"r");    //取得文件大小  $fileSize=filesize($filePath);    header("Content-type:application/octet-stream");//設定header頭為下載  header("Accept-Ranges:bytes");  header("Accept-Length:".$fileSize);//響應大小  header("Content-Disposition: attachment; filename=testName");//文件名  ob_end_clean();//緩衝區結束  ob_implicit_flush();//強制每當有輸出的時候,即刻把輸出發送到瀏覽器  header('X-Accel-Buffering: no'); // 不緩衝數據  $buffer=1024;  $bufferCount=0;    while(!feof($fp)&&$fileSize-$bufferCount>0){//循環讀取文件數據      $data=fread($fp,$buffer);      $bufferCount+=$buffer;      echo $data;//輸出文件      sleep(1);  }    fclose($fp);

這樣,我們就已經實現了,每秒只輸出1024位元組的數據:

我們可以增加下載速度,把buffer改成更大的值,例如102400,那麼就會變成每秒下載100kb:

文件斷點續傳

那麼,我們該如何實現文件斷點續傳呢?首先,我們要了解http協議中,關於請求頭的幾個參數:

content-range和range,

在文件斷點續傳中,必須包含一個斷點續傳的參數,例如:

請求下載頭:

Range: bytes=0-801 //一般請求下載整個文件是bytes=0- 或不用這個頭

響應文件頭:

Content-Range: bytes 0-800/801 //801:文件總大小

正常下載文件時,不需要使用range頭,而當斷點續傳時,由於再之前已經獲得了n位元組數據,所以可以直接請求

Range: bytes=n位元組-總文件大小,代表着n位元組之前的數據不再下載

響應頭也是如此,那麼,我們通過之前的限速下載,進行暫停,然後繼續下載試試吧:

可看到,我們下載到600kb之後暫停了,然後我們代碼記錄下下次請求的請求數據:

<?php  /**   * Created by PhpStorm.   * User: tioncico   * Date: 19-2-4   * Time: 下午4:30   */  $filePath = './hyxd.zip';//文件  $fp=fopen($filePath,"r");  set_time_limit(1);  //取得文件大小  $fileSize=filesize($filePath);  file_put_contents('1.txt',json_encode($_SERVER));  //下面的代碼直接忽略了,主要看server

當我點擊繼續下載時,瀏覽器會報出下載失敗,原因是我們沒有正確的響應它需要的數據,然後我們看下1.txt並打印成數組:

可看到,瀏覽器增加了一個range的請求頭參數,想請求61400位元組-文件尾的文件數據,那麼,我們後端該如何處理呢?

我們只需要輸出61400之後的文件內容即可

為了方便測試查看,我將文件改為了2.txt,內容如下:

編寫可斷點續傳代碼:

<?php  /**   * Created by PhpStorm.   * User: tioncico   * Date: 19-2-4   * Time: 下午4:30   */  $filePath = './2.txt';//文件  $fp=fopen($filePath,"r");  //set_time_limit(1);  //取得文件大小  $fileSize=filesize($filePath);  $buffer=5000;  $bufferCount=0;  header("Content-type:application/octet-stream");//設定header頭為下載  header("Content-Disposition: attachment; filename=2.txt");//文件名  if (!empty($_SERVER['HTTP_RANGE'])){      //切割字符串      $range = explode('-',substr($_SERVER['HTTP_RANGE'],6));      fseek($fp,$range[0]);//移動文件指針到range上      header('HTTP/1.1 206 Partial Content');      header("Content-Range: bytes $range[0]-$fileSize/$fileSize");      header("content-length:".$fileSize-$range[0]);  }else{      header("Accept-Length:".$fileSize);//響應大小  }    ob_end_clean();//緩衝區結束  ob_implicit_flush();//強制每當有輸出的時候,即刻把輸出發送到瀏覽器  header('X-Accel-Buffering: no'); // 不緩衝數據  while(!feof($fp)&&$fileSize-$bufferCount>0){//循環讀取文件數據      $data=fread($fp,$buffer);      $bufferCount+=$buffer;      echo $data;//輸出文件      sleep(1);  }  fclose($fp);

使用谷歌瀏覽器進行下載並暫停

查看當前下載內容:

可看到,最後下載到的字符串為13517x,恢復瀏覽器下載,繼續暫停

成功對接,並看到現在斷點在51017x中,繼續下載直到完成:

使用代碼驗證:

$txt = file_get_contents('/home/tioncico/Downloads/2.txt');    $arr = explode('x',$txt);  var_dump(count($arr));  var_dump($arr[count($arr)-2]);

成功下載

多線程下載

通過前面,我們或許發現了什麼:

1:限速是限制當前連接的數量

2:可以通過range來實現文件分片下載

那麼,我們能不能使用多個連接,每個連接只下載x個位元組,到最後進行拼裝成一個文件呢?答案是可以的

下面,我們就使用php的curl_multi進行多線程下載

<?php    $filePath = '127.0.0.1/2.txt';  //查看文件大小  $ch = curl_init();    //$headerData = [  //    "Range: bytes=0-1"  //];  //curl_setopt($ch, CURLOPT_HTTPHEADER, $headerData);  curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "HEAD");  curl_setopt($ch, CURLOPT_URL, $filePath);  curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // return don't print  curl_setopt($ch, CURLOPT_TIMEOUT, 30);  curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)');  curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); // 302 redirect  curl_setopt($ch, CURLOPT_MAXREDIRS, 7);  curl_setopt($ch, CURLOPT_HEADER, true);//需要獲取header頭  curl_setopt($ch, CURLOPT_NOBODY, 1);    //不需要body,只需要獲取header頭的文件大小  $sContent = curl_exec($ch);  // 獲得響應結果里的:頭大小  $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);//獲取header頭大小  // 根據頭大小去獲取頭信息內容  $header = substr($sContent, 0, $headerSize);//獲取真實的header頭  curl_close($ch);  $headerArr = explode("rn", $header);  foreach ($headerArr as $item) {      $value = explode(':', $item);      if ($value[0] == 'Content-Length') {          $fileSize = (int)$value[1];//文件大小          break;      }  }    //開啟多線程下載  $mh = curl_multi_init();  $count = 5;//n個線程  $handle = [];//n線程數組  $data = [];//數據分段數組  $fileData = ceil($fileSize / $count);  for ($i = 0; $i < $count; $i++) {      $ch = curl_init();        //判斷是否讀取數量大於剩餘數量      if ($fileData > ($fileSize-($i * $fileData))) {          $headerData = [              "Range:bytes=" . $i * $fileData . "-" . ($fileSize)          ];      }else{          $headerData = [              "Range:bytes=" . $i * $fileData . "-" .(($i+1)*$fileData)          ];      }      echo PHP_EOL;      curl_setopt($ch, CURLOPT_URL, $filePath);      curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // return don't print      curl_setopt($ch, CURLOPT_TIMEOUT, 30);      curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)');      curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); // 302 redirect      curl_setopt($ch, CURLOPT_HTTPHEADER, $headerData);      curl_setopt($ch, CURLOPT_MAXREDIRS, 7);      curl_multi_add_handle($mh, $ch); // 把 curl resource 放進 multi curl handler 里      $handle[$i] = $ch;  }  $active = null;    do {      //同時執行多線程,直到全部完成或超時      $mrc = curl_multi_exec($mh, $active);  } while ($active);    for ($i = 0; $i < $count; $i++) {      $data[$i] = curl_multi_getcontent($handle[$i]);      curl_multi_remove_handle($mh, $handle[$i]);  }  curl_multi_close($mh);  $file = implode('',$data);//組合成一個文件  $arr = explode('x',$file);  var_dump($data);  var_dump(count($arr));  var_dump($arr[count($arr)-2]);  //測試文件是否正確

運行截圖:

該代碼將會開出5個線程,按照不同的文件段去同時下載,再最後組裝成一個字符串,即實現了多線程下載

以上代碼是訪問nginx直接測試的,之前的代碼不支持head  http頭,我們需要修改一下才可以支持(但這是標準http寫法)

我們需要修改下之前的代碼,使其支持range的結束位置:

<?php  /**   * Created by PhpStorm.   * User: tioncico   * Date: 19-2-4   * Time: 下午4:30   */  $filePath = './2.txt';//文件  $fp = fopen($filePath, "r");  //set_time_limit(1);  //取得文件大小  $fileSize = filesize($filePath);  $buffer = 50000;  $bufferCount = 0;  header("Content-type:application/octet-stream");//設定header頭為下載  header("Content-Disposition: attachment; filename=2.txt");//文件名  if (!empty($_SERVER['HTTP_RANGE'])) {      //切割字符串      $range = explode('-', substr($_SERVER['HTTP_RANGE'], 6));      fseek($fp, $range[0]);//移動文件指針到range上      header('HTTP/1.1 206 Partial Content');      header("Content-Range: bytes $range[0]-$range[1]/$fileSize");      $range[1]>0&&$fileSize=$range[1];//只獲取range[1]的數量      header("content-length:" . $fileSize - $range[0]);  } else {      header("Accept-Length:" . $fileSize);//響應大小  }    ob_end_clean();//緩衝區結束  ob_implicit_flush();//強制每當有輸出的時候,即刻把輸出發送到瀏覽器  header('X-Accel-Buffering: no'); // 不緩衝數據  while (!feof($fp) && $fileSize-$range[0] - $bufferCount > 0) {//循環讀取文件數據      //避免多讀取      $buffer>($fileSize-$range[0]-$bufferCount)&&$buffer=$fileSize-$range[0]-$bufferCount;      $data = fread($fp, $buffer);      $bufferCount += $buffer;      echo $data;//輸出文件      sleep(1);  }  fclose($fp);

修改下多線程下載代碼:

<?php    $filePath = '127.0.0.1';  //查看文件大小  $ch = curl_init();    $headerData = [      "Range: bytes=0-1"  ];  curl_setopt($ch, CURLOPT_HTTPHEADER, $headerData);  //curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "HEAD");  curl_setopt($ch, CURLOPT_URL, $filePath);  curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // return don't print  curl_setopt($ch, CURLOPT_TIMEOUT, 0);  curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)');  curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); // 302 redirect  curl_setopt($ch, CURLOPT_MAXREDIRS, 7);  curl_setopt($ch, CURLOPT_HEADER, true);//需要獲取header頭  curl_setopt($ch, CURLOPT_NOBODY, 1);    //不需要body,只需要獲取header頭的文件大小  $sContent = curl_exec($ch);  // 獲得響應結果里的:頭大小  $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);//獲取header頭大小  // 根據頭大小去獲取頭信息內容  $header = substr($sContent, 0, $headerSize);//獲取真實的header頭  curl_close($ch);  $headerArr = explode("rn", $header);  foreach ($headerArr as $item) {      $value = explode(':', $item);      if ($value[0] == 'Content-Range') {//通過分段,獲取到文件大小          $fileSize = explode('/',$value[1])[1];//文件大小          break;      }  }  var_dump($fileSize);  //開啟多線程下載  $mh = curl_multi_init();  $count = 5;//n個線程  $handle = [];//n線程數組  $data = [];//數據分段數組  $fileData = ceil($fileSize / $count);  for ($i = 0; $i < $count; $i++) {      $ch = curl_init();        //判斷是否讀取數量大於剩餘數量      if ($fileData > ($fileSize-($i * $fileData))) {          $headerData = [              "Range:bytes=" . $i * $fileData . "-" . ($fileSize)          ];      }else{          $headerData = [              "Range:bytes=" . $i * $fileData . "-" .(($i+1)*$fileData)          ];      }      echo PHP_EOL;      curl_setopt($ch, CURLOPT_URL, $filePath);      curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // return don't print      curl_setopt($ch, CURLOPT_TIMEOUT, 0);      curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)');      curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); // 302 redirect      curl_setopt($ch, CURLOPT_HTTPHEADER, $headerData);      curl_setopt($ch, CURLOPT_MAXREDIRS, 7);      curl_multi_add_handle($mh, $ch); // 把 curl resource 放進 multi curl handler 里      $handle[$i] = $ch;  }  $active = null;    do {      //同時執行多線程,直到全部完成或超時      $mrc = curl_multi_exec($mh, $active);  } while ($active);    for ($i = 0; $i < $count; $i++) {      $data[$i] = curl_multi_getcontent($handle[$i]);      curl_multi_remove_handle($mh, $handle[$i]);  }  curl_multi_close($mh);  $file = implode('',$data);//組合成一個文件  $arr = explode('x',$file);  var_dump($data);  var_dump(count($arr));  var_dump($arr[count($arr)-2]);  //測試文件是否正確

成功下載,測試耗時結果為:5個線程4秒左右完成,1個線程花費13秒完成

本文為仙士可原創文章,轉載無需和我聯繫,但請註明來自仙士可博客www.php20.cn