分类: LINUX
2009-07-13 12:45:16
Author:FinalBSD
Date:2008-07-08
之前讲Etag的时候都只是对断点续传做了最简单的说明,没有深入研究。今天研究了一会,希望可以回答Laurence的问题,呵呵:)
1.断点续传概念
断点续传的理解可以分为两部分:一部分是断点,一部分是续传。
断点的由来是在下载过程中,将一个下载文件分成了多个部分,同时进行多个部分一起的下载,当某个时间点,任务被暂停了,此时下载暂停的位置就是断点了。
续传就更好理解些了,就是当一个未完成的下载任务再次开始时,会从上次的断点继续传送。
注:以上这短话来自迅雷网站的说明。
2.HTTP/1.1和断点续传
HTTP1.1的描述中有几个东西可以支持断点续传
2.1 If-Range
If-Range是另一个起条件判断的请求头(我们之前讲过If-Match/If-None-Match,If-Modified-Since/If-Unmodified-Since).If-Range头用来避免客户端在下载了某资源(比如图片)的一部分后,下次重新下载又从头开始下载。(这对于某些慢速网络来说,也许一辈子也下载不了某完整文件)。使用If-Range之后,客户端每次可以从上次下载的部分之后继续开始下载。
If-Range的使用格式为:If-Range: Etag|Http-Date
也就是说If-Range后面可以使用Etag或者Last-Modified返回的值:
If-Range: "df6b0-b4a-3be1b5e1"
If-Range: Tue, 8 Jul 2008 05:05:56 GMT
逻辑上来讲,上面2种方式分别和If-Match,If-Unmodified-Since的工作原理一样,他们的值正是服务器返回的Etag和Last-Modified值。
2.2 Range
Range是另外一个Request Header,也就是由客户端发给服务器的,如果没有发送,客户端发送的If-Range会被忽略。同样,如果服务器端根本不支持断点续传,If-Range也一样被忽略。总之,离开了Range,If-Range的存在没有任何意义。
2.3 Accept-ranges
Accept-ranges是一个响应头,服务器发送这个头来告诉客户端它支持Range(范围)请求,后面的值,可以是bytes那么客户端发回来的:Ranges: bytes=2400- 就表示请求2400字节到文件末尾的这些数据。
2.4 Content-Range
这是一个响应头,说明了服务器提供的内容的字节范围和整个资源的长度。
3.工作方式(这部分来自网络上的文章)
下面的代码显示了IIS发送给客户端的用于响应一个初始下载请求的一些头信息,他向客户端传递了被请求的文档的周详信息。
HTTP/1.1 200 OK Connection: close Date: Tue, 19 Oct 2004 15:11:23 GMT Accept-Ranges: bytes Last-Modified: Sun, 26 Sep 2004 15:52:45 GMT ETag: "47febb2cfd76c41:2062" Cache-Control: private Content-Type: application/x-zip-compressed Content-Length: 2844011 |
在接收这些头信息之后,假如下载被中断了,IE浏览器在后来的下载请求中会把Etag值和Range头信息发送回服务器。下面的代码显示了尝试恢复被中断下载时IE发送给服务器的一些头信息。
GET HTTP/1.0 Range: bytes=822603- Unless-Modified-Since: Sun, 26 Sep 2004 15:52:45 GMT If-Range: "47febb2cfd76c41:2062" |
请注意,If-Range 元素包含服务器可用于标识要重新发送的文件的原始 ETag 值。您还会看到 Unless-Modified-Since 元素包含了最初下载的开始日期和时间。服务器将利用此信息来确定自最初下载开始后该文件是否已被修改过。如果已被修改,则服务器将从头开始重新下载。这些头信息表明IE缓存了IIS提供的实体标签,并在If-Range头信息中把他发送回服务器了,这是确保下载从准确相同的文档恢复的一种途径。不幸的是,并非任何的浏览器的工作方式都相同。客户端发送的用于验证文档的其他HTTP头信息可能是If-Match、If-Unmodified-Since或Unless-Modified-Since。很明显,该规范对于客户端软件必须支持哪些头信息,或必须使用哪些头信息没有明确的规定。因此,有些客户端根本就没有使用头信息,而IE只使用If-Range和Unless-Modified-Since。您最好用代码检查这些信息。采用这种方式的时候,您的应用程式能够在很高的层次遵循HTTP规范,并能够使用多种浏览器。Range头信息指明了被请求的字节范围--在例子中他是服务器应该恢复文档流的起始点。
当IIS接收到恢复下载的请求类型时,他发回包含下面的头信息的响应信息:
HTTP/1.1 206 Partial Content Content-Range: bytes 822603-2844010/2844011 Accept-Ranges: bytes Last-Modified: Sun, 26 Sep 2004 15:52:45 GMT ETag: "47febb2cfd76c41:2062" Cache-Control: private Content-Type: application/x-zip-compressed Content-Length: 2021408 |
请注意上面的代码和最初的下载请求的HTTP响应有点差别--恢复下载的请求是206而最初下载的请求是200。这表明通过线路传递进来的内容是部分文档。这一次Content-Range头信息指出了被传递字节的精确数量和位置。
IE对于这些头信息是很挑剔的。假如最初的响应没有包含Etag头信息,IE永远不会尝试恢复下载。我测试过的其他客户端不使用ETag头信息,他们简单得依赖于文档名、请求范围,并使用Last-Modified头信息(假如他们试图验证该文档)。
图片引用:(感谢i_amok )
深入了解HTTP协议
前面的部分中显示的头信息对于使恢复下载的解决方案运行来说是足够的,但是他没有完全覆盖HTTP规范。 在单个请求中,Range头信息能够询问多个范围,这种特性称为"多部分范围(multipart ranges)"。请不要和分段下载(segmented downloading)混淆,几乎任何的下载工具都使用分段下载来提高下载速度。这些工具声称通过打开两个或多个并发的连接(每个连接请求文档的不同范围)提高了下载速度。 多部分范围的想法并没有开启多个连接,但是他能够使客户端软件能够在单个请求/响应周期中请求某个文档的最前面的十个和最后面的十个字节。
诚实地说,我从来都没有找到使用这种特性软件片断。但是我拒绝在代码声明中写入"他并不是完全的HTTP兼容的"。略去这个特性必定会触犯墨菲法则(Murphy's Law)。无论如何,多部分范围还是被用于电子邮件传输中,把头信息、普通文本和附件分开。
4.PHP的实现(这部分来自:)
--------------------------------
/**
* 发送文件
*
* @author: legend()
* @link:
* @description: send file to client
* @version: 1.0
*
* @param string $fileName 文件名称或路径
* @param string $fancyName 自定义的文件名,为空则使用filename
* @param boolean $forceDownload 是否强制下载
* @param integer $speedLimit 速度限制,单位为字节,0为不限制,不支持windows服务器
* @param string $$contentType 文件类型,默认为application/octet-stream
*
* @return boolean
*/
function sendFile($fileName, $fancyName = '', $forceDownload = true, $speedLimit = 0, $contentType = '')
{
if (!is_readable($fileName))
{
header("HTTP/1.1 404 Not Found");
return false;
}$fileStat = stat($fileName);
$lastModified = $fileStat['mtime'];
$md5 = md5($fileStat['mtime'] .'='. $fileStat['ino'] .'='. $fileStat['size']);
$etag = '"' . $md5 . '-' . crc32($md5) . '"';header('Last-Modified: ' . gmdate("D, d M Y H:i:s", $lastModified) . ' GMT');
header("ETag: $etag");
if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) >= $lastModified)
{
header("HTTP/1.1 304 Not Modified");
return true;
}if (isset($_SERVER['HTTP_IF_UNMODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_UNMODIFIED_SINCE']) < $lastModified)
{
header("HTTP/1.1 304 Not Modified");
return true;
}if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] == $etag)
{
header("HTTP/1.1 304 Not Modified");
return true;
}if ($fancyName == '')
{
$fancyName = basename($fileName);
}
if ($contentType == '')
{
$contentType = 'application/octet-stream';
}$fileSize = $fileStat['size'];
$contentLength = $fileSize;
$isPartial = false;if (isset($_SERVER['HTTP_RANGE']))
{
if (preg_match('/^bytes=(\d*)-(\d*)$/', $_SERVER['HTTP_RANGE'], $matches))
{
$startPos = $matches[1];
$endPos = $matches[2];if ($startPos == '' && $endPos == '')
{
return false;
}
if ($startPos == '')
{
$startPos = $fileSize - $endPos;
$endPos = $fileSize - 1;
}
else if ($endPos == '')
{
$endPos = $fileSize - 1;
}$startPos = $startPos < 0 ? 0 : $startPos;
$endPos = $endPos > $fileSize - 1 ? $fileSize - 1 : $endPos;$length = $endPos - $startPos + 1;
if ($length < 0)
{
return false;
}$contentLength = $length;
$isPartial = true;
}
}
// send headers
if ($isPartial)
{
header('HTTP/1.1 206 Partial Content');
header("Content-Range: bytes $startPos-$endPos/$fileSize");
}
else
{
header("HTTP/1.1 200 OK");
$startPos = 0;
$endPos = $contentLength - 1;
}header('Pragma: cache');
header('Cache-Control: public, must-revalidate, max-age=0');
header('Accept-Ranges: bytes');
header('Content-type: ' . $contentType);
header('Content-Length: ' . $contentLength);
if ($forceDownload)
{
header('Content-Disposition: attachment; filename="' . rawurlencode($fancyName). '"');
}header("Content-Transfer-Encoding: binary");
$bufferSize = 2048;if ($speedLimit != 0)
{
$packetTime = floor($bufferSize * 1000000 / $speedLimit);
}$bytesSent = 0;
$fp = fopen($fileName, "rb");
fseek($fp, $startPos);//fpassthru($fp);
while ($bytesSent < $contentLength && !feof($fp) && connection_status() == 0 )
{
if ($speedLimit != 0)
{
list($usec, $sec) = explode(" ", microtime());
$outputTimeStart = ((float)$usec + (float)$sec);
}$readBufferSize = $contentLength - $bytesSent < $bufferSize ? $contentLength - $bytesSent : $bufferSize;
$buffer = fread($fp, $readBufferSize);echo $buffer;
ob_flush();
flush();$bytesSent += $readBufferSize;
if ($speedLimit != 0)
{
list($usec, $sec) = explode(" ", microtime());
$outputTimeEnd = ((float)$usec + (float)$sec);
$useTime = ((float) $outputTimeEnd - (float) $outputTimeStart) * 1000000;
$sleepTime = round($packetTime - $useTime);
if ($sleepTime > 0)
{
usleep($sleepTime);
}
}
}
return true;
}
-------------------