目录

用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

更多相关文章

  1. 从J2EE转向Android的第九天-----文件存储
  2. Android线程间通信的Message机制
  3. Android的线程使用来更新UI----View的几种更新方法(Thread、Hand
  4. Android笔记四 虚拟机Dalvik、Android各种java包功能、Android相
  5. android保存手势操作到文件&读取识别手势
  6. Android之子线程更新UI

随机推荐

  1. Android属性动画与自定义属性动画
  2. 解决:android NDK的AMediaCodec配置surfac
  3. Android(安卓)sqlite中text格式文本的比
  4. (4.2.4)【android开源组件】Android(安卓)
  5. Mac系统下利用ADB命令连接android手机并
  6. android:activity销毁后,fragment使用控件,
  7. Android(安卓)Application对象必须掌握的
  8. Android(安卓)Studio & Typora 快捷键
  9. Android(安卓)3.0 r1 API中文文档(113)
  10. Java乔晓松-android中获取手机视频的缩略