多线程下载介绍
多线程背景知识
多线程下载任务可以更快完成文件的下载,多线程下载文件之所以快,是因为其抢占的服务器资源多。如:假设服务器同时最多服务100个用户,在服务器中一条线程对应一个用户,100条线程在计算机中并非并发执行,而是由cpu划分时间片轮流执行,如果A应用使用了99条线程下载文件,那么相当于占用了99个用户的资源,假设一秒内cpu分配给每条线程的平均执行时间是10ms,A应用在服务器中就得到了990ms,而其他应用在1秒内只有10ms的执行时间。
多线程下载的实现过程
在客户端进行下载的时候,
第一步,在本地创建一个大小与服务器文件相同大小的零时文件。
第二步,计算机分配几个线程去下载服务器上的资源,知道每个线程下载文件的位置。假设服务器上的资源大小是10byte,编号从0到9,现在开启三个线程去服务器下载文件,那么每个线程下载文件的大小就等于文件的长度除以线程的个数,也就是三个线程。所以线程一下载的起始位置是0到2,线程二下载文件的起始位置3到5,最后一个线程特殊一点,下载文件的起始位置为6到文件末尾位置。由此得到每个线程下载文件起始位置的计算方法:开始位置 =(线程id -1)* 每一块的大小,结束位置 = 线程id*每一块的大小-1
第三步,开启多个线程,本例中开启三个线程。、
多线程下载的代码实现
首先,按照上述描述的步骤,链接服务器,获取文件,获取文件的长度,在本地创建一个大小与服务器一样大的临时文件。
public class Demo{
public static void main() throws Exception{
String path ="http://127.0.0.1/post/abc/sleep.gif";
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.setConnectTimeout(5000);
conn.setRequestMethod("GET");
int code = conn.getResponseCode();
if (code == 200){
int length = conn.getContentLength();
System.out.println("文件长度"+length);
}else{
System.out.println("服务器错误");
}
}
}
通过getContentLength()方法得到文件长度。然后就是分配几个线程去下载服务器上的资源文件。假设本例中使用三个线程,那么需要知道每个线程下载文件的大小以及起始位置。可以通过资源的长度除以线程个数得到每个线程下载文件的大小,每个线程下载文件的位置可以根据上面的公式计算得到,这里需要注意的是最后一个线程的处理,如果文件大小不能被整除,则需要对最后一个线程的终点位置做特殊的处理。代码如下:
if (code == 200){
int length = conn.getContentLength();
System.out.println("文件长度"+length);
int blockSize = length/threadCount;
for (int threadId = 1;threadId <= threadCount;threadId++){
int startIndex = (threadId-1)*blockSize-1;
int endIndex = threadId*blockSize-1;
if (threadId == threadCount){
endIndex = length;
}
System.out.println("线程"+threadId+"下载:--"+startIndex+"-->"+endIndex);
}
}
至此,完成了多线程下载的第二步,接下来开启多个线程,让每个线程去下载对应的文件,开辟子线程,需要继承Thread类,并实现Thread中的run方法。在线程需要的几个参数:线程id,线程下载的开始位置,线程下载的结束位置,下载路径。在线程中定义以上变量,将参数传递进来,定义构造方法。代码如下:
public class DownloadThread extends Thread{
private int threadId;
private int startIndex;
private int endIndex;
private String path;
public DownloadThread(String path,int startIndex,int endIndex,int threadId){
this.path = path;
this.startIndex = startIndex;
this.endIndex = endIndex;
this.threadId = threadId;
}
public void run(){
try{
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection)url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Range", "bytes=" + startIndex + "-" + endIndex);
conn.setConnectTimeout(5000);
int code = conn.getResponseCode();
System.out.println(code);
}catch (Exception e){
e.printStackTrace();
}
}
}
对于服务返回的状态码,如果是200,则代表从服务器请求全部资源成功,如果是成功从服务器请求部分资源,则服务器返回的状态码为206。在请求服务器资源的时候,如果使用getInputStream()这个方法返回服务器全部的资源,如果只想从服务器下载一部分的资源,需要通过一个特殊的的Http请求头去指定,使用Http的Range头字段指定每条线程从文件的什么位置开始,下载到什么位置为止。为了让不同线程下载的同一文件写到一个位置,这里需要用到java文件中的一个API:RandomAccessFile这个类,该类的实例支持对随机访问文件的读取和写入。随机访问文件的行为类似存储在文件系统中 的一个大型byte数组,存在向该隐含数组的光标和索引,成为文件指针;输入操作从文件指针开始读取字节,并随着对字节的读取而迁移此指针。如果随机访问文件以随机读取/写入模式创建,则输出操作也是可用的;输出从文件指针开始写入字节,并随着对字节的写入而前移此文件指针。它构造方法中接收的两个参数,一个是文件名,另一个是在随机访问文件在写时候的模式,mode参数指定用以打开文件的访问模式。当我们在主线程中 得到服务器文件的长度后,接下来就可以使用RandomAccessFile这个API去创建这个与服务器上源文件同样大小的临时文件。代码如下:
int length = conn.getContentLength();
System.out.println("文件长度"+length);
//在客户端本地创建一个与服务器端文件大小一样的临时文件
RandomAccessFile raf = new RandomAccessFile("sleep.gif","rwd");
//指定创建文件的大小
raf.setLength(length);
raf.close();
在子线程中代码:
InputStream is = conn.getInputStream();
RandomAccessFile raf = new RandomAccessFile("sleep.gif","rwd");
raf.seek(startIndex);
int len = 0;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer))!=-1){
raf.write(buffer,0,len);
}
is.close();
raf.close();
seek()方法标识随机写文件的时候从哪个位置开始写,多线程下载的时候,第一个线程下载的开始位置是文件开头,第二个线程的下载位置应该是从第二个线程开始的地方存取文件,这样就可以在线程中开辟子线程了。
断点下载
在服务器下载文件的过程中,如果出现断电或者其他意外情况终止下载,希望下次打开下载的时候能从上次未下载完成的位置继续下载,这就需要引入断点下载技术,需要让子线程分别去记住已经下载的长度。可以通过文件读写的方式来下载文件,在开始下载的时候,首先让下载的子线程去读取已经下载的长度,从中来读取已经下载的进度。定义一个文件,用来记录当前线程下载的数据长度。代码如下:
InputStream is = conn.getInputStream();
RandomAccessFile raf = new RandomAccessFile("sleep.gif","rwd");
raf.seek(startIndex);
int len = 0;
byte[] buffer = new byte[1024];
//已经下载的数据的长度
int total = 0;
File file = new File(threadId+".txt");
while ((len = is.read(buffer))!=-1){
FileOutputStream fos = new FileOutputStream(file);
raf.write(buffer,0,len);
total+=len;
fos.write(String.valueOf(total).getBytes());
fos.close();
}
为了记录已经下载的文件长度记录,需要在每个线程开始的地方检查是否存在记录下载长度的文件,如果存在,读取这个文件数据,把它添加到开始下载的位置里面去:
代码如下:
public void run(){
try{
File tempFile = new File(threadId+".txt");
if (tempFile.exists() && tempFile.length()>0){
FileInputStream fis = new FileInputStream(tempFile);
byte[] temp = new byte[1024];
int leng = fis.read(temp);
String downloadLen = new String(temp,0,leng);
int downloadLenInt = Integer.parseInt(downloadLen);
///修改下载的真正的位置
startIndex = downloadLenInt;
fis.close();
}
文件下载完成后,需要将下载文件清除。