多线程断点续传下载¶
多线程下载意义¶
在日常的场景下,网络中不可能只有下载方与服务器之间这样一条连接,为了避免在这样的场景下的网络拥塞,TCP 协议通过调节窗口的大小来避免出现拥塞,但这个窗口的大小可能没办法达到我们预期的效果:充分利用我们的带宽。因此我们可以采用多个 TCP 连接的形式来提高我们带宽的利用率,从而加快下载速度。
任务分配¶
前面提到了我们主要的目的是将一个总的下载任务分摊到多个子任务中,比如假设我们用 5 个线程下载这个文件,那么我们就可以对一个长度为 N 的任务进行如下图的均分:
但真实场景下往往 N 都不是刚好为 5 的倍数的,因此对于最后一个任务还需要加上剩余的任务量,也就是 N/5+N%5。
Http Range 请求头¶
我们如何实现向服务器只请求这个文件的某一段而不是全部呢?
我们可以通过在请求头中加入 Range 字段来指定请求的范围,从而实现指定某一段的数据。
如:RANGE bytes=10000-19999
就指定了 10000-19999 这段字节的数据
所以我们的核心思想就是通过它拿到文件对应字节段的 InputStream,然后对它读取并写入文件。
RandomAccessFile 文件写入¶
由于我们是多线程下载,因此文件并不是每次都是从前往后一个个字节写入的,随时可能在文件的任何一个地方写入数据。因此我们需要能够在文件的指定位置写入数据。这里我们用到了RandomAccessFile
来实现这个功能。
RandomAccessFile
是一个随机访问文件类,同时整合了 FileOutputStream
和 FileInputStream
,支持从文件的任何字节处读写数据。通过它我们就可以在文件的任何字节处写入数据。
我们对于每个子任务来说都有一个开始和结束的位置。每个任务都可以通过 RandomAccessFile::seek
跳转到文件的对应字节位置,然后从该位置开始读取 InputStream
并写入。
这样,就实现了不同线程对文件的随机写入。
文件大小的获取¶
由于我们在真正开始下载之前,我们需要先将任务分配到各个线程,因此我们需要先了解到文件的大小。
为了获取到文件的大小,我们用到 Response Headers
中的 Content-Length
字段。
如下图所示,可以看到,打开该下载请求的链接后,Response Headers
中包含了我们需要的 Content-Length
,也就是该文件的大小,单位是字节。
断点续传¶
对于多个子任务,我们如何实现它们的断点续传呢?
其实原理很简单,只需要保证每个子任务的下载进度能够被即时地记录即可。这样继续下载时只需要读取这些下载记录,从上次下载结束的位置开始下载即可。
它的实现有很多方式,只要能做到数据持久化即可。这里我使用的是数据库来实现。
这样,我们的子任务需要拥有一些必要的信息
completedSize
:当前下载完成大小taskSize
:子任务总大小startPos
:子任务开始位置currentPos
:子任务进行到的位置endPos
:子任务结束位置
通过这些信息,我们就能够记录子任务的下载进度从而恢复我们之前的下载,实现断点续传。
代码实现¶
可参考原文:Android多线程断点续传下载原理及实现