android学习笔记----多线程断点续传下载原理设计

目录

用java实现多线程下载:

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

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


 

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

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

 

下载原理:

android学习笔记----多线程断点续传下载原理设计_第1张图片

 

用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  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文件。

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

android学习笔记----多线程断点续传下载原理设计_第2张图片

android学习笔记----多线程断点续传下载原理设计_第3张图片

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

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目录如下:

android学习笔记----多线程断点续传下载原理设计_第4张图片

 

因为是模拟器,所以这里使用了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 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();
    }


    // 点击按钮实现下载的逻辑
    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 pbLists; // 用来存进度条的引用

    private int runningThread;
    private int threadCount;

    public DownLoadThread(int startIndex, int endIndex, int threadId,
                          String path, List 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  void close(T t) {
        try {
            if (t != null) {
                t.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果如下:

 

android学习笔记----多线程断点续传下载原理设计_第5张图片

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

android学习笔记----多线程断点续传下载原理设计_第6张图片

 

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

 

 

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

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

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

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

转载于:https://www.cnblogs.com/lcy0515/p/10807874.html

你可能感兴趣的:(android学习笔记----多线程断点续传下载原理设计)