Heim >Backend-Entwicklung >PHP-Tutorial >php 实现文件下载 断点续传 原理
php 实现文件下载 断点续传 原理
php文件下载
文件下载就是把文件从服务器上面下下来,比如一个文件是http://www.xxx.com/1.rar(真实存在这个文件),直接在浏览器上面输入,是可以弹出下载对话框的,但是如果是一个图片地址,很可能就是不是下载了,而是用浏览器直接打开.还有就是为了不让客户端看到下载地址,最好也是不直接提供下载文件地址的.还有就是比如只有登录用户才能下载。。 等等 .所以还是要单独写个下载程序
有几个响应头比较重要,记录下
Content-type
Content-Disposition
Content-Length
Pragma
Cache-control
Content-type
Content-type 告诉浏览器文件的MIME 类型,这是非常重要的一个响应头了,MIME种类繁多,真是太多了
Content-Disposition
Content-Disposition 是 MIME 协议的扩展,MIME 协议指示 MIME 用户代理如何显示附加的文件。当 Internet Explorer 接收到头时,它会激活文件下载对话框,它的文件名框自动填充了头中指定的文件名。 嗯,就是这个头哟,激活弹出提示下载框
因为MIME的类型非常多,很可能会在程序中漏掉一些MIME类型.可以这样,漏掉了类型,全部以"Content-type: application/octet-stream" "Content-Disposition: attachment; filename=xxx.xx"(xx.xx为文件名) 经过一些浏览器的测试发现下载下来的文件都是正确的
Content-Length
"Content-Length: 123" 就是告诉浏览器这个文件的大小是123字节,其实我发现好像不设置这个头,浏览器也能自己识别
Pragma Cache-control
把这2个头都设置成public 告诉浏览器缓存(其实对于缓存头 我有很多都没理解清楚 除了public 它表示全部都缓存,但是至于浏览器缓存不缓存,可能还不一定,我试了很多浏览器,没有一个是从浏览器缓存读文件的,但是从服务器上下的)
在输出文件的时候,我看网上大概有2中方式:一种是一次性输出,一中是分段输出(每次输出一部分),我去csdn问了下,别人说分段输出好,减轻服务器压力,因为每次读文件的时候占的内存都少
一个例子(里面的MIME很少,更多的MIME最好去网上找)
class downLoad{ var $file_name; var $file_dir; var $buffer_size = 1024; var $err = ""; public static $MIME_type = array( "pdf" =>"application/pdf", "exe" =>"application/octet-stream", "zip" =>"application/zip", "doc" =>"application/msword", "xls" =>"application/vnd.ms-excel", "ppt" =>"application/vnd.ms-powerpoint", "gif" =>"image/gif", "png" =>"image/png", "jpeg" =>"jpg", "mp3" =>"audio/mpeg", "wav" =>"audio/x-wav", "mpeg" =>"mpg", "mpe" =>"video/mpeg", "mov" =>"video/quicktime", "avi" =>"video/x-msvideo", ); public function __construct($file_dir="",$file_name=""){ $this->file_dir = $file_dir; $this->file_name = $file_name; $this->path = $file_dir."/".$file_name; $this->suffix = pathinfo($file_name,PATHINFO_EXTENSION); } public function down(){ if(!file_exists($this->path)){ $this->err = "该文件被移除了"; return false; } $content_type = $this->getMIME($this->suffix); $file_size = filesize($this->path); header("Content-type: ".$content_type); header('Content-Disposition: attachment; filename="'.$this->file_name.'"'); @header("Cache-control: public"); @header("Pragma: public"); header("Content-Length: ".$file_size); ob_end_clean(); //readfile($this->path); 一次性读出来 $fp= fopen($this->path,"r"); $buffer_size = $this->buffer_size; $cur_pos = 0; //记录读了多少了 while(!feof($fp) && $file_size>$buffer_size+$cur_pos){ $buffer = fread($fp,$buffer_size); //每次读1024字节 echo $buffer; $cur_pos += $buffer_size; } //把剩下的读出来 因为文件的带下很有很能不是1024 的整数倍 $buffer = fread($fp,$file_size-$cur_pos); echo $buffer; fclose($fp); return true; } public function getMIME($key=""){ if($key == "" || !isset(self::$MIME_type[$key])){ return "application/octet-stream"; } return self::$MIME_type[$key]; } } // $x = new downLoad($file_dir,$file_name); $file_dir路径 比如 all $file_name文件名 比如 a.exe 合起来就是全部的路径了all/a.exe // $x->down();
php 断点续传
这里面的断点续传指的是下载时候的断点续传的断点续传,上传的断点续传完全不明白原理
一些预备的东西
http 状态码206的意思
响应头 "Accept-Ranges: bytes"
响应头 "Content-Range:bytes start-end/all"
fseek
ob_flush
http 状态码 206
请求的 header 必须包含一个 Range 字段,表明自己请求第几个字节到第几个字节的内容。如果服务器支持的话,就返回 206 Partial Content,然后使用 header 的 Content-Range 指明范围,并在 body 内提供这个范围内的数据。
Accept-Ranges: bytes
这个字段说明Web服务器是否支持Range(是否支持断点续传功能),如果支持,则返回Accept-Ranges: bytes,如果不支持,则返回Accept-Ranges: none.
Content-Range:
用于响应头,指定了返回的Web资源的字节范围。这个字段值的格式是: 开始字节位置—结束字节位置/Web资源的总字节数,一个例子:Content-Range:1000-3000/5000
fseek : 该函数把文件指针从当前位置向前或向后移动到新的位置,新位置从文件头开始以字节数度量
ob_flush : 该函数把文件指针从当前位置向前或向后移动到新的位置,新位置从文件头开始以字节数度量
我测试了一下 ie7,8是不支持断点续传的(也许可以 但是我不会),苹果浏览器也不行.哎.......... (还好 firefox chrome Opera支持)
断点续传的过程是这样的
首先客户端发一次请求给php请求下载文件
这时因为是第一次请求 所以请求头里面是没有 Range头的
我用Fiddler抓包的截图
请求头
在php端 $_SERVER['HTTP_RANGE']可以取请求头里面的Rang, 因为第一次不存在这个,所以我们取文件内容就取全部,然后返回的状态码也是200
响应头
当传了一部分内容后,点击暂停,在点击几乎继续,抓包的响应头就有Range了
php端返回的时候也把返回内容的范围返回了 Content-Range: bytes 4194304-37939151/37939152 是返回数据的范围
Accept-Ranges: bytes表示支持断点续传
Content-Length: 33744847 里面的值是根据请求的Range, 也就是 $_SERVER['HTTP_RANGE'], 和文件的总大小,相减得出来的
另外http状态码是206
例子
class downLoad { var $file_path; var $file_name; var $file_suffix; var $speed = 1024; //表示下载速度1m var $err = ""; var $range_start=0; var $range_end; var $http_range; var $file_size; public static $MIME_type = array( "pdf" =>"application/pdf", "exe" =>"application/octet-stream", "zip" =>"application/zip", "doc" =>"application/msword", "xls" =>"application/vnd.ms-excel", "ppt" =>"application/vnd.ms-powerpoint", "gif" =>"image/gif", "png" =>"image/png", "jpeg" =>"jpg", "mp3" =>"audio/mpeg", "wav" =>"audio/x-wav", "mpeg" =>"mpg", "mpe" =>"video/mpeg", "mov" =>"video/quicktime", "avi" =>"video/x-msvideo", ); public function __construct($file_path="",$http_ranges="",$speed=""){ $this->file_path = $file_path; $this->file_name = basename($file_path); $this->file_suffix = substr(strrchr($this->file_name, '.'),1); $this->file_size= filesize($file_path); $this->http_range = (isset($http_ranges) && !empty($http_ranges)!="")?$http_ranges:false; !empty($speed)&&($this->speed = $speed); } public function down(){ if(!file_exists($this->file_path)){ $this->err = "该文件被移除"; return false; } $file_path = $this->file_path; $content_type = $this->getMIME($this->file_suffix); $http_range = $this->http_range; if($http_range){ //$http_range 的形式为 bytes=3145728- 差不多就是这个样子的 $http_range = preg_replace('/[\s|,]*/', '', $http_range); $arr = explode('-',substr($http_range,6)); $this->range_start = ( isset($arr[0]) && !empty($arr[0]) ) ? $arr[0] : 0; $this->range_end = ( isset($arr[1]) && !empty($arr[1]) && $arr[1]!=0 ) ? $arr[1] : $this->file_size-1; } $this->setHeader(); $fh = fopen($file_path, "rb"); fseek($fh, $this->range_start); $speed = $this->speed; while(!feof($fh)) { echo fread($fh, 1024*$speed); ob_flush(); sleep(1); } fclose($fh); } public function getMIME($key=""){ if($key=="" || !isset(self::$MIME_type[$key])){ return "application/octet-stream"; } return self::$MIME_type[$key]; } public function setHeader(){ header('Cache-control: public'); header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename='.$this->file_name); if($this->http_range){ $range_start = $this->range_start; $range_end = $this->range_end; header('HTTP/1.1 206 Partial Content'); header('Accept-Ranges: bytes'); header( sprintf('Content-Length: %u',$range_end - $range_start) ); header( sprintf('Content-Range: bytes %s-%s/%s', $range_start, $range_end, $this->file_size) ); }else{ header("HTTP/1.1 200 OK"); header(sprintf('Content-Length: %s',$this->file_size)); } } } //$http_range = isset($_SERVER['HTTP_RANGE'])?$_SERVER['HTTP_RANGE']:false; //$x = new downLoad("all/1.exe",$http_range); //$x->down();