什么是断点续传下载?

就是下载文件时,不必重头开始下载,而是从指定的位置继续下载,这样的功能就做断点续传下载。断点续传的理解可以分为两部分:一部分是断点,一部分是续传下载。断点的由来是在下载过程中,将一个下载文件分成了多个部分,同时进行多个部分一起的下载,当某个时间点,任务被暂停了或因网络原因断网、或停电、程序闪退或退出等等影响,此时下载中断的位置就是断点了。续传就是当一个未完成的下载任务再次开始时,会从上次的断点继续传送下载。当然,在实际的业务开发中,就是把一个大文件事先分成多个小片段返回给前端。

简单介绍下HTTP断点续传原理

PHP支持断点续传,主要依靠HTTP协议中 header HTTP_RANGE实现。HTTP断点续传原理Http头 Range、Content-Range()HTTP头中一般断点下载时才用到Range和Content-Range实体头,Range用户请求头中,指定第一个字节的位置和最后一个字节的位置,如(Range:200-300)Content-Range用于响应头请求下载整个文件。

不使用断点续传

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

使用断点续传

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了。

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回应,回应的内容为新的文件的全部数据。

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

?>

PHP实现大文件断点下载的更多相关文章

  1. php的lavarel框架中join和orWhere的用法

    Laravel是一个开源PHP框架,功能强大且易于理解。它遵循模型 - 视图 - 控制器设计模式(MVC)。Laravel重用了不同框架的现有组件,这有助于创建Web应用程序。这样设计的Web应用程序更加结构化和实用。Laravel框架的主要特点:1.模块化包装2.依赖管理器完全基于composer......

  2. php 实现使用curl模拟百度蜘蛛进行采集

    //实现使用curl模拟百度蜘蛛进行采集class Curlcontent{protected function _GetContent( $url ){$this->ch = curl_init();$this->ip = '220.181.108.'.rand(1,255); //......

  3. 原生PHP网页导出和导入excel文件实例

    原生PHP实现的网页导出和导入excel文件实例,包括上传也是用的原生。还可在exportExcel方法里设置字体等表格样式。导出和导入表单代码:1 <p style="margin:10px 0"><a href="export.php" ......

  4. php中数组最简单的使用方法

    我们在说映射的时候,有些小伙伴就会联想到数组,没错,因为数组就是用了映射的思想。这里很多刚学php的小伙伴对概念不是很了解,不过小编把数组的定义、语法整理出来,大家跟着进行实例学习就可以了,最后再看看使用数组的一些注意事项。下面我们进入今天的学习吧。1.定义数组是PHP中的重要数组类型之一,是复合类......

  5. 前后端(PHP)使用AES对称加密

    前端代码:// 这个是加密用的 function encrypt(text){ var key = CryptoJS.enc.Utf8.parse('1234567890654321'); //为了避免补位,直接用16位的秘钥 var iv = CryptoJS.enc.Utf8.parse('12......

  6. 用PHP实现的服务端socket具体实例

    实现方法如下:使用到的相关函数: socket_create、socket_set_block、socket_bind、socket_listen、socket_accept、socket_read、socket_write,这些函数具体参数说明在PHP文档上写很详细,这里就不再赘述,这里只是介绍服......

  7. php中unable to fork报错简单解决方法

    今天小编遇到一个问题,当调用了system方法,并且执行了shell脚本,开始的时候,一切都非常正常,但是当程序运行后一段时间,出现了显示unable to fork的报错,这个是什么原因呢,后来小编排查了下,主要是因为达到用户的进程上限了,下面小编给大家介绍下解决方式。限制linux用户的进程数修......

  8. 【函数分享】每日PHP函数分享

    str_pad() 使用另一个字符串填充字符串为指定长度 。string str_pad ( string $input, int $pad_length[, string $pad_string=" "[, int $pad_type=STR_PAD_RIGHT]])参数描......

  9. PHP基础之与MySQL那些事

    前言这篇文章会对PHP的MySQL扩展库,MySQLI的扩展库,SQL批量执行,事务控制等等进行一些简单的讲解。MySQL扩展PHP中MySQL扩展,虽然因为安全的原因,在PHP5.6及往上不在支持MySQL扩展库,但是还是要学习的,通过编写案例的方式来讲解。案例先说下操作数据库的大体思路吧,就是先......

  10. php的curl携带header请求头信息实现http访问的方法

    导读:curl请求时添加请求头信息可以模拟真人操作,不容易被当成是爬虫机器人(采集),从而可以绕过Incapsula等安全验证机制。1、首先使用浏览器(示例使用的是火狐浏览器)访问接口网址,使用F12调试,查看请求头信息,如下:2、实现代码:?1234567891011121314151617181......

随机推荐

  1. MySQL下载地址与Centos7安装MySQL以及启动问题排查

    MySQL国内镜像下载地址以及开源镜像相关站点Centos7安装MySQL启动问题(The server quit without updating PID file)解决方式目录一、MySQL国内镜像下载二、国内镜像相关站点三、Centos7安装MySQL5.71. 下载并解压至/usr/loca......

  2. 如何改变R语言默认存储包的路径

    怎么更改R中包的存储路径呢?方法一可以在R里面用如下命令.libPaths("C:/Program Files/R/R-3.3.1/library")方法二在安装某一个包得时候用如下命令install.packages("thepackage",lib=&qu......

  3. 用Python制作音乐海报

    前言前段时间在一个朋友那么得到的灵感,想到可以用音乐播放页面作为一张海报图片。其实接下来要讲的和海报还是有差距的,而具体实现也只是简单的图片粘贴,但是在效果上还是不错的。效果图如下,希望大家喜欢:左边是原图,右边是需要添加到中间的图,也是图的主角。其实如果直接用ps实现上面的图是非常简单的,反倒是用......

  4. Json转换工具

    import java.util.List;import com.fasterxml.jackson.core.JsonProcessingException;import com.fasterxml.jackson.databind.JavaType;import com.fasterxml.ja......

  5. python3表格数据处理

    技术背景数据处理是一个当下非常热门的研究方向,通过对于大型实际场景中的数据进行建模,可以用于预测下一阶段可能出现的情况。比如我们有过去的2002年-2018年的黄金价格的数据:该数据来源于Gitee上的一个开源项目。其中包含有:时间、开盘价、收盘价、最高价、最低价、交易数以及成交额这么几个参数。假如......

  6. 使用JWT创建安全的ASP.NET Core Web API

    在本文中,你将学习如何在ASP.NET Core Web API中使用JWT身份验证。我将在编写代码时逐步简化。我们将构建两个终结点,一个用于客户登录,另一个用于获取客户订单。这些api将连接到在本地机器上运行的SQL Server Express数据库。JWT是什么?JWT或JSON Web To......

  7. 搭建 mariadb 数据库主从同步

    一、主(master)数据库配置1. my.cnf 添加配置[mariadb]log-binserver_id=1log-basename=master1binlog-format=mixedmax_binlog_size=200Mexpire_logs_days=7 server_id 必须唯一。......

  8. Java中多线程启动,为什么调用的是start方法,而不是run方法?

    前言大年初二,大家新年快乐,我又开始码字了。写这篇文章,源于在家和基友交流的时候,基友问到了,我猛然发现还真是这么回事,多线程启动调用的都是start,那么为什么没人掉用run呢?于是打开我的idea,翻一波代码,带大家一探究竟。继承thread类实现多线程我们知道java有三种方式实现多线程,这里......

  9. pandas 颠倒列顺序的两种解决方案

    在数据预处理过程中可能需要将列的顺序颠倒,有两种方法。import numpy as npimport pandas as pddf = pd.DataFrame(np.array(range(20)).reshape(4,5))print(df)原始dataframe如下:0 1 2 3 ......

  10. ASP.NET Core 基本知识 - 配置(Configuration)

    翻译自 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-5.0ASP.NET Core 中的配置使用一个或者多个配置提供程(configuration provide......