断点续传
思考:
1.断点续传,就是当一次下载任务由于某些因素中断下载,能够继续上次下载的位置继续下载,不必在从头开始。
2.支持多线程,就是把一个下载任务分配给多个线程去下载,每一个线程只下载一部分内容。
分析:
http协议,一次下载请求,就是请求远端服务器一个资源(url),建立TCP链接,把服务器资源写入本地的一个过程。
那么如果要实现断点续传的必要条件:
1.我们能够知道下载资源的大小(多少字节)
2.我们可以请求某一部分资源(一个文件的某一段字节数据)
3.服务器告诉我们返回的资源总大小和偏移量(从某一位置开始返回数据)
查找http协议规范
http/1.1请求头中有下面相关字段
请求头
GET /file.zip HTTP/1.1
Range:bytes=680- // 从某一个位置开始请求
...
响应头
HTTP/1.1 206 Partial Content
Accept-Ranges : bytes ///告诉客户端,我们是支持断点传输的
Content-Length : //返回文件总长度(少字节)
Content-Range :bytes //返回的字节范围
...
模拟一次单线程断点续传:
1.客户端发起请求,下载file.zip
GET /file.zip HTTP/1.1
2.服务器收到请求返回
HTTP/1.1 200 OK
Accept-Ranges : bytes ///告诉客户端,我们是支持断点传输的
Content-Length : 2000 //返回文件总长度(2000字节)
...
3.客户端在下载到600字节的时候突然间中断了下载链接,然后再次发起续传请求
GET /file.zip HTTP/1.1
Range:bytes=600- // 告诉服务器我从600字节开始下载数据
...
4.服务器收到请求给予回应
HTTP/1.1 206 Partial Content // 注意响应码206,不是200
Accept-Ranges : bytes ///告诉客户端,我们是支持断点传输的
Content-Length : 1400//返回剩余未下载文件总长度
Content-Range :bytes 600-1399//返回的字节范围
...
多线程的下载其实就是,在第一次请求只拿到要下载文件的总大小,然后计算分配给几个线程,每个线程具体下载哪一段数据,然后在去请求对应的数据。
代码设计与实现:
相关技术介绍:
1.HttpURLConnection
URL url = new URL(mFileInfo.getUrl());//请求的链接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();//打开链接
conn.setConnectTimeout(5 * 1000);//设置超时时间
conn.setRequestMethod("GET");//设置请求方法
conn.setRequestProperty("Range", "bytes=" + start + "-" +end);// 设置下载文件开始到结束的位置
int code = conn.getResponseCode();//拿到响应码
conn.getInputStream();// 拿到输入流
2.RandomAccessFile
RandomAccessFile是不属于InputStream和OutputStream类系的。实际上,除了实现DataInput和DataOutput接口之外(DataInputStream和DataOutputStream也实现了这两个接口),它和这两个类系毫不相干,甚至都没有用InputStream和OutputStream已经准备好的功能;它是一个完全独立的类,所有方法(绝大多数都只属于它自己)都是从零开始写的。这可能是因为RandomAccessFile能在文件里面前后移动,所以它的行为与其它的I/O类有些根本性的不同。总而言之,它是一个直接继承Object的,独立的类。
http://www.android-doc.com/reference/java/io/RandomAccessFile.html
void seek(long offset)//移动到某一位置,多线程下载(每个线程写入位置都不同)
代码设计思路(多线程下载支持断点续传)
1.使用数据库来存储每一个线程信息的下载信息,(线程id,下载的url,起始下载位置,终止下载位置,当前下载进度)
2.第一次请求的时候,计算好每一个线程需要下载的数据块,存入数据库,并在每一次写入数据时,修改进度
3.还原下载的时候,从数据库拿到这些信息,接着未完成的请求。
4.如果当前线程完成自己的任务,就从数据库里面移除
核心代码
获取文件大小
HttpURLConnection conn = null;
RandomAccessFile raf = null;
try {
URL url = new URL(mFileInfo.getUrl());
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5 * 1000);
conn.setRequestMethod("GET");
int code = conn.getResponseCode();
int length = -1;
if (code == HttpURLConnection.HTTP_OK) {
length = conn.getContentLength();
}
//如果文件长度为小于0,表示获取文件失败,直接返回
if (length <= 0) {
return;
}
// 判断文件路径是否存在,不存在创建
File dir = new File(DownloadPath);
if (!dir.exists()) {
dir.mkdir();
}
// 创建本地文件
File file = new File(dir, mFileInfo.getFileName());
raf = new RandomAccessFile(file, "rwd");
raf.setLength(length);
// 设置文件长度
mFileInfo.setLength(length);
// 将FileInfo对象传给Handler
Message msg = Message.obtain();
msg.obj = mFileInfo;
msg.what = MSG_INIT;
mHandler.sendMessage(msg);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.disconnect();
}
try {
if (raf != null) {
raf.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
下载线程任务
HttpURLConnection conn = null;
RandomAccessFile raf = null;
InputStream is = null;
try {
URL url = new URL(mFileInfo.getUrl());
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5 * 1000);
conn.setRequestMethod("GET");
long start = threadInfo.getStart() + threadInfo.getFinished();
// 设置下载文件开始到结束的位置
conn.setRequestProperty("Range", "bytes=" + start + "-" + threadInfo.getEnd());
File file = new File(DownloadService.DownloadPath, mFileInfo.getFileName());
raf = new RandomAccessFile(file, "rwd");
// 设置文件偏移量
raf.seek(start);
mFinished += threadInfo.getFinished();
Log.d("xzq","下载起始位置 = "+start+", mFinished = "+mFinished);
Intent intent = new Intent();
intent.setAction(DownloadService.ACTION_UPDATE);
int code = conn.getResponseCode();
if (code == HttpURLConnection.HTTP_PARTIAL) {
is = conn.getInputStream();
byte[] bt = new byte[1024];
int len = -1;
while ((len = is.read(bt)) != -1) {
raf.write(bt, 0, len);
// 累计整个文件完成进度
mFinished += len;
// 累加每个线程完成的进度
threadInfo.setFinished(threadInfo.getFinished() + len);
if (mIsPause) {
mDao.updateThread(threadInfo.getUrl(), threadInfo.getId(), threadInfo.getFinished());
return;
}
}
}
// 标识线程是否执行完毕
isFinished = true;
// 判断是否所有线程都执行完毕
checkAllFinished();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.disconnect();
}
try {
if (is != null) {
is.close();
}
if (raf != null) {
raf.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}