zoukankan      html  css  js  c++  java
  • 掀开断点续传那一层面纱(下载篇)

    1、简介

      这一篇文章主要介绍的是http协议下载时的断点续传,详细到各个步骤。主要步骤有:DNS查找、TCP三次握手、http请求发送、TCP协议数据传输、暂停后的状态、继续下载、TCP三次握手、http请求发送、数据传输、。。。、下载成功发送http响应信息、TCP四次握手断开连接。

    2、原理知识

      2.1、问答问答

      问:什么是断点续传?断点续传的原理是什么?

      答:断点续传就是信号中断后(掉线或关机等),下次能够从上次的地方接着传送(一般指下载或上传),不支持断点续传就意味着下次下载或上传必须从零开始。http协议中的断点续传是基于Http头Range以及Content-Range。HTTP头中一般断点下载时才用到Range和Content-Range实体头,Range用户请求头中,指定第一个字节的位置和最后一个字节的位置,如( Range:200-300或者Range:200- );Content-Range用于响应头。通俗的来讲就是文件大小为10,这次下载了3,被中断了,下次继续下载时则将指针移到3位置,从3开始下载,最终将整个文件下载下来。

      2.2、简单http下载文件

      请求下载整个文件: 
      GET /test.rar HTTP/1.1 
      Connection: close 
      Host: 192.168.95.11
      Range: bytes=0-801 //一般请求下载整个文件是bytes=0- 或不用这个头
      一般正常回应 :
      HTTP/1.1 200 OK 
      Content-Length: 801      
      Content-Type: application/octet-stream 
      Content-Range: bytes 0-800/801 //801:文件总大小

      2.3、重要的几个头

      响应头:

      Content-type:Content-type 告诉浏览器文件的MIME 类型,这是非常重要的一个响应头了,MIME种类繁多。很可能会在程序中漏掉一些MIME类型,表示全部为 content-type:application/octet-stream(字节流)

      Content-Disposition:是 MIME 协议的扩展,MIME 协议指示 MIME 用户代理如何显示附加的文件。当 Internet Explorer 接收到头时,它会激活文件下载对话框,它的文件名框自动填充了头中指定的文件名。 嗯,就是这个头哟,激活弹出提示下载框,一般这样写content-disposition:attachment; filename=name

      Content-Length:"Content-Length: 321" 就是告诉浏览器这个文件的大小是321字节,其实我发现好像不设置这个头,浏览器也能自己识别
      Pragma Cache-control:把这2个头都设置成public 告诉浏览器缓存,我一般设置cache-control:public

      Content-Range:字段说明服务器返回了文件的某个范围及文件的总长度。这时Content-Length字段就不是整个文件的大小了,而是对应文件这个范围的字节数,这一点一定要注意。一般格式,Content-Range: bytes 500-999/1000

      响应头: 

      Range:可以请求实体的一个或者多个子范围。

      例如:
      表示头500个字节:bytes=0-499
      表示第二个500字节:bytes=500-999
      表示最后500个字节:bytes=-500
      表示500字节以后的范围:bytes=500-  【下载断点续传(一般range格式为500-)】
      第一个和最后一个字节:bytes=0-0,-1
      同时指定几个范围:bytes=500-600,601-999
      但是服务器可以忽略此请求头,如果无条件GET包含Range请求头,响应会以状态码206(PartialContent)返回而不是以200(OK)。【206表示服务器已经完成get的部分请求,即表示断点续传】

    3、支持断点续传的文件下载类

    类中含有注释,这里不再多解释了

    FileDownload.class.php

    复制代码
      1 <?PHP
      2 #文件下载(支持断点续传)
      3 class FileDownload
      4 {
      5     #下载速度
      6     private $_speed = 512;
      7 
      8     /**
      9     * @desc 下载文件
     10     *  
     11     * @param $file string 下载的文件路径
     12     * @param $name string 保存文件时的文件名,不写则最终下载文件默认为原文件名
     13     * @param $reload bool 是否使用断点续传方式下载
     14     */
     15     public function download($file, $name='', $reload=false)
     16     {
     17         if(file_exists($file))  #判断文件是否存在
     18         {
     19             if($name == '')     #判断命名参数是否存在
     20             {
     21                 $name = basename($file);    #采用原文件名进行存储
     22             }
     23             $fHandle = fopen($file, 'rb');   #只读方式打开;为移植性考虑,使用b标记打开文件(不同系统有不同换行符)
     24             $fileSize = filesize($file);    #文件大小
     25             $ranges = $this->getRange($fileSize);  #断点续传时,先查看下载的区间范围
     26             header('cache-control:public');         #可以被任何缓存所缓存
     27             header('content-type:application/octet-stream');  #告诉浏览器响应的对象的类型(字节流、浏览器默认使用下载方式处理)
     28             header('content-disposition:attachment; filename='.$name); #不打开此文件,刺激浏览器弹出下载窗口
     29             #判断是否使用续传方式进行下载
     30             #且请求头ranges不能为null(为null表示第一次请求下载)
     31             if($reload && $ranges!=null)
     32             {
     33                 header('HTTP/1.1 206 Partial Content');     #发送自定义报文 206续传状态码
     34                 header('Accept-Ranges:bytes');              #表明服务器支持Range请求,所支持的单位是字节
     35                 # 剩余长度 
     36                 header(sprintf('content-length:%u',$ranges['end']-$ranges['start'])); 
     37                 # range信息 
     38                 header(sprintf('content-range:bytes %s-%s/%s', $ranges['start'], $ranges['end'], $fileSize));  
     39                 # fHandle指针跳到断点位置 
     40                 fseek($fHandle, sprintf('%u', $ranges['start'])); 
     41             }
     42             else
     43             {
     44                 header('HTTP/1.1 200 OK'); 
     45                 header('content-length:'.$fileSize);
     46             }
     47             while(!feof($fHandle))
     48             { 
     49                 echo fread($fHandle, round($this->_speed*1024,0)); 
     50                 ob_flush();    #把数据从PHP的缓冲中释放出来
     51                 //sleep(2); // 用于测试,减慢下载速度 
     52             } 
     53             ($fHandle!=null) && fclose($fHandle);
     54         }
     55         else
     56         {
     57             #没文件
     58             header("HTTP/1.1 404 Not Found");
     59             return false;
     60         }
     61     }
     62 
     63     /**
     64     * @desc 获取请求头部range信息
     65     *
     66     * @param $fileSize int 该文件的大小
     67     *
     68     * @return array|null 返回range信息或者null
     69     */
     70     public function getRange($fileSize)
     71     {
     72         if(isset($_SERVER['HTTP_RANGE']) && !empty($_SERVER['HTTP_RANGE']))
     73         {
     74             #请求头部range信息  Range: bytes=41078-
    
     75             $range = $_SERVER['HTTP_RANGE']; 
     76             $range = preg_replace('/[s|,].*/', '', $range); 
     77             $range = explode('-', substr($range, 6));       #只需将41078-进行分割变成数组
     78             #断点续传头部range信息都是为 4444- 这种形式 ,因此切割后形成的数组就只有两个元素
     79             $range = array_combine(array('start','end'), $range); 
     80             if(empty($range['start']))
     81             { 
     82                 $range['start'] = 0; 
     83             } 
     84             if(empty($range['end']))
     85             { 
     86                 $range['end'] = $fileSize; 
     87             } 
     88             return $range; 
     89         }
     90         return null;    #第一次请求没有range信息
     91     }
     92 
     93     /**
     94     * @desc 设置文件下载速度
     95     *
     96     * @param $speed int 下载速度
     97     */
     98     public function setSpeed($speed)
     99     { 
    100         if(is_numeric($speed) && $speed>16 && $speed<4096)
    101         { 
    102             $this->_speed = $speed; 
    103         } 
    104     } 
    105 
    106 }
    107 
    108 ?>
    复制代码

    4、测试并分析其中的步骤

      4.1、前提准备工作

    • 将上面类文件中第六行下载速度更改为10
    • 去掉上面类文件第51行的注释,使它有延迟
    • 使用火狐浏览器进行下载测试
    • 使用Wireshark抓包工具进行抓包分析
    • test.php文件
    复制代码
    1 <?php
    2 include 'FileDownload.class.php';
    3 $a=new FileDownload();
    4 #不支持断点续传
    5 #$b=$a->download('./aa.txt','bb.txt');  
    6 #支持断点续传
    7 #$b=$a->download('./aa.txt','bb.txt',1);    
    8 ?>
    复制代码

      开始测试:

      4.2、测试支持断点续传下载

      执行步骤:

      1、打开抓包工具进行监控

      2、用火狐浏览器进行访问,Enter下载

      

      3、确认下载

      4、中途暂停两次,最后下载成功

    成功下载!

      分析抓包:

      1、首先Enter,第一步当然是进行DNS查找啦。这里就不展开讲了,可以参考这里的内容http://www.cnblogs.com/phpstudy2015-6/p/6810130.html#_label18

      2、拿到域名对应的IP后,浏览器向服务器80端口发起TCP的连接请求,请看下面的抓包图-1,一到三行尾TCP连接,即TCP三次握手。具体可以参考我写的这篇文章http://www.cnblogs.com/phpstudy2015-6/p/6810130.html#_label2

     抓包图-1

      3、TCP连接后,浏览器发起一个HTTP请求,即抓包图-1中的第4行。下图是该http GET请求。第一次请求不存在信息头range

    http请求图 

      4、http请求后,开始TCP数据传输,请看上面的抓包图-1,第5行后就开始有顺序的进行tcp层数据传输(192.168.95.11Web主机连续发送两次数据给192.168.95.10浏览器;浏览器接收并回应一次Web主机,告诉Web主机已经收到数据并且完整无误,可以继续传输!)

      5、此时暂停下载,。请看下面的抓包图-2,第72行的时候,暂停下载(即断开与Web服务器的连接)。因为这是突然断开的,Web主机并不知道浏览器已经断开了,所以还一直发送数据给浏览器(73~76),但是Web服务器没有收到浏览器的回应,最后它也不发数据,大家分手了。

      这个请求最后是没有收到Web服务器的http响应信息的。按照原本的请求是下载完整个文件后,Web才发送http响应消息的,但是浏览器突然单方面断开,此时数据都没传送完,怎么会给你相应消息呢!

    抓包图-2

      6、继续下载。请看下图的抓包图-3。

      点击继续下载时,即再从新发送一个http请求给服务器。

      第77~79行是TCP连接(三次握手)

      第80行为发送http请求信息

      请看下面的http请求信息,这一次含有请求头Range,这是Web重要机制。在暂停下载的时候,浏览器会记住已经已经接受的字节数,待继续下载的时候,在构建http请求信息的时候会增加这一个重要的请求头信息。这也是支持断点续传的一个前提条件。

      浏览器携带Range头信息请求Web服务器,此时我们需要在代码层对这个重要信息进行处理。即取出该字节数出,然后在文件中定位指针,然后读文件开始续传。【这是断点续传应用中的逻辑关键】

    抓包图-3

      7、重复暂停一次,在继续下载,观察对比。暂停两次可以从抓包图-1中最右边可以看到两个红色的横线。

      8、最后下载成功啦,此时Web服务器会发送http响应信息给浏览器。

      第350行尾响应行

      看下面的http响应图,响应状态码为206

      用红色线标记的是我们代码中自定义的响应头

     抓包图-4

    http响应图

      9、TCP四次握手,端断开连接。看上面的抓包图-4

      第352~354是TCP断开连接。四次握手为什么是只有三次通讯呢?

      TCP断开具体也可以参考我之前写的文章。

      第一次,浏览器发送FIN包(表示要断开)、ACK(确认序列号)。seq=361 

      第二、三次,Web服务器接受到浏览器发来的包,并回复FIN包(我也要要断开)、ACK(确认序列号)。seq=174554、ack=362 【Web将浏览器发来的seq=361+1=362,转变成ack=362发给浏览器,表示我已经知道了】【此时浏览器并一起发送seq=174554,告诉浏览器说我要关闭连接啦】

      第四次,浏览器回复Web服务器,ack=174555 【浏览器将Web服务器发来的seq=174554+1,转变换成ack=174555发给Web主机,表示我已经知道了】

       TCP一直说是四次握手断开,我认为这应该是逻辑上的四次握手,从抓包上来看的话,第二、三次合并为一次通讯了。

      4.3、测试不支持断点续传下载

      执行步骤:

      1、打开抓包工具进行监控

      2、用火狐浏览器进行访问,Enter下载

      3、暂停下载

      4、继续下载。突然不行了,下载失败!为什么会这样呢!下面我们来分析分析

      抓包分析:

      1、TCP连接、http get请求无异常

     

      2、从抓包分析在断开前都无任何异常

      3、继续下载抓包分析

      TCP连接正常

      http请求信息,看上去是正常的,但是相对于我们所写的程序就不对劲了。请求信息中含有Range请求头,他需要的是数据该该Range范围内的,而我们程序定义的是非断点续传,即每次访问都是重写下载,因此Web传输的数据对不上浏览器之前的数据,最终出错啦!

     5、总结

      从学习OSI网络模型、TCP/IP网络模型到深入了解TCP传输、http协议、DNS查找、以及http URL访问具体细节步骤,最后到这个HTTP协议应用--断点续传,收获还是挺丰厚的。 以上是自己对断点续传的理解,以及做的相应测试,若有不对的地方,希望大家指出,好让我改正改正。

    (以上是自己的一些见解,若有不足或者错误的地方请各位指出)

     作者:那一叶随风   http://www.cnblogs.com/phpstudy2015-6/

     原文地址:http://www.cnblogs.com/phpstudy2015-6/p/6821478.html 

     声明:本博客文章为原创,只代表本人在工作学习中某一时间内总结的观点或结论。转载时请在文章页面明显位置给出原文链接

  • 相关阅读:
    Leetcode Reverse Words in a String
    topcoder SRM 619 DIV2 GoodCompanyDivTwo
    topcoder SRM 618 DIV2 MovingRooksDiv2
    topcoder SRM 618 DIV2 WritingWords
    topcoder SRM 618 DIV2 LongWordsDiv2
    Zepto Code Rush 2014 A. Feed with Candy
    Zepto Code Rush 2014 B
    Codeforces Round #245 (Div. 2) B
    Codeforces Round #245 (Div. 2) A
    Codeforces Round #247 (Div. 2) B
  • 原文地址:https://www.cnblogs.com/applelife/p/10476728.html
Copyright © 2011-2022 走看看