目录
用android实现多线程下载(HttpURLConnection):
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=========================