上一篇简单介绍了下载器的功能https://developer.aliyun.com/article/779660?spm=a2c6h.13148508.0.0.27584f0eY3aLSm
本次主要是简要介绍一下下载器的内部实现,并不复杂,所以只抓重点来说。
源码地址 https://git@github.com:renzhenming/DownloadModel.git
断点续传
下载器的核心是下载,第二功能是断点续传,我们首先来说下断点续传的原理。断点续传就是从文件上次中断的地方开始重新下载或上传,主要是为了解决由于网络故障导致下载中断或者需要支持手动暂停后再次恢复下载的问题,如果每次出现中断都要重新开始下载的话,对于大文件下载上传是非常痛苦的,所以每次中断我们需要主动记录当前任务执行的位置,以便再次启动的时候从这个位置继续执行。
要实现断点续传需要哪些条件呢?
1.http协议
首先,http协议是支持断点续传的,关键在于他的协议头设置的Range,我们看代码。这个range的作用就是告诉服务器,我要的资源,请你从rangeStart 的位置开始返回给我,到rangeEnd的位置结束,此时服务器返回的responseCode就是206,表示部分资源返回
cn.setRequestProperty("Range", "bytes=" + rangeStart + "-" + rangeEnd);//告诉服务器请求资源的范围
2.RandomAccessFile
然后呢,服务器给我的是一个从半路截取的文件,我怎么保证这半截文件能和上次的半截文件拼装成一个完整的呢?这个时候就需要用到一种特殊的流
RandomAccessFile
这样一来问题比较明朗了,RandomAccessFile可以从指定位置访问文件,不正是和我们断点续传的要求契合了。下载一个资源,我们肯定要把他写入一个文件中,而当我们写着写着由于各种原因中断了,但是这个时候我们记录下中断的位置,等再次开启请求的时候,把这个位置通过Range传递给服务器,服务器从这个位置开始返回资源,我拿到之后,通过RandomAccessFile从文件的这个位置开始继续写入,就可以保证下载完成后,这是一个完整的文件。注意从指定位置写入后会将原有的内容替换掉
RandomAccessFile randomAccessFile2 = new RandomAccessFile(positionFile, "rwd");
//指定当前线程从哪个位置开始写入流
randomAccessFile.seek(lastDownloadIndex);
断点续传的核心就在于这两点
多线程下载
断点续传是多线程下载的基础,没有断点续传无法实现多线程下载
在下载一个文件的时候。通常的做法是开启一个线程。但是在有些场景下,这样无法最大的利用系统的硬件资源。比如在一个8核处理器的机器上下载一个10G大小的文件,如果开启一个线程的话,不仅效率慢,而且8核我们只利用了其中八分之一的产能,这种情况完全可以开启八个线程共同工作(理想状况,只有这一个任务),且不论这样说是否完全准确,但是原理上是说的通的
所以这个时候就需要实现多个线程下载一个文件了,既然多个线程下载一个文件,那就会有一个问题需要解决,如何防止两个线程下载的资源重复?
这就相当于8个人吃一个蛋糕,需要进行一下合理的分配才能防止争抢的问题。所以很简单,我们把这个蛋糕分成8份就可以了,至于是要平均分,还是不平均分,就不是很重要了,只要分成了8分,8个人就可以各自吃各自的
对于一个文件也是如此,我们把文件分成8份(平均分是一种简单的方式),分别交给8个线程去下载,边下载边写入指定位置,这样下载完成后得到的就是一个完整的文件
既然需要平分,那么就要知道资源大小,而一个服务端的资源,如果我们只有url的话,是不知道他有多大的,那么就要首先获取到长度
cn.getContentLength()
所以下载会分成两次网络请求,第一次请求获取到资源长度,第二次才开始从指定位置下载
拿到长度后开始分配资源
//根据资源的大小和线程数量计算每个线程下载文件的大小,还要计算每个线程下载的开始位置和结束位置。
int blockSize = fileTotalLength / threadCount;//每个线程下载的大小
//循环计算每个线程的开始位置和结束位置
LogUtils.d("开启" + threadCount + "个线程开始下载,每个线程需要下载的资源长度为:" + blockSize);
for (int threadId = 0; threadId < threadCount; threadId++) {
int startIndex = threadId * blockSize;//线程的开始下载位置
int endIndex = (threadId + 1) * blockSize - 1;//下载的结束位置
//计算最后一个线程的结束位置
if (threadId == threadCount - 1) {
endIndex = fileTotalLength - 1;//最后一个线程下载的结束位置
}
LogUtils.d("线程" + threadId + "下载的开始位置:" + startIndex + " 结束下载的位置:" + endIndex);
//开启这些线程去下载
DownloadThread downloadThread = new DownloadThread(downloadUrl, downloadPath,threadId,startIndex, endIndex, countDownLatch, downloadRequestTimeout);
downloadThreads.add(downloadThread);
new Thread(downloadThread).start();
}
有几个线程就分成几份,指定每个线程开始下载和结束下载的位置,下载过程中,通过RandomAccessFile指定文件的写入位置
//指定当前线程从哪个位置开始写入流
randomAccessFile.seek(lastDownloadIndex);
完成之后就可以得到一个完成的资源了