文件在下载的过程中,手动暂停或异常时,下载被中断,此时如果需要恢复下载,又不想重新下载的话,那么就需要实现断点续传了,断点续传的意思就是,恢复下载时,文件从被中断的位置继续下载,而无需重新将文件重新下载,最大的好处便是节省时间以及网络产生的流量了。要实现文件下载的断点续传,就必须首先需要明白Http断点续传的原理。
Http请求头Range是断点续传的核心。
什么是Range
当用户在听一首歌的时候,如果听到一半(网络下载了一半),网络断掉了,用户需要继续听的时候,文件服务器不支持断点的话,则用户需要重新下载这个文件。而Range支持的话,客户端应该记录了之前已经读取的文件范围,网络恢复之后,则向服务器发送读取剩余Range的请求,服务端只需要发送客户端请求的那部分内容,而不用整个文件发送回客户端,以此节省网络带宽。
HTTP1.1规范的Range的约定
服务端通过请求头Range:bytes=start-end来判断是否做Range请求,如果这个值存在并且有效,将返回206的响应码给客户端,告知这个请求支持断点续传。Range指定的是一个闭合区间范围(也可以是多个区间范围,不需要连续),Range请求头的格式规范如下:
Range: bytes = start-end,start2-end2,start3-end3,...,startN-endN
start-end 是一个闭合区间,即包含start到end的部分,所以下一个请求应该以end+1开头
例如:
Range: bytes=0-100 (0到100字节的数据)
Range: bytes=40- (40字节以后的数据)
Range: bytes=-500(最后一个500字节的数据)
Range: bytes=0-0,-1 (第一个和最后一个字节)
Range: bytes=500-600,800-999 (同时指定两个范围)
响应头
Content-Range:bytes 0-100/3103
服务器响应了前(0-100)个字节的数据,该资源一共有(3103)个字节大小。
Content-Length:101
表示这次请求,服务器响应了101个字节数据,我们通常也通过Content-Length去获取整个资源文件的大小,作为分段下载的基础
以HttpUrlConnection为例
URL url = new URL(fileUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(10 * 1000);
connection.setRequestProperty("Accept-Ranges", "bytes");
connection.setRequestProperty("Connection", "Keep-Alive");
//加上这个头部,则可以防止出现getContentLength()为-1的问题
connection.setRequestProperty("Accept-Encoding", "identity");
//必须加这个头部,否则无法返回正常支持断点续传的响应码206,这里取值范围是从第一个字节到结束,所以是请求的是整个资源文件
connection.setRequestProperty("Range", "bytes=0-");
connection.connect();
//假如此请求支持断点续传的方式将返回206响应码,否则资源请求成功的时候返回正常的200
int code = connection.getResponseCode() ;
//获取本次服务器响应的字节长度
long fileLength = connection.getContentLength();
实现多线程断点续传的基本思路
RandomAccessFile
多线程如何操作同一个文件,分段写入呢?了解一下RandomAccessFile
RandomAccessFile类的主要功能是完成随机读取功能,可以读取指定位置的内容。
之前的File类只是针对文件本身进行操作的,而如果要想对文件内容进行操作,则可以使用RandomAccessFile类,此类属于随机读取类,可以随机读取一个文件中指定位置的数据
构造方法
public RandomAccessFile(File file, String mode)throws FileNotFoundException
public RandomAccessFile(String name, String mode) throws FileNotFoundException
mode 文件的打开模式
r:读模式
w:只写
rw:读写,如果使用此模式,如果此文件不存在,则会自动创建。
指定读写的位置
randomAccessFile.seek(startIndex);
HTTP请求头Range
首先,访问服务器资源,获取资源文件的长度,即ContentLength,然后将ContentLength进行划分,假设ContentLength=1000,开辟4个线程去分段下载,每段下载250个字节,那么每个线程的Range请求头部应该为
Thread1 Range:bytes=0-249
Thread2 Range:bytes=250-499
Thread3 Range:bytes=500-749
Thread4 Range:bytes=750-999
区间范围由自己定义即可,不一定非要按等分
当每个线程开始分段下载,访问RandomAccessFile的开始位置就应该是首字节的位置,如Thread2应该如下设置
//Thread2
randomAccessFile.seek(250);
手动停止或出现中断时,应该保存上次写入的最后一个字节位置,以作为下一次请求的开始。以上面作为例子,假设线程Thread2中断时,写入的最后一个字节的位置为350,那么恢复下载的时候,Thread2请求头就应该为
Range:bytes=351-499
此时Thread2访问RandomAccessFile的开始位置应该是351,即
//Thread2
randomAccessFile.seek(351);