Android多线程断点续传下载

最近项目里用到了断点续传下载,所以在博客里做个记录,便于以后回顾。

背景知识

依赖于Http/206响应

首先你需要知道文件大小以及远程服务器是否支持HTTP 206请求.使用curl命令可以查看任意资源的HTTP头,使用下面的curl命令可以发送一个HEAD请求

$ curl -I http://avatar.csdn.net/D/0/9/1_wz249863091.jpg
这张图是我在CSDN博客的头像

这里写图片描述

这里只看2个属性

Accept-Ranges: bytes - 该响应头表明服务器支持Range请求,以及服务器所支持的单位是字节(这也是唯一可用的单位).我们还能知道:服务器支持断点续传,以及支持同时下载文件的多个部分,也就是说下载工具可以利用范围请求加速下载该文件.Accept-Ranges: none 响应头表示服务器不支持范围请求.

Content-Length: 30220 - Content-Length响应头表明了响应实体的大小,也就是真实的图片文件的大小是30220字节 (30K).

正常情况下,我们不指定range属性的时候,默认值都是0-length,也就是全量下载,返回的code就是200
如果我们指定了range,那么返回的code就是206

代码实现

根据上面的背景知识,可以写一个工具类,便于整个项目使用

package com.example.tony.myapplication;

import android.os.Environment;
import android.text.TextUtils;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

/**
 * 断点续传工具类
 *
 * @author Tony.W
 */
public class DownloadUtil {
    private static final String RANGE = "Range";
    private static final String BYTE = "bytes=";
    private static final String TO = "-";
    private static final String RWS = "rws";
    private static final String SLASH = "/";
    private static final String UTF_8 = "utf-8";

    //Http协议--部分下载的状态码
    private static final int HTTP_PARTIAL_CODE = 206;
    //Htpp协议--下载成功的状态码
    private static final int HTPP_SUCCESS_CODE = 200;
    //Http超时时间
    private static final int TIME_OUT = 3000;
    //默认下载线程数
    private static final int DEFALUT_THREAD_COUNT = 3;

    /**
     * 断点续传下载
     *
     * @param path 资源路径
     * @param threadCount 下载线程数
     * */
    public static void down(String path, int threadCount){
        URL url = null;
        HttpURLConnection conn = null;
        RandomAccessFile raf = null;
        try {
            url = new URL(path);
            conn = (HttpURLConnection) url.openConnection();
            if (conn.getResponseCode() == HTPP_SUCCESS_CODE){
                //文件长度
                int fileLen = conn.getContentLength();
                //缓存文件名字
                String cacheFileName = path.substring(path.lastIndexOf(SLASH) + 1);
                File file = new File(Environment.getExternalStorageDirectory(), cacheFileName);
                raf = new RandomAccessFile(file, RWS);
                raf.setLength(fileLen);
                int partialLen = fileLen / threadCount;
                for(int i = 0;i<threadCount;i++){
                    new DownloadThread(url, i, partialLen, file);
                }
            }

        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(raf != null){
                try {
                    raf.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 断点续传下载,默认开启3个线程下载
     *
     * @param path 资源路径
     * */
    public static void down(String path){
        down(path, DEFALUT_THREAD_COUNT);
    }


    private static final class DownloadThread extends  Thread{
        //下载资源的url
        private URL url;
        //下载的线程Id
        private int threadId;
        //每个线程需要下载的长度
        private int particalLen;
        //缓存文件地址
        private File file;
        //是否暂停下载
        private boolean isPause = false;

        /**
         * 下载线程
         * */
        public DownloadThread(URL url, int threadId, int particalLen, File file){
            this.file = file;
            this.url = url;
            this.particalLen = particalLen;
            this.threadId = threadId;
        }

        /**
         * 从文件读取数据
         * */
        public static String readFromFile(File file) throws IOException {
            if (!file.exists() || file.isDirectory()) {
                return null;
            }
            BufferedReader br = new BufferedReader(new FileReader(file));
            String temp = null;
            StringBuffer sb = new StringBuffer();
            temp = br.readLine();
            while (temp != null) {
                sb.append(temp);
                temp = br.readLine();
            }
            return sb.toString();
        }

        /**
         * 向文件写入数据
         * */
        public static void writeToFile(File file, String data) throws IOException {
            if (!file.exists()) {
                file.createNewFile();
            }
            //重新写入,不是追加写入模式
            FileOutputStream out = new FileOutputStream(file, false);
            out.write(data.getBytes(UTF_8));
            out.close();
        }

        @Override
        public void run() {
            downTask();
        }

        private void downTask(){
            //缓存文件名字
            String cacheFileName = Environment.getExternalStorageDirectory() + url.getPath().substring(url.getPath().lastIndexOf(SLASH) + 1 + threadId);
            File cacheFile = new File(cacheFileName);
            //读取缓存文件,判断是否之前已经有下载部分
            float hasDownLen = 0;
            //下载开始位置
            int start = 0 + threadId * particalLen;
            //下载结束为止
            int end = (0 + threadId + 1) * particalLen -1;

            String data = null;
            try {
                data = readFromFile(cacheFile);
            } catch (IOException e) {
                e.printStackTrace();
            }

            if (!TextUtils.isEmpty(data)) {
                hasDownLen = Float.valueOf(data);
                start += hasDownLen;
            }

            HttpURLConnection conn = null;
            try {
                conn = (HttpURLConnection) url.openConnection();
            } catch (IOException e) {
                e.printStackTrace();
            }
            if(conn == null){
                return;
            }

            conn.setReadTimeout(TIME_OUT);
            //指定下载部分,如果超过服务器部分,以服务器为准
            conn.setRequestProperty(RANGE, BYTE + start + TO + end);
            RandomAccessFile raf = null;
            InputStream in = null;
            try {
                if (conn.getResponseCode() == HTTP_PARTIAL_CODE) {
                    raf = new RandomAccessFile(file, RWS);
                    raf.seek(start);
                    in = conn.getInputStream();
                    //分段下载,每段大小为1024个字节
                    byte[] buf = new byte[1024];
                    int len = 0;
                    //如果读到最后,就跳出循环
                    while ((len = in.read(buf)) != -1) {
                        //将下载的数据存到文件
                        raf.write(buf);
                        //记录下载长度
                        hasDownLen += len;
                        writeToFile(cacheFile, String.valueOf(hasDownLen));
                    }
                    //如果完成该部分下载,删除缓存文件
                    cacheFile.delete();
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (raf != null) {
                        raf.close();
                    }
                    if (in != null) {
                        in.close();
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

这里只实现了多线程断点续传功能,如果要结合UI,可以加入一个Handler。
但是需要主要,由于都是static的静态方法,如果处理Handler的时候需要格外小心,不要造成内存泄漏

另外可以观察DDMS,在下载的时候,会产生N+1个文件,N就是你开启线程的数量,当下载全部完成后,只会留下一个数据文件,其余临时缓存文件都会被删除

附件已经上传,有需要的朋友可以下载
http://download.csdn.net/detail/wz249863091/9566461
如果有什么问题,请留言或者私信,第一时间会做出合理修改

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值