一、断点续传

    所谓断点续传,即在文件传输过程中,由于主动或者被动原因中断了传输过程。下一次重新建立连接,不需要从头开始继续下载。这个流程就可以称之为断点续传。

将任务(一个文件或压缩包)人为的划分为一个或多个部分,每一个部分采用一个线程进行上传/下载,如果碰到网络故障,可以从已经上传/下载的部分开始继续上传/下载未完成的部分,而没有必要从头开始上传/下载

二、断点续传的用途

有时用户上传/下载文件需要历时数小时,万一线路中断,不具备断点续传的 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()