就是下载文件时,没必要重头开始下载,而是从指定的位置继续下载,这样的功能就作断点续传下载。断点续传的理解能够分为两部分:一部分是断点,一部分是续传下载。断点的由来是在下载过程当中,将一个下载文件分红了多个部分,同时进行多个部分一块儿的下载,当某个时间点,任务被暂停了或因网络缘由断网、或停电、程序闪退或退出等等影响,此时下载中断的位置就是断点了。续传就是当一个未完成的下载任务再次开始时,会从上次的断点继续传送下载。固然,在实际的业务开发中,就是把一个大文件事先分红多个小片断返回给前端。
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'; } ?>