多线程下载与断点续传

1. 多线程下载

1.1 为什么要使用多线程

多线程编程的目的,就是“最大限度地利用CPU资源”,当某一线程的处理不需要占用CPU而只和I/O等资源打交道时,让需要占用CPU资源的其它线程有机会获得CPU资源。从根本上说,这就是多线程编程的最终目的。

因为单线程只会映射到一个CPU上,而多线程会映射到多个CPU上,多线程技术本质是多线程硬件化,所以也会加快程序的执行速度。现在的PC或者手机有很多都是多核的,如果只使用单一的线程去处理任务,资源得不到充分利用。

1.2 多线程能提高效率吗

打个比方,比如修一个桥洞,有2种开工方法

 

方案一、只在桥的一头挖,直至挖到桥的另一头,从而打通桥洞,这可以看成是单线程。

方案二、在桥的两头挖,同时开工,最后在桥的中间接通,从而打通桥动,这感觉肯定比方案一快了很多,好比多线程。

假设每挖5分钟,就需要清理一下挖出来的泥土。有一个小车在清理它们,工人只有一个。

单线程的做法是: 挖5分钟。然后工人停止挖,小车清理石土的5分钟里,工人在等待。

2个线程的做法是: 挖5分钟,小车来清理泥土。这5分钟里,工人在另一头挖。

这个比喻至少能说明点问题:小车清理泥土,就相当于磁盘io等相对于cpu计算来说比较慢的操作。在cpu空闲的时候可以让其去做其它事情,达到充分利用的效果。

1.3线程越多越好?

 

并不是线程越多性能越好,当线程超过一定数量的时候,线程的调度将会变成很大的开销,反而会让性能降低,所以要适当使用多线程,不能滥用。二者不是线性关系。

计算机中一般来说只有一个CPU,也就是说只有一个工人。现在把修桥方案变动一下。

方案一:只在山的一头挖,直至挖到山的另一头,从而打通隧道,这可以看成是单线程。

方案二:在山的两头挖,同时开工,最后在山的中间接通,从而打通隧道,这感觉肯定比1快了很多,好比多线程。

方案二虽然是在山的两头开挖,但是由于工作的人只有一个,所以只有让这个人在山的两头跑,挖一会这头再去挖另一头,来回跑是要花费额外时间的(好比线程的切换和调度)。

再举二个例子:

例子一:

A单核单处理器,开一个线程跑循环输出10万条打印信息

B开100个线程输出10万条打印信息。

后者比前者慢,因为输出端是临界资源(临界资源:多道程序系统中存在许多进程,它们共享各种资源,然而有很多资源一次只能供一个进程使用。一次仅允许一个进程使用的资源称为临界资源。许多物理设备都属于临界资源,如输入机、打印机、磁带机等。),线程抢占的时间大,单线程则无需抢占。

例子二:

A网络服务器处理,每个请求开一个线程,请求的处理时间极短,迅速返回。

B一次提交10万个请求,则有10万次线程创建和销毁对应于一个工作线程处理这10万条。请求后者比前者肯定快。

2. 断点续传

2.1 为什么要使用断点续传

在进行数据上传的时候可能是多线程操作,很多图像数据同时做上传或者单一的图像,如果图像比较多或者单一图像数据比较大,自然不希望失败一次或者暂停一次之后完全重传,有断点续传功能可以节省网络流量和节省用户时间,体验自然比你一次次的重传好很多。

2.2 什么是断点续传

所谓断点续传,也就是要从文件已经下载的地方开始继续下载。HTTP/1.1 开始就支持了。一般断点下载时才用到 Range 和 Content-Range 实体头。

什么是Range?

当用户在听一首歌的时候,如果听到一半(网络下载了一半),网络断掉了,用户需要继续听的时候,文件服务器不支持断点的话,则用户需要重新下载这个文件。而Range支持的话,客户端应该记录了之前已经读取的文件范围,网络恢复之后,则向服务器发送读取剩余Range的请求,服务端只需要发送客户端请求的那部分内容,而不用整个文件发送回客户端,以此节省网络带宽。

如果Server支持Range,首先就要告诉客户端,咱支持Range,之后客户端才可能发起带Range的请求。

response.setHeader(‘Accept-Ranges’, ‘bytes’);

Server通过请求头中的Range: bytes=0-xxx来判断是否是做Range请求,如果这个值存在而且有效,则只发回请求的那部分文件内容,响应的状态码变成206,表示Partial Content,并设置Content-Range。如果无效,则返回416状态码,表明Request Range Not Satisfiable。如果不包含Range的请求头,则继续通过常规的方式响应。
 

2.3 应用场景

假设你要开发一个多线程下载工具,你会自然的想到把文件分割成多个部分,比如4个部分,然后创建4个线程,每个线程负责下载一个部分,如果文件大小为403个byte,那么你的分割方式可以为:0-99 (前100个字节),100-199(第二个100字节),200-299(第三个100字节),300-402(最后103个字节)。

分割完成,每个线程都明白自己的任务,比如线程3的任务是负责下载200-299这部分文件,现在的问题是:线程3发送一个什么样的请求报文,才能够保证只请求文件的200-299字节,而不会干扰其他线程的任务。这时,我们可以使用HTTP1.1的Range头。

Range头域可以请求实体的一个或者多个子范围,Range的值为0表示第一个字节,也就是Range计算字节数是从0开始的:

表示头500个字节:Range: bytes=0-499

表示第二个500字节:Range: bytes=500-999

表示最后500个字节:Range: bytes=-500

表示500字节以后的范围:Range: bytes=500-

第一个和最后一个字节:Range: bytes=0-0,-1

同时指定几个范围:Range: bytes=500-600,601-999

所以,线程3发送的请求报文必须有这一行:

Range: bytes=200-299

服务器接收到线程3的请求报文,发现这是一个带有Range头的GET请求,如果一切正常,服务器的**响应报文会有下面这行:

HTTP/1.1 206 OK**

表示处理请求成功,响应报文还有这一行

Content-Range: bytes 200-299/403

斜杠后面的403表示文件的大小

3. RandomAccessFile

下载的数据流最终要保存到文件中,我们平常创建流对象关联文件,开始读文件或者写文件都是从头开始的, 不能从中间开始, 如果是开多线程下载一个文件,就需要从指定的位置去读写一个文件;而RandomAccessFile就可以解决这个问题。

构造方法:RandomAccessFile raf = newRandomAccessFile(File file, String mode);

 其中参数 mode 的值可选 "r":可读,"w" :可写,"rw":可读性;

成员方法:

  • ​ seek(int index);可以将指针移动到某个位置开始读写;
  • ​ setLength(long len);给写入文件预留空间:

4.代码实现

以下载一个apk为例来看一下断点续传的实现:


class DownloadTask extends AsyncTask<UpdateInfo, Integer, Boolean> {

    private static final int TIME_OUT = 30000;
    private static final int BUFFER_SIZE = 1024 * 100;

    private Context mContext;

    private String mRootUrl;
    private File mLoadedFile;
    private File mApkFile;

    //文件总长度
    private long mBytesTotal = 0;
    //本地已下载
    private long mBytesLoaded = 0;
    private long mTimeLast = 0;

    private HttpURLConnection mConnection;
    private String mMd5;
    private OnUpdateListener mUpdateListener;
    private UpdateInfo mUpdateInfo;

    public DownloadTask(OnUpdateListener updateListener, String rootUrl) {
        super();
        mContext = MainApplication.getPackageContext();
        mUpdateListener = updateListener;
        mRootUrl = rootUrl;
    }

    @Override
    protected Boolean doInBackground(UpdateInfo... params) {
        //开始下载
        if (mUpdateListener != null) {
            mUpdateListener.onDownloadStart("");
        }
        mUpdateInfo = params[0];
        File savePath = new File(params[0].getApkDir());
        if (!savePath.exists()) {
            savePath.mkdirs();
        }
        mApkFile = new File(savePath, params[0].getFileName());

        return download(savePath, params[0].getAppId());
    }


    @Override
    protected void onProgressUpdate(Integer... progress) {
        Logger.d("onProgressUpdate:" + progress[0]);
        if (mUpdateListener != null) {
            mUpdateListener.onDownloading(progress[0]);
        }
    }

    @Override
    protected void onPostExecute(Boolean result) {
        if (mUpdateListener != null) {
            if (result) {
                mUpdateListener.onDownloadSuccess(mApkFile.getPath());
            }
        }
    }

    private HttpURLConnection create(URL url) throws IOException {
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        connection.setConnectTimeout(30000);
        connection.setReadTimeout(30000);
        connection.setRequestProperty("Connection", "close");
        connection.setRequestProperty("imei", DeviceGetter.getDeviceGetter().getIMEI());
        return connection;
    }

    private void setDownloadFailed(int errorCode) {
        setDownloadFailed(errorCode, null);
    }

    private void setDownloadFailed(int errorCode, String msg) {
        if (mUpdateListener != null) {
            mUpdateListener.onDownloadFailed(new UpdateError(errorCode, msg));
        }
    }

    private boolean download(File savePath, String appId) {
        try {

            if (!UpdateUtil.checkNetwork(mContext)) {
                setDownloadFailed(DOWNLOAD_NETWORK_IO);
                return false;
            }
            String url = mRootUrl + appId;
            Logger.d("apkUrl: " + url);
            mConnection = create(new URL(url));
            mConnection.connect();
            int statusCode = mConnection.getResponseCode();
            if (statusCode != 200 && statusCode != 206) {
                setDownloadFailed(DOWNLOAD_HTTP_STATUS);
                return false;
            }

            //获取文件总大小
            String contentRange = mConnection.getHeaderField("Content-Range");
            Logger.d("contentRange : " + contentRange);
            try {
                mBytesTotal = Long.valueOf((contentRange.split("/"))[1]);
                Logger.d("mBytesTotal : " + mBytesTotal);
            } catch (Exception e) {
                setDownloadFailed(DOWNLOAD_RESULT_ERROR);
                return false;
            }

            //获取文件MD5
            mMd5 = mConnection.getHeaderField("Content-MD5");
            Logger.d("mMd5 : " + mMd5);
            if (TextUtils.isEmpty(mMd5)) {
                setDownloadFailed(DOWNLOAD_RESULT_ERROR);
                return false;
            }

            //判断apk是否已存在
            if (UpdateUtil.verify(mApkFile, mMd5)) {
                if (mUpdateListener != null) {
                    mUpdateListener.onVerifySuccess(mUpdateInfo);
                }
                return true;
            }

            //如果临时文件存在,则断点续传
            mLoadedFile = new File(savePath, mMd5);
            if (mLoadedFile.exists() && mLoadedFile.isFile()) {
                //设置断点开始位置
                mBytesLoaded = mLoadedFile.length();
                Logger.d("本地已下载 : " + mBytesLoaded);
                //本地已下载文件太小则忽略,文件大小超出则删除
                if (mBytesLoaded < 1024 * 100 || mBytesLoaded > mBytesTotal) {
                    mLoadedFile.delete();
                    mLoadedFile.createNewFile();
                    mBytesLoaded = 0;
                }
            }

            if (mBytesLoaded != mBytesTotal) {
                if (mBytesLoaded > 0) {
                    mConnection.disconnect();
                    mConnection = create(mConnection.getURL());
                    mConnection.addRequestProperty("Range", "bytes=" + mBytesLoaded + "-");
                    mConnection.connect();
                    statusCode = mConnection.getResponseCode();
                    if (statusCode != 200 && statusCode != 206) {
                        setDownloadFailed(DOWNLOAD_HTTP_STATUS);
                        return false;
                    }
                }

                if (!checkSpace(mBytesTotal - mBytesLoaded)) {
                    setDownloadFailed(DOWNLOAD_DISK_NO_SPACE);
                    return false;
                }

                int bytesCopied = copy(mConnection.getInputStream(), new RandomAccessFile(mLoadedFile, "rwd"));

                if ((mBytesLoaded + bytesCopied) != mBytesTotal && mBytesTotal != -1) {
                    Logger.e("下载失败:下载不完整;文件总大小:" + mBytesTotal + "; 已下载:" + (mBytesLoaded + bytesCopied));
                    if (mLoadedFile != null && mLoadedFile.exists()) {
                        mLoadedFile.delete();
                    }
                    setDownloadFailed(DOWNLOAD_INCOMPLETE);
                    return false;
                }
            } else {
                Logger.d("本地文件完整,不用再次下载");
            }

            //校验apk
            if (mUpdateListener != null) {
                mUpdateListener.onVerifyStart("");
            }
            if (mLoadedFile.renameTo(mApkFile) && UpdateUtil.verify(mApkFile, mMd5)) {
                Logger.d("文件校验成功");
                if (mUpdateListener != null) {
                    mUpdateListener.onVerifySuccess(mUpdateInfo);
                }
                return true;
            } else {
                setDownloadFailed(DOWNLOAD_VERIFY);
                return false;
            }

        } catch (SocketTimeoutException e) {
            setDownloadFailed(DOWNLOAD_NETWORK_TIMEOUT);
        } catch (MalformedURLException e) {
            setDownloadFailed(DOWNLOAD_URL_ERROR);
        } catch (IOException e) {
            setDownloadFailed(DOWNLOAD_DISK_IO, e.getMessage());
        }
        return false;
    }

    private int copy(InputStream in, RandomAccessFile out) throws IOException {
        byte[] buffer = new byte[BUFFER_SIZE];
        BufferedInputStream bis = new BufferedInputStream(in, BUFFER_SIZE);
        try {
            out.seek(out.length());

            int bytes = 0;
            int progress = 0;
            int preProgress = 0;
            while (!isCancelled()) {
                int n = bis.read(buffer, 0, BUFFER_SIZE);
                if (n == -1) {
                    Logger.d("read == -1");
                    publishProgress(100);
                    break;
                }
                out.write(buffer, 0, n);
                bytes += n;

                //更新进度
                long now = System.currentTimeMillis();
                if (now - mTimeLast > 900) {
                    mTimeLast = now;
                    progress = (int) ((mBytesLoaded + bytes) * 100 / mBytesTotal);
                    if (progress > preProgress) {
                        preProgress = progress;
                        publishProgress(progress);
                    }
                }
            }
            return bytes;
        } finally {
            out.close();
            bis.close();
            in.close();
        }
    }

    private boolean checkSpace(long need) {
        File path = Environment.getExternalStorageDirectory();
        return need <= path.getFreeSpace();
    }

}

关于apk的md5校验:

    public static boolean verify(File apk, String md5) {
        if (!apk.exists() || TextUtils.isEmpty(md5)) {
            return false;
        }
        String _md5 = null;
        _md5 = md5(apk);
        Logger.d("_md5 : " +  _md5);
        if (TextUtils.isEmpty(_md5)) {
            return false;
        }
        boolean result = _md5.equalsIgnoreCase(md5);
        if (!result) {
            apk.delete();
        }
        return result;
    }


    public static String md5(File file) {
        MessageDigest digest = null;
        FileInputStream fis = null;
        byte[] buffer = new byte[10240];
        try {
            if (!file.isFile()) {
                return "";
            }
            digest = MessageDigest.getInstance("MD5");
            fis = new FileInputStream(file);
            while (true) {
                int len;
                if ((len = fis.read(buffer, 0, 10240)) == -1) {
                    fis.close();
                    break;
                }
                digest.update(buffer, 0, len);
            }
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return Base64.encode(digest.digest());
    }


public class Base64 {
    public static String encode(byte[] data) {
        return android.util.Base64.encodeToString(data, android.util.Base64.NO_WRAP);
    }

    public static byte[] decode(String str) {
        return android.util.Base64.decode(str, android.util.Base64.NO_WRAP);
    }
}

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值