zoukankan      html  css  js  c++  java
  • android学习笔记----多线程断点续传下载原理设计

    目录

    用java实现多线程下载:

    用android实现多线程下载(HttpURLConnection):

    用android实现多线程下载(OkHttp):


    android实现(HttpURLConnection)的Demo源码:https://github.com/liuchenyang0515/MultithreadBreakpointDowload

    android实现(OkHttp)的Demo源码(推荐):https://github.com/liuchenyang0515/MultithreadBreakpointDowload1

    下载原理:

    用java实现多线程下载:

    先把tomcat服务器开起来,然后在webapps/ROOT/目录下放abc.exe供下载测试

    先来段java实现的代码:

    import java.io.BufferedReader;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.RandomAccessFile;
    import java.net.HttpURLConnection;
    import java.net.MalformedURLException;
    import java.net.ProtocolException;
    import java.net.URL;
    
    public class MultiDownload {
        // 定一下载路径
        private static final String path = "http://192.168.164.1:8080/abc.exe";
        private static final int threadCount = 3; // 假设开3个线程
        private static int runningThread; // 代表当前正在运行的线程
    
        public static void main(String[] args) {
            RandomAccessFile rafAccessFile = null;
            // 获取服务器文件的大小
            try {
                HttpURLConnection conn = connectNetSettings();
                int code = conn.getResponseCode();
                if (code == 200) {
                    // 获取服务器文件的大小
                    int length = conn.getContentLength();
                    // 把线程的数量赋值给正在运行的线程
                    runningThread = threadCount;
                    rafAccessFile = new RandomAccessFile(getFileName(path), "rw");
                    // 创建一个和服务器大小一样的的文件,提前申请好空间
                    rafAccessFile.setLength(length);
                    rafAccessFile.close();
                    int blockSize = length / threadCount;
    
                    // 计算每个线程下载的开始位置和结束位置
                    for (int i = 0; i < threadCount; ++i) {
                        int startIndex = i * blockSize; // 每个线程下载的开始位置
                        int endIndex; // 每个线程下载的结束位置
                        if (i == threadCount - 1) { // 如果是最后一个线程
                            endIndex = length - 1;
                        } else {
                            endIndex = (i + 1) * blockSize - 1;
                        }
                        System.out.println("线程id:" + i + "理论下载的位置" + startIndex + "=========" + endIndex);
                        // 四 开启线程去服务器下载文件
                        DownLoadThread downLoadThread = new DownLoadThread(startIndex, endIndex, i);
                        downLoadThread.start();
                    }
                }
    
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                try {
                    rafAccessFile.close();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    
        private static HttpURLConnection connectNetSettings() throws MalformedURLException, IOException, ProtocolException {
            URL url = new URL(path);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(5000);
            return conn;
        }
    
        // 定义线程去服务器下载文件
        private static class DownLoadThread extends Thread {
            // 通过构造方法把每个线程下载的开始和结束位置传进来
            private int startIndex;
            private int endIndex;
            private int threadId;
    
            public DownLoadThread(int startIndex, int endIndex, int threadId) {
                this.startIndex = startIndex;
                this.endIndex = endIndex;
                this.threadId = threadId;
            }
    
            public <T extends java.io.Closeable> void close(T t) {
                try {
                    if (t != null) {
                        t.close();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
    
            @Override
            public void run() {
                InputStream in = null;
                RandomAccessFile raf = null;
                BufferedReader br = null;
                RandomAccessFile raff = null;
                RandomAccessFile breakpoint = null;
                try {
                    HttpURLConnection conn = connectNetSettings();
                    File file = new File(threadId + ".txt");
                    if (file.exists() && file.length() > 0) {
                        br = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
                        String lastPosition = br.readLine(); // 读出来的内容就是上次下载保存的位置
                        int last = Integer.parseInt(lastPosition);
                        // 要改变一下startIndex位置
                        startIndex = last;
                        System.out.println("线程id:" + threadId + "真实下载的位置" + startIndex + "=========" + endIndex);
                        br.close();
                    }
    
                    // 设置一个请求头Range,作用是告诉服务器每个线程下载的开始和结束位置
                    // 固定写法
                    conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);
    
                    int code = conn.getResponseCode();
                    // 206代表部分资源请求成功,200表示请求全部资源成功
                    if (code == 206) {
                        // 创建随机读写文件对象
                        raf = new RandomAccessFile(getFileName(path), "rw");
                        // 每个线程要从自己的位置开始写
                        raf.seek(startIndex);
                        // 存的是abc.exe
                        in = conn.getInputStream();
                        // 把数据写到文件中
                        int len = -1;
                        byte[] buffer = new byte[1024];
    
                        int total = 0; // 代表当前线程下载的大小
                        // 下面这句不要写在while里面,避免重复关联文件导致文件无法删除
                        raff = new RandomAccessFile(threadId + ".txt", "rwd");// 关联文件时,文件指针初始为0的位置
                        while ((len = in.read(buffer)) != -1) {
                            raf.write(buffer, 0, len);
                            total += len;
                            // 实现断点续传,就是把当前线程下载的位置存起来
                            // 下次再下载的时候,就是按照上次下载的位置继续下载就行
                            int currentThreadPosition = startIndex + total;
                            // 用FileOuputStream可能因为突然停止导致不能立即写到硬盘
                            raff.writeBytes(String.valueOf(currentThreadPosition));
                            raff.seek(0); // 记录断点的txt文件需要每次从头开始写而不是续写,默认从文件指针处继续写
                        }
                        raff.close();
                        raf.close();
                        in.close();
                        System.out.println("线程id:" + threadId + "下载完成");
                        synchronized (DownLoadThread.class) {
                            breakpoint = new RandomAccessFile("time.txt", "rwd");
                            breakpoint.seek(0); // 准备从time.txt开头读取未下载完成的线程个数
                            String s = null;
                            if ((s = breakpoint.readLine()) != null) {// 读取剩余的需要下载的线程个数
                                runningThread = Integer.valueOf(s);
                            }
                            --runningThread;
                            breakpoint.seek(0); // 尝试读取后文件指针变化,再设置为0,从0处开始写入
                            breakpoint.write(String.valueOf(runningThread).getBytes());
                            breakpoint.close();
                            if (runningThread == 0) {
                                for (int i = 0; i < threadCount; ++i) {
                                    File deleteFile = new File(i + ".txt");
                                    System.out.println(deleteFile.toString());
                                    deleteFile.delete();
                                }
                                new File("time.txt").delete();
                            }
                        }
                    }
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } finally {
                    close(breakpoint);
                    close(raff);
                    close(raf);
                    close(in);
                    close(br);
                }
            }
        }
    
        public static String getFileName(String path) {
            int index = path.lastIndexOf("/") + 1;
            return path.substring(index);
        }
    }

     假如断点特殊情况,断的很巧妙,一个线程下载完了别的线程还没下载完,下次再开始下载的时候,runningThread又被初始化为3个,其他2个线程下载完后runningThread=1不为0,这样就导致删除不了txt文件。

    方法:同样将还没下载完成的线程个数写到文件中

    想要达到上面效果,必须这么处理:

    synchronized (DownLoadThread.class) {
        breakpoint = new RandomAccessFile("time.txt", "rwd");
        breakpoint.seek(0); // 准备从time.txt开头读取未下载完成的线程个数
        String s = null;
        if ((s = breakpoint.readLine()) != null) { // 读取剩余的需要下载的线程个数
            runningThread = Integer.valueOf(s);
        }
        --runningThread;
        breakpoint.seek(0); // 尝试读取后文件指针变化,再设置为0,从0处开始写入
        breakpoint.write(String.valueOf(runningThread).getBytes());
        breakpoint.close();
        if (runningThread == 0) {
            for (int i = 0; i < threadCount; ++i) {
                File deleteFile = new File(i + ".txt");
                System.out.println(deleteFile.toString());
                deleteFile.delete();
            }
            new File("time.txt").delete();
        }
    }

    笔记批注:

            流处理我尝试关闭了2次,第一次是因为想尽早关闭,减少占用资源消耗,第二次是在finally{...},是想尽量确保所有的流能关闭。

            有几个线程就把资源大小除以几,除不尽的就让最后一个线程多下载一点,这就是为什么我们经常用迅雷下载的时候明明到了99%却最后下载的越来越慢,因为别的线程都下载完了,还在等待最后一个线程下载。

    setRequestProperty是HttpURLConnection继承的URLConnection中的方法。

    public void setRequestProperty(String key, String value)

    设置一般请求属性。 如果具有密钥的属性已存在,则使用新值覆盖其值。

    注意:HTTP需要所有请求属性,它们可以合法地使用相同键的多个实例来使用逗号分隔的列表语法,这样可以将多个属性附加到单个属性中。

    参数

    key - 请求已知的关键字(例如,“ Accept ”)。

    value - value的值。

    异常

    IllegalStateException - 如果已经连接

    NullPointerException - 如果键是 null

    另请参见:

    getRequestProperty(java.lang.String)

    用android实现多线程下载(HttpURLConnection):

    android的demo目录如下:

    因为是模拟器,所以这里使用了SD卡,并没有判断SD卡是否存在

    如果需要做的更加完善,需要

    判断SD卡是否存在

    下载前要判断手机网络类型,是在wifi情况下载还是蜂窝移动数据下载

    下载前需要扫描手机是否有病毒等等......

    这里没有实现那么多,主要为了实现多线程现在和断点续传的功能。

    MainActivity.java

    package com.example.multi_threadbreakpointdowload;
    
    import android.Manifest;
    import android.content.pm.PackageManager;
    import android.os.Bundle;
    import android.os.Environment;
    import android.support.annotation.NonNull;
    import android.support.v4.app.ActivityCompat;
    import android.support.v4.content.ContextCompat;
    import android.support.v7.app.AppCompatActivity;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.widget.EditText;
    import android.widget.LinearLayout;
    import android.widget.ProgressBar;
    import android.widget.Toast;
    
    import java.io.File;
    import java.io.IOException;
    import java.io.RandomAccessFile;
    import java.net.HttpURLConnection;
    import java.util.ArrayList;
    import java.util.List;
    
    import static com.example.multi_threadbreakpointdowload.ConnectionUtils.close;
    
    public class MainActivity extends AppCompatActivity {
    
        private LinearLayout ll_pb_layout;
        private EditText et_threadCount;
        private EditText et_path;
        private String path;
        private int runningThread;
        private int threadCount;
        private List<ProgressBar> pbLists; // 用来存进度条的引用
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            et_path = (EditText) findViewById(R.id.et_path);
            et_threadCount = (EditText) findViewById(R.id.et_threadCount);
            ll_pb_layout = (LinearLayout) findViewById(R.id.ll_pb);
    
            // 添加一个集合,用来存进度条的引用
            pbLists = new ArrayList<ProgressBar>();
        }
    
    
        // 点击按钮实现下载的逻辑
        public void onclick(View v) throws IOException {
            if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                    != PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
            } else {
                switch (v.getId()) {
                    case R.id.btn_01:
                        runDownLoad();
                        break;
                    case R.id.btn_02:
                        clearReady();
                        runDownLoad();
                        break;
                }
            }
        }
    
        private void clearReady() {
            for (int i = 0; i < threadCount; ++i) {
                File deleteFile = new File(Environment.getExternalStorageDirectory().getPath() + "/" + i + ".txt");
                deleteFile.delete();
            }
            new File(Environment.getExternalStorageDirectory().getPath() + "/time.txt").delete();
        }
    
        @Override
        public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
            switch (requestCode) {
                case 1:
                    if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                        runDownLoad();
                    } else {
                        Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
                    }
                    break;
            }
        }
    
        private void runDownLoad() {
            // 获取下载的路径
            path = et_path.getText().toString().trim();
            // 获取线程的数量
            threadCount = Integer.parseInt(et_threadCount.getText().toString().trim());
            // 先移除上次进度条再添加
            ll_pb_layout.removeAllViews();
            pbLists.clear();
            for (int i = 0; i < threadCount; ++i) {
                // 把定义的item布局转换成一个View对象
                // item布局的父布局是ll_pb_layout对象对应的布局,然后false就是这个view按照子布局item的形式来
                ProgressBar pbView = (ProgressBar) LayoutInflater.from(MainActivity.this).inflate(R.layout.item, ll_pb_layout, false);
    
                // 把pbView添加到集合中
                pbLists.add(pbView);
    
                // 动态添加进度条
                ll_pb_layout.addView(pbView);
            }
            new Thread() {
                @Override
                public void run() {
                    RandomAccessFile rafAccessFile = null;
                    // 获取服务器文件的大小
                    try {
                        HttpURLConnection conn = ConnectionUtils.connectNetSettings(path);
                        int code = conn.getResponseCode();
                        if (code == 200) {
                            // 获取服务器文件的大小
                            int length = conn.getContentLength();
                            // 把线程的数量赋值给正在运行的线程
                            runningThread = threadCount;
                            rafAccessFile = new RandomAccessFile(ConnectionUtils.getFileName(path), "rw");
                            // 创建一个和服务器大小一样的的文件,提前申请好空间
                            rafAccessFile.setLength(length);
                            rafAccessFile.close();
                            int blockSize = length / threadCount;
    
                            // 计算每个线程下载的开始位置和结束位置
                            for (int i = 0; i < threadCount; ++i) {
                                int startIndex = i * blockSize; // 每个线程下载的开始位置
                                int endIndex; // 每个线程下载的结束位置
                                if (i == threadCount - 1) { // 如果是最后一个线程
                                    endIndex = length - 1;
                                } else {
                                    endIndex = (i + 1) * blockSize - 1;
                                }
                                System.out.println("线程id:" + i + "理论下载的位置" + startIndex + "=========" + endIndex);
                                // 四 开启线程去服务器下载文件
                                DownLoadThread downLoadThread = new DownLoadThread(startIndex, endIndex,
                                        i, path, pbLists, runningThread, threadCount);
                                downLoadThread.start();
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        close(rafAccessFile);
                    }
                }
            }.start();
        }
    }
    

    DownLoadThread.java

    package com.example.multi_threadbreakpointdowload;
    
    import android.os.Environment;
    import android.widget.ProgressBar;
    
    import java.io.BufferedReader;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.RandomAccessFile;
    import java.net.HttpURLConnection;
    import java.util.List;
    
    import static com.example.multi_threadbreakpointdowload.ConnectionUtils.close;
    
    // 定义线程去服务器下载文件
    public class DownLoadThread extends Thread {
        // 通过构造方法把每个线程下载的开始和结束位置传进来
        private int startIndex;
        private int endIndex;
        private int threadId;
        private String path;
        private int PbMaxSize; // 代表当前线程下载的最大值
        private int pblastPositon; // 如果中断过,获取上次下载的位置
        private List<ProgressBar> pbLists; // 用来存进度条的引用
    
        private int runningThread;
        private int threadCount;
    
        public DownLoadThread(int startIndex, int endIndex, int threadId,
                              String path, List<ProgressBar> pbLists, int runningThread, int threadCount) {
            this.startIndex = startIndex;
            this.endIndex = endIndex;
            this.threadId = threadId;
            this.path = path;
            this.pbLists = pbLists;
            this.runningThread = runningThread;
            this.threadCount = threadCount;
        }
    
    
        @Override
        public void run() {
            InputStream in = null;
            RandomAccessFile raf = null;
            BufferedReader br = null;
            RandomAccessFile raff = null;
            RandomAccessFile breakpoint = null;
            try {
                // 计算当前进度条的最大值
                PbMaxSize = endIndex - startIndex;
    
                HttpURLConnection conn = ConnectionUtils.connectNetSettings(path);
                File file = new File(Environment.getExternalStorageDirectory().getPath() + "/" + threadId + ".txt");
                if (file.exists() && file.length() > 0) {
                    br = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
                    String lastPosition = br.readLine(); // 读出来的内容就是上次下载保存的位置
                    int last = Integer.parseInt(lastPosition);
    
                    // 给我们定义的进度条位置赋值
                    pblastPositon = last - startIndex;
                    // 要改变一下startIndex位置
                    startIndex = last;
                    System.out.println("线程id:" + threadId + "真实下载的位置" + startIndex + "=========" + endIndex);
                    br.close();
                }
    
                // 设置一个请求头Range,作用是告诉服务器每个线程下载的开始和结束位置
                // 固定写法
                conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);
    
                int code = conn.getResponseCode();
                // 206代表部分资源请求成功,200表示请求全部资源成功
                if (code == 206) {
                    // 创建随机读写文件对象
                    raf = new RandomAccessFile(ConnectionUtils.getFileName(path), "rw");
                    // 每个线程要从自己的位置开始写
                    raf.seek(startIndex);
                    // 存的是abc.exe
                    in = conn.getInputStream();
                    // 把数据写到文件中
                    int len = -1;
                    byte[] buffer = new byte[1024 * 1024];
    
                    int total = 0; // 代表当前线程下载的大小
                    // 下面这句不要写在while里面,避免重复关联文件导致文件无法删除
                    raff = new RandomAccessFile(Environment.getExternalStorageDirectory().getPath() + "/" + threadId + ".txt", "rwd");
                    while ((len = in.read(buffer)) != -1) {
                        raf.write(buffer, 0, len);
                        total += len;
                        // 实现断点续传,就是把当前线程下载的位置存起来
                        // 下次再下载的时候,就是按照上次下载的位置继续下载就行
                        int currentThreadPosition = startIndex + total;
                        // 用FileOuputStream可能因为突然停止导致不能立即写到硬盘
                        raff.writeBytes(String.valueOf(currentThreadPosition));
                        raff.seek(0);// 避免每次写数据不断往后添加
                        // 设置当前进度条的最大值和当前进度
                        pbLists.get(threadId).setMax(PbMaxSize); // 设置进度条的最大值
                        pbLists.get(threadId).setProgress(pblastPositon + total); // 设置当前进度条的当前进度
                    }
                    raff.close();
                    raf.close();
                    in.close();
                    System.out.println("线程id:" + threadId + "下载完成");
                    synchronized (DownLoadThread.class) {
                        breakpoint = new RandomAccessFile(Environment.getExternalStorageDirectory().getPath() + "/time.txt", "rwd");
                        breakpoint.seek(0); // 准备从time.txt开头读取未下载完成的线程个数
                        String s = null;
                        if ((s = breakpoint.readLine()) != null) {
                            runningThread = Integer.valueOf(s);
                        }
                        --runningThread;
                        breakpoint.seek(0); // 尝试读取后文件指针变化,再设置为0,从0处开始写入
                        breakpoint.write(String.valueOf(runningThread).getBytes());
                        breakpoint.close();
                        if (runningThread == 0) {
                            for (int i = 0; i < threadCount; ++i) {
                                File deleteFile = new File(Environment.getExternalStorageDirectory().getPath() + "/" + i + ".txt");
                                System.out.println(deleteFile.toString());
                                deleteFile.delete();
                            }
                            new File(Environment.getExternalStorageDirectory().getPath() + "/time.txt").delete();
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                close(raff);
                close(raf);
                close(in);
                close(br);
            }
        }
    }

    ConnectionUtils.java

    package com.example.multi_threadbreakpointdowload;
    
    import android.os.Environment;
    
    import java.net.HttpURLConnection;
    import java.net.URL;
    
    public class ConnectionUtils {
        static HttpURLConnection connectNetSettings(String path) throws Exception {
            URL url = new URL(path);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            conn.setRequestMethod("GET");
            conn.setConnectTimeout(5000);
            return conn;
        }
    
        static String getFileName(String path) {
            int index = path.lastIndexOf("/") + 1;
            return Environment.getExternalStorageDirectory().getPath() + "/" + path.substring(index);
        }
    
        static <T extends java.io.Closeable> void close(T t) {
            try {
                if (t != null) {
                    t.close();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    运行结果如下:

    出现断点时,断点下载也测试成功,进度条也从断点开始加载显示

    当然为了应对极度变态的断电情况出现的,所有线程都执行完了,准备去删除txt文件的时候没有执行完,导致还剩余txt文件,下次再下载的时候就会出问题,所以添加了“重新下载”按钮,就把txt文件全部删掉再开始下载。

    用android实现多线程下载(OkHttp):

    由于篇幅原因,OkHttp实现的直接放在github,和用HttpURLConnection实现的效果完全相同

    地址https://github.com/liuchenyang0515/MultithreadBreakpointDowload1

    ===========================Talk is cheap, show me the code=========================

    CSDN博客地址:https://blog.csdn.net/qq_34115899
  • 相关阅读:
    Winform中多线程无法访问使用 Control.CheckForIllegalCrossThreadCalls = false;
    PV操作-生产者/消费者关系
    table表格长度超出屏幕范围,可滑动
    Koa2中间件计算响应总耗时/设置响应头/读取Json文件返回给客户端
    Koa2简介和搭建
    计算机浮点数的表示和运算
    CSS实现Loading加载中动画
    RPC
    Git常用命令
    如何解决 shell 脚本重复执行的问题
  • 原文地址:https://www.cnblogs.com/lcy0515/p/10807874.html
Copyright © 2011-2022 走看看