讲讲断点续传那点儿事

本篇文章已授权微信公众号 dasu_Android(大苏)独家发布

这次想来讲讲断点续传,以前没相关需求,所以一直没去接触,近阶段了解了之后,其实并不复杂,那么也便来写一篇记录一下,分享给大伙,也方便自己后续查阅。

提问

Q1:如果你的 app 需要下载大文件,那么是否有方法可以缩短下载耗时?

Q2:如果你的 app 在下载大文件时,程序因各种原因被迫中断了,那么下次再重启时,文件是否还需要重头开始下载?

Q3:你的 app 下载大文件时,支持暂停并恢复下载么?即使这两个操作分布在程序进程被杀前后。

理论基础

讲之前,先来通俗的解释下什么是断点续传

说得白一点,其实也就是下载文件时,不必重头开始下载,而是从指定的位置继续下载,这样的功能就叫做断点续传。

既然如此,那么要实现断点续传的关键点其实也就是两点:

  • 如何告知服务端,从指定的位置下载
  • 如何知道客户端想要的指定位置是多少

是吧,理论上来讲,当这两点都可以做到的时候,自然就可以实现断点续传了。那么,要如何做到呢?

其实,也很简单,并不需要我们自己去写一些什么,HTTP 协议本身就支持断点续传了,所以借助它就可以实现告知服务端,从指定位置下载的功能了。

而另一点,就更简单了,文件是下载到客户端设备上的,那么只要获取到这份下载到一半的文件,看一下它目前的大小,也就知道需要让服务端从哪开始继续下载了。

那么,下面就介绍一下涉及到的相关理论:

Range & Content-Length & Content-Range & If-Range

这些都是 HTTP 包中 Header 头部的一些字段信息,其中 Range 和 If-Range 是请求头中的字段,Content-Length 和 Content-Range 是响应头中的字段。

Range

当请求头中出现 Range 字段时,表示告知服务端,客户端下载该文件想要从指定的位置开始下载,至于 Range 字段属性值的格式有以下几种:

格式 含义
Range:bytes=0-500 表示下载从0到500字节的文件,即头500个字节
Range:bytes=501-1000 表示下载从500到1000这部分的文件,单位字节
Range:bytes=-500 表示下载最后的500个字节
Range:bytes=500- 表示下载从500开始到文件结束这部分的内容

当 app 想实现缩短大文件的下载耗时,可以开启多个下载线程,每个线程只负责文件的一部分下载,当所有线程下载结束后,将每个线程下载的文件按顺序拼接成一个完整的文件,这样就可以达到缩短下载大文件的耗时目的了。

那么,此时,就可以使用 Range:bytes=501-1000 这种格式了,每个线程在各自的请求头字段中,以这种格式加入相对应的信息即可达到目的了。

如果 app 想实现断点续传,文件下载到一半被迫中断,下次启动还可以继续接着上次进度下载时,那么此时可以使用 Range:bytes=500- 这种格式了,只要先获取本地那份文件目前的大小,通过在请求头中加入 Range 字段信息即可。

Content-Length

Content-Length 字段出现在响应头中,用于告知客户端此次下载的文件大小。

一般,如果客户端需要实现下载进度实时更新时,就需要知道文件的总大小和目前下载的大小,后者可以通过对本地文件的操作得知,前者一般就是通过响应头中的 Content-Length 字段得知。

另外,如果想要实现多线程同时分段下载大文件功能时,显然在下载前,客户端需要先知道文件总大小,才可以做到动态进行分段,因此一般在下载前都会先发送一个不需要携带 body 信息请求,用于先获取响应头中的 Content-Length 字段来得知文件总大小。

但有一点需要注意:Content-Length 只表示此链接中下载的文件大小

什么意思,也就是说,如果这条链接是一次性将整个文件下载下来的,那么 Content-Length 就表示这个文件的总大小。

但,如果这条链接指定了 Range,表明了只是下载文件的指定部分的内容,那么此时 Content-Length 表示的就只是这一部分的大小。

所以,如果客户端实现了下载进度实时更新功能时,需要注意一下。因为如果文件是断点续传的,那么进度条的分母就不能用每次 HTTP 链接中的 Content-Length。要么下载前先发一条获取用于文件总大小的请求,然后一直维护着这个数据,要么就使用 Content-Range 字段。

Content-Range

Content-Range 字段也是出现在响应头中,用于告知客户端此链接下载的文件是哪个部分的,以及文件的总大小。

比如,当客户端在请求头中指定了 Range:bayes=501-1000 来下载一个总大小为 2000 字节文件的中间一部分内容时,此时,响应头中的 Content-Range 字段信息如下:

Content-Range:bytes 501-1000/2000

斜杠前表示此链接下载的文件是哪一部分,斜杠后表示文件的总大小。

If-Range

断点续传,说白点也就是分多次下载,既然不是一次性下载,那么就无法保证多次下载的间隔。

也就是说,有可能出现这种场景,这次由于某些原因只下载的一部分,而下次重启继续下载,但可能等到过了很多天后才重启去继续下载,如果在这期间,服务端的这份文件更新了怎么办?

只要不是一次性下载的,那么就有可能会出现这种场景,显然,这时候,就不希望断点续传了,而是要让客户端直接重头开始下载,毕竟文件都已经发生更新了,不是同一份了,再继续恢复下载也没有什么意义。

那么,客户端要如何知道服务端的文件是否发生变化,要重头下载呢?

这时就可以结合 If-Range 字段来实现了,这个也是在请求头中的字段,跟 Range 字段一起使用,它的作用是给 Range 字段生效设置了一些条件,只有满足这些条件,Range 才能生效。

也就是说,只有先满足 If-Range,那么才能通过 Range 来实现断点续传。

那它的条件值可以设置为哪些呢?有两种,Last-Modified 或者 ETag,这两个也都是响应头中的字段。

具体可以参考这篇文章:MDN If-Range

抓包示例

以上就是断点续传相关的理论基础,下面抓个包,看看请求头和响应头中的信息,来总结一下理论基础。

断点续传.png

首先先发起一个请求,设置了不携带 BODY 信息,这样就可以在下载前先获取到文件的总大小。至于怎么设置不携带 BODY 信息,不同的网络框架不同,具体下节代码示例中说明。

断点续传2.png

这是下载中断后,重启想要继续下载时发起的请求信息,请求头中指定了 Range:bytes=12341380- 表示本地已经下载了这么多,需要从这里开始继续往下下载。

响应头中返回了这部分的内容,并在 Content-Length 和 Content-Range 字段中给出了相关信息。

代码示例

理论基础掌握了,那么下面就是来看看代码怎么实现。不管用什么语言,使用了什么网络框架,要写的代码都有两个部分:

  • 文件处理操作
  • 添加请求头信息操作

文件处理操作有两个关键点,一是获取文件大小,二是以追加的方式写文件。添加请求头的操作则是参考各自网络框架的指示即可。

下面介绍了三种示例,分别是 C++&libcurl,Android&HttpURLConnection,Android&OkHttp。&前面是语言,后面是所使用的网络框架。

C++&libcurl

//引入libcurl库
#include <curl\curl.h>
#pragma comment(lib,"libcurl.lib") 
//文件操作库
#include <sys/stat.h>
#include <fstream>

char* mLocalFilePath;//下载到本地的文件

//获取已下载部分的大小,如果没有则返回0
curl_off_t getLocalFileLength()
{
    curl_off_t ret = 0;
    struct stat fileStat;
    ret = stat(mLocalFilePath, &fileStat);
    if (ret == 0)
    {
        return fileStat.st_size;//返回本地文件已下载的大小
    }
    else
    {
        return 0;
    }
}

//下载前先发送一次请求,获取文件的总大小
double getDownloadFileLength()
{
    double rel = 0, downloadFileLenth = 0;
    CURL *handle = curl_easy_init();
    curl_easy_setopt(handle, CURLOPT_URL, mDownloadFileUrl);
    curl_easy_setopt(handle, CURLOPT_HEADER, 1);    //只需要header头
    curl_easy_setopt(handle, CURLOPT_NOBODY, 1);    //不需要body
    if (curl_easy_perform(handle) == CURLE_OK) {
        curl_easy_getinfo(handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &downloadFileLenth);
    }
    else {
        downloadFileLenth = -1;
    }
    rel = downloadFileLenth;
    curl_easy_cleanup(handle);
    return rel;
}

//文件下载
CURLcode downloadInternal()
{
    //1. 获取本地已下载的大小,有则断点续传
    curl_off_t localFileLenth = getLocalFileLength();
    //2. 以追加的方式写入文件
    FILE *file = fopen(mLocalFilePath, "ab+");
    CURL* mHandler = curl_easy_init();
    if (mHandler && file)
    {
         //3. 设置url
        curl_easy_setopt(mHandler, CURLOPT_URL, mDownloadFileUrl);
        //4. 设置请求头 Range 字段信息,localFileLength 不等于0时,值大小就表示从哪开始下载 
        curl_easy_setopt(mHandler, CURLOPT_RESUME_FROM_LARGE, localFileLenth);
        
        //5. 设置接收数据的处理函数和存放变量
        curl_easy_setopt(mHandler, CURLOPT_WRITEFUNCTION, writeFile);
        curl_easy_setopt(mHandler, CURLOPT_WRITEDATA, file);
        // 6. 发起请求
        CURLcode rel = curl_easy_perform(mHandler);
        fclose(file);
        return rel;
    }
    curl_easy_cleanup(mHandler);
    return CURLE_FAILED_INIT;
}

writeFile 函数和下载进度通知的函数我都没贴,用过 libcurl 的应该都知道怎么写,或者网上搜一下,资料很多。上面就是将断点续传的几个关键函数贴出来,理清楚了即可。

Android&HttpURLConnection

Android&OkHttp

由于最近都在忙 C++ 的项目了,Android 暂时还没时间自己写个 demo 测试一下,所以先给几篇网上找的链接占个坑,后续抽个时间自己再来写个 demo。

之所以列了这两点,是因为感觉目前 Android 中网络框架大多都是用的 OkHttp 了,而下载文件还有很多都是用的 HttpURLConnection,所以这两个都想研究一下,怎么写断点续传。

Android多线程断点续传下载

Android使用OKHttp3实现下载(断点续传、显示进度)

两篇我都有大概过了下,其实断点续传原理不难,真的蛮简单的,所以实现上基本也大同小异,就是不同的网络框架的 api 用法不同而已。以及,如何维护本地已下载文件的大小的思路,有的是直接去获取文件对象查看,有的则是手动自己建个数据库维护。


大家好,我是 dasu,欢迎关注我的公众号(dasuAndroidTv),如果你觉得本篇内容有帮助到你,可以转载但记得要关注,要标明原文哦,谢谢支持~


dasuAndroidTv2.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • 网络下载是我们在项目中经常要用到的功能,如果是小文件的下载,比如图片和文字之类的,我们可以直接请求源地址,然后一次...
    liang_1阅读 2,433评论 0 53
  • API定义规范 本规范设计基于如下使用场景: 请求频率不是非常高:如果产品的使用周期内请求频率非常高,建议使用双通...
    有涯逐无涯阅读 2,521评论 0 6
  • 我们在网页开发过程中经常会有打印页面的需求,通过JS来实现的方法有很多,这里我做了一个整理,供大家参考。 方式一:...
    m_gyf阅读 15,112评论 6 27
  • 目前,国内智慧社区的建设还处于初级阶段,智能化建设主要围绕社区出行、安防等领域展开。智能门锁将从出入口控制入手,成...
    栖雲社区阅读 309评论 0 0
  • 时光荏苒,日月如梭。在众多不公平中上天分给每人的时间也算是公平的,时间不会因为你的身份地位而改变,每个人...
    ef1fdaa714eb阅读 253评论 0 0