多线程断点下载

发表于:,更新于:,By Sally
大纲
  1. 1. 多线程断点下载原理
    1. 1.1. 原理
    2. 1.2. server端处理思路
    3. 1.3. client端处理思路
    4. 1.4. 实现断点下载
    5. 1.5. 知识点补充
  2. 2. 代码

多线程断点下载原理

原理

  • 开启的线程数为 threadCound = 3

  • 每个线程下载的大小为 blockSize = 3

  • 要下载文件的总大小为 size = 10


线程号 | 下载开始位置 | 下载结束位置 | eg
0 0*blockSize 1*blockSize - 1 0 - 2
1 1*blockSize 2*blockSize - 1 3 - 5
2 2*blockSize size 6 - 9

server端处理思路

  1. 获得目标文件的大小

  2. 根据开启线程的个数,执行下载操作

  3. 这里需要用到RandomAccessFile这个随机读写文件的类,

    • 该类可以创建指定大小的空白文件
    • 该类还能指定文件读写的位置
    • 该类在创建对象时,可以指定文件的操作模式(r, rw, rwd, rws)
    • 因为有了文件的操作模式,所以该类会实时的刷新文件到底层的存储设备,而不像一般的文件操作类的flush()动作,只是将读到的文件刷新到硬盘的缓冲区
  4. http协议

    • 这里需要用到一个http 协议,Range 指定读取服务器端文件的位置

    • HttpUrlConnection.setRequestProperties("Range", "byte=startIndex-endIndex");

client端处理思路

  1. 创建一个与目标文件一样大小的空白文件

  2. 开启若干个线程,执行下载

  3. 待所有的线程都下载完毕,则该文件下载完成,这时才可以删除设置断点的临时文件

  4. 删除临时文件时,注意线程安全

实现断点下载

  1. 实时的将当前线程的下载进度(已下载文件的大小)写入到文件中

  2. 如果一次没有下载完,再次下载时,应该从上次下载的位置开始下载(即:文件的开始下载位置以及已下载文件的总大小需要实时记录)

  3. 直到所有的线程都下载完毕,才能将所有临时存储的下载文件的大小删除 (这里注意:线程安全问题,需要加锁)

知识点补充

  1. http请求的一个设置信息setRequestProperties("Range", "byte=startIndex-endIndex")

  2. 文件的读写位置都是从0开始的

  3. http协议 : 服务器返回的文件,该文件的读写位置也是从0开始的

  4. RandomAccessFile 的读写模式r,rw,rwd,rws

    • r : 以只读方式打开,调用结果对象的人和write方法都会抛出IOException异常
    • rw : 打开以便读取和写入,如果该文件不存在,则尝试创建该文件
    • rws : 打开以便读取和写入,对于’rw’,还要求对文件内容和元数据的每个更新都同步写入到底层存储设备
    • rwd : 打开以便读取和写入,对于’rw’,还要求对文件内容的每个更新都同步写入到底层存储设备

代码

  • 主函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public class DownLoadDemo {

// 开启的线程数
private static int threadCount = 3;

// 每个下载块的大小
private static long blockSize = 0;

// 正在运行的线程的个数
private static int runningThreadCount = 0;

public static void main(String[] args) {

// 1. 先获得 服务器文件大小
String mUrl = "http://192/168.0.123:8080/ex.exe";
HttpURLConnection conn = null;
try {
URL url = new URL(mUrl);
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setReadTimeout(5000);
int responseCode = conn.getResponseCode();
// 读取成功
if(responseCode / 100 == 2) {

// 得到服务器返回 的文件的大小
long size = conn.getContentLength();
System.out.println("****** 获得服务器文件的大小 : " + size);

// 每个线程下载的文件大小
blockSize = size / threadCount;

// 在本地创建一个同样大小的 空文件 : params:文件对象;文件的读写模式
File file = new File("temp.exe");
RandomAccessFile raf = new RandomAccessFile(file, "rw");

// 设置文件的大小
raf.setLength(size);

// 2. 开启线程,下载文件资源
runningThreadCount = threadCount;
for(int i=0; i<blockSize; i++) {
long startIndex = i*blockSize;
long endIndex = (i+1) * blockSize - 1;
// 最后一个线程下载文件的结束位置
if(i == blockSize-1) {
endIndex = size - 1;
}
System.out.println("开启线程 :" + i + " 执行下载, 下载的位置是 :" + startIndex + " - " + endIndex);
new DownThread(i, startIndex, endIndex, mUrl).start();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(conn != null) {
conn.disconnect();
}
}
}
}
  • 启动线程,执行下载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* 执行下载的线程,内部类
* @author sally
*
*/

private static class DownThread extends Thread {

private int threadId;
private long startIndex;
private long endIndex;
private String path;

public DownThread(int threadId, long startIndex, long endIndex,
String path)
{

super();
this.threadId = threadId;
this.startIndex = startIndex;
this.endIndex = endIndex;
this.path = path;
}

@Override
public void run() {
HttpURLConnection conn = null;
try {
// 断点下载 : 保存当前线程下载文件大小的文件
File positionFile = new File(threadId + ".txt");

// 当前线程下载的文件的总大小
int total = 0;

URL url = new URL(path);
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setReadTimeout(5000);
int responseCode = conn.getResponseCode();
if(responseCode / 100 ==2) {
InputStream is = conn.getInputStream();
File file = new File("temp.exe");
RandomAccessFile raf = new RandomAccessFile(file, "rw");

// 首先,上次下载文件必须存在
if(positionFile.exists() && positionFile.length()>0) {
// 从上一次下载的位置的总大小开始下载
FileInputStream fis = new FileInputStream(positionFile);
BufferedReader reader = new BufferedReader(new InputStreamReader(fis));

// 获取 当前线程上次下载文件的总大小
int lastTotal = Integer.valueOf(reader.readLine());
System.out.println("上次线程 : " + threadId + " 下载的总大小为 : " + lastTotal);

// 开始位置 = 当前位置 + 上次下载文件的总大小
startIndex += lastTotal;

// 下载文件的总大小 = 当前大小 + 上次下载文件的总大小
total += lastTotal;

fis.close();
}

// 指定文件开始写的 位置
raf.seek(startIndex);
System.out.println("第" + threadId + "个线程下载文件的开始位置是 : " + String.valueOf(startIndex-1));

// 开始写文件
byte[] buf = new byte[1024*1024];
int len = 0;

while((len = is.read(buf)) != -1) {
raf.write(buf, 0, len);

// 保存当前线程下载的文件大小
RandomAccessFile rf = new RandomAccessFile(positionFile, "rwd");
total += len;
rf.write(String.valueOf(total).getBytes());
rf.close();
}
is.close();
raf.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 同步代码块,只有当所有为线程都下载完了,才能删除临时文件
synchronized (DownThread.class) {
runningThreadCount--;
if(runningThreadCount < 1) {
System.out.println("所有的线程都下载完毕了");
for(int i=threadCount; i>0; i--) {
File file = new File(i + ".txt");
file.delete();
}
}
}
if(conn != null) {
conn.disconnect();
}
}
}
}