文件下载过程中被暂停或中断,恢复下载时如果重新下载整个文件,会消耗额外的网络流量及时间.断点续传,可以按照Client的需求,对文件进行分段下载,返回Client需要的文件内容,节省流量与时间.断点续传在 HTTP/1.1 (RFC2616)中得到支持.HTTP/1.0不支持断点续传.
当然,使用PHP做下载并不太好,这里仅做为讨论。
下面以resumedownload.txt纯文本为例,内容如下:
1234567890
1.Client:
在请求的http header中添加 Range: bytes=3-9表明需要获取文件的哪一个片段,值得注意的是,Range bytes是从0开始计数的,也就是说Range: bytes=0-0 表示请求第一个byte,即”1″
2.Server:
在响应的http header中添加HTTP/1.1 206 Partial Content,表明输出的为文件片段(partial就是部分,局部的意思!),
在响应的http header中添加Content-Range: bytes 0-9/10,表明输出的是哪一片段的文件.(其中0表示起始位置,9表示终止位置,最后面的10表示整个文件的大小)
下面是php实现(Github ResumeDownload):
<?php /** * A download resume class * * Client request header example: * Range: bytes=0-199 * * Sever usage: * $url = 'http://www.xxx.com/xxx.zip'; * $rd = new ResumeDownload($url); * $rd->download(); * * @author aiddroid * @date 2013-11-22 * @encoding UTF-8 */ class ResumeDownload { //file path private $filePath; //file name private $fileName; //file handler private $fileHandler; //the start of download private $rangeStart; //the end of download private $rangeEnd; //file size private $fileSize; private $log; /** * construction * @param type $filePath file path * @return type */ function __construct($filePath, $log = false) { $this->log = $log; $this->filePath = $filePath; //check if file exists and readable if (!is_file($this->filePath) || !($this->fileHandler = fopen($this->filePath, 'rb'))) { return $this->sendError(); } $this->fileName = basename($this->filePath); $this->fileSize = filesize($this->filePath); //get range value from http header $this->initDownloadRange(); } /** * resume download */ public function download() { $this->sendHeader(); $this->sendContent(); } /* * get range start and end from http header */ private function initDownloadRange() { preg_match("/^bytes=(\d?)-(\d*?)$/i", $_SERVER['HTTP_RANGE'], $match); $this->rangeStart = intval($match[1]); $this->rangeEnd = intval($match[2]); //log $this->log("$this->rangeStart $this->rangeEnd $this->fileSize", __FUNCTION__); //check range value if ($this->rangeStart < 0 || $this->rangeStart >= $this->fileSize || (!empty($this->rangeEnd) && $this->rangeEnd < $this->rangeStart) || $this->rangeEnd >= $this->fileSize) { return $this->sendError('HTTP/1.1 416 Requested Range Not Satisfiable'); } } /* * send output header */ private function sendHeader() { //clear php errors or warnings ob_clean(); header('Content-Length: ' . ($this->rangeEnd - $this->rangeStart)); if ($this->rangeStart > 0) { header('HTTP/1.1 206 Partial Content'); //Content-Range: bytes 21010-47021/47022 header('Content-Range: bytes ' . $this->rangeStart . '-' . $this->rangeEnd . '/' . $this->fileSize); } else { header('Accept-Ranges: bytes'); } header('Content-Type: application/octet-stream'); //for filename header('Content-Disposition: attachment;filename=' . $this->fileName); } /** * send real data */ private function sendContent() { //seek file point to read fseek($this->fileHandler, $this->rangeStart); //read file as range if (!empty($this->rangeEnd)) { //Range: bytes=0-0 -> read the first byte $content = fread($this->fileHandler, ($this->rangeEnd - $this->rangeStart + 1)); echo $content; return; } //file passthrough? fpassthru($this->fileHandler); } /** * send error */ private function sendError($error) { header($error ? $error : 'HTTP/1.0 404 Not Found'); } /** * log * @param type $content * @param type $logFile */ private function log($content, $logFile = '') { if ($this->log) { $logFile = empty($logFile) ? __FUNCTION__ : $logFile; $logFile = __CLASS__ . '.' . $logFile; error_log(date('Y-m-d H:i:s') . " $content \n", 3, $logFile . '.' . date('Ymd') . '.log'); } } }