一、断点续传
所谓断点续传,即在文件传输过程中,由于主动或者被动原因中断了传输过程。下一次重新建立连接,不需要从头开始继续下载。这个流程就可以称之为断点续传。
将任务(一个文件或压缩包)人为的划分为一个或多个部分,每一个部分采用一个线程进行上传/下载,如果碰到网络故障,可以从已经上传/下载的部分开始继续上传/下载未完成的部分,而没有必要从头开始上传/下载。
二、断点续传的用途
有时用户上传/下载文件需要历时数小时,万一线路中断,不具备断点续传的 HTTP/FTP 服务器或下载软件就只能从头重传,比较好的 HTTP/FTP 服务器或下载软件具有断点续传能力,允许用户从上传/下载断线的地方继续传送,这样大大减少了用户的烦恼。
常见的支持断点续传的上传/下载软件:QQ 旋风、迅雷、快车、电驴、酷6、土豆、优酷、百度视频、新浪视频、腾讯视频、百度云等。
在 Linux/Unix 系统下,常用支持断点续传的 FTP 客户端软件是 lftp。
三、Range & Content-Range
HTTP1.1 协议(RFC2616)开始支持获取文件的部分内容,这为并行下载以及断点续传提供了技术支持。它通过在 Header 里两个参数实现的,客户端发请求时对应的是 Range ,服务器端响应时对应的是 Content-Range。
Range
用于请求头中,指定第一个字节的位置和最后一个字节的位置,一般格式:
Range:(unit=first byte pos)-[last byte pos]
Range 头部的格式有以下几种情况:
Range: bytes=0-499 表示第 0-499 字节范围的内容
Range: bytes=500-999 表示第 500-999 字节范围的内容
Range: bytes=-500 表示最后 500 字节的内容
Range: bytes=500- 表示从第 500 字节开始到文件结束部分的内容
Range: bytes=0-0,-1 表示第一个和最后一个字节
Range: bytes=500-600,601-999 同时指定几个范围
Content-Range
用于响应头中,在发出带 Range 的请求后,服务器会在 Content-Range 头部返回当前接受的范围和文件总大小。一般格式:
Content-Range: bytes (unit first byte pos) - [last byte pos]/[entity legth]
例如:
Content-Range: bytes 0-499/22400
0-499 是指当前发送的数据的范围,而 22400 则是文件的总大小。
HTTP/1.1 200 Ok(不使用断点续传方式)
HTTP/1.1 206 Partial Content(使用断点续传方式)
四、如何验证服务器支持 断点续传呢?
执行如下指令:
curl -I --range 0-9 http://www.baidu.com/img/bdlogo.gif
响应如下:
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Cache-Control: max-age=315360000
Connection: Keep-Alive
Content-Length: 10
Content-Range: bytes 0-9/1575
Content-Type: image/gif
Date: Mon, 22 Oct 2018 07:09:17 GMT
Etag: "627-4d648041f6b80"
Expires: Thu, 19 Oct 2028 07:09:17 GMT
Last-Modified: Fri, 22 Feb 2013 03:45:02 GMT
P3p: CP=" OTI DSP COR IVA OUR IND COM "
Server: Apache
Set-Cookie: BAIDUID=6D311DFAA45F7ED39571527EC3A6F50F:FG=1; expires=Tue, 22-Oct-19 07:09:17 GMT; max-age=31536000; path=/; domain=.baidu.com; version=1
注意到:
能够找到 Content-Range,则表明服务器支持断点续传。
有些服务器还会返回 Accept-Ranges,输出结果 Accept-Ranges: bytes ,说明服务器支持按字节下载。
五、code-demo
# -*- coding: utf-8 -*-
#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 4
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.
import urllib
import os
import sys
TARGET_URL = 'http://ufldl.stanford.edu/housenumbers/'
TARGET_FILE = 'test_32x32.mat'
file_total_size =64275384
class CustomURLOpener(urllib.FancyURLopener):
def http_error_206(self, url, fp, errcode, errmsg, headers, data=None):
pass
def resume_download():
file_exists = False
CustomURLClass = CustomURLOpener()
if os.path.exists(TARGET_FILE):
out_file = open(TARGET_FILE, "ab")
file_exists = os.path.getsize(TARGET_FILE)
# If the file exists, then only download the unfinished part
CustomURLClass.addheader("range", "bytes=%s-" % (file_exists))
print "bytes=%s-" % (file_exists)
else:
out_file = open(TARGET_FILE, "wb")
web_page = CustomURLClass.open(TARGET_URL + TARGET_FILE)
# Check if last download was OK
# FILE_LENGTH 文件大小
file_length = int(web_page.headers['Content-Length'])
if file_exists >= file_total_size:
loop = 0
print "File download %d Bytes, remain size %d Bytes need download" % (file_exists, file_length)
print "File already downloaded!"
exit(1)
else:
print "File download %d Bytes, remain size %d Bytes need download" %(file_exists, file_length)
byte_count = 0
while True:
data = web_page.read(4096)
if not data:
break
out_file.write(data)
byte_count = byte_count + len(data)
out_file.flush()
done = int((50*(byte_count + file_exists) / file_total_size))
sys.stdout.write("\r[%s%s] %d%%" % ('█' * done, ' ' * (50 - done), 100 * (byte_count + file_exists) / file_total_size))
sys.stdout.flush()
web_page.close()
out_file.close()
for k, v in web_page.headers.items():
print k, "=", v
print "File copied", byte_count, "bytes from", web_page.url
if __name__ == '__main__':
resume_download()