刚学了下多线程的下载,可能是初次接触的原因吧,理解起来觉得稍微有点难。所以想写一篇博客来记录下,加深自己理解的同时,也希望能够帮到一些刚接触的小伙伴。由于涉及到网络的传输,那么就会涉及到http协议。建议在读本文之前您对http协议有一定的了解。
线程可以通俗的理解为下载的通道,一个线程就是文件下载的一个通道,多线程就是同时打开了多个通道对文件进行下载。当服务器提供下载服务时,用户之间共享带宽,在优先级相同的情况下,总服务器会对总下载线程进行平均分配。我们平时用的迅雷下载就是多线程下载。
多线程的下载大致可以分为如下几个步骤:
1: 获取目标文件的大小(totalSize)
按照常识,我们在下载一个文件之前,通常情况下是要知道该文件的大小,这样才好在本地留好足量的空间来存储,免得出现还未下载完,存储空间就爆了的情况。为了方便代码的演示,本文在本地tomcat服务器的webapps/ROOT目录下新建一个test.txt的文件,里面存储了0123456789这10字节的数据。
2: 确定要开启几个线程(threadCount)
需要的文件在服务器上,那我们要开通几个通道去下载呢?一般情况下这是由CPU去决定的,但是CPU开启线程的数目也是有限的,不是想开几个线程就开几个线程。所开线程的最大数量=(CPU核数+1),例如你的CPU核数为4,那么电脑最多可以开启5条线程。为了方便代码演示,本文的threadCount=3
3: 计算平均每个线程需要下载多少个字节的数据(blockSize)
理想情况下多线程下载是按照平均分配原则的,即:单线程下载的字节数等于文件总大小除以开启的线程总条数,当不能整除时,则最后开启的线程将剩余的字节一起下载。例如:本文中的totalSize=10,threadCount=3,则前两个开启的线程下载3KB的数据,第三个开启的线程需要下载(3+1)KB的数据。
4:计算各个线程要下载的字节范围。
平时我们做项目讲究分工明确,同理多线程下载也需要明确各个下载的字节范围,这样才能将文件高效、快速、准确的下载下来。即在下载过程中,各个线程都要明确自己的开始索引(startIndex)和结束索引(endIndex)。
从上图我们可以总结出一个公式: startIndex = threadId乘以blockSize; endIndex = (threadId+1)乘以blockSize-1; 如果是最后一条线程,那么结束索引为: endIndex = totalSize - 1;
5: 使用for循环开启3个子线程
//每次循环启动一条线程下载
for(int threadId=0; threadId<3;threadId++){
/**
* 计算各个线程要下载的字节范围
*/
//开始索引
int startIndex = threadId * blockSize;
//结束索引
int endIndex = (threadId+1)* blockSize-1;
//如果是最后一条线程(因为最后一条线程可能会长一点)
if(threadId == (threadCount -1)){
endIndex = totalSize -1;
}
/**
* 启动子线程下载
*/
new DownloadThread(threadId,startIndex,endIndex).start();
}
6:获取各个线程的目标文件的开始索引和结束索引的范围。
告诉服务器,只要目标段的数据,这样就需要通过Http协议的请求头去设置(range:bytes=0-499 )
connection.setRequestProperty("range", "bytes="+startIndex+"-"+endIndex);
7:使用RandomAccessFile随机文件访问类。创建一个RandomAccessFile对象,将返回的字节流写到文件指定的范围
此处有个注意事项:让RandomAccessFile对象写字节流之前,需要移动RandomAccessFile对象到指定的位置开始写。
raf.seek(startIndex);
以上就是多线程下载的大致步骤。代码如下:
package com.example;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;
public class DownloadTest {
private static final String path = "http://localhost:8080/test.txt";
public static void main(String[] args) throws Exception {
/**
* 1.获取目标文件的大小
*/
int totalSize = new URL(path).openConnection().getContentLength();
System.out.println("目标文件的总大小为:"+totalSize+"B");
/**
*2. 确定开启几个线程
*开启线程的总数=CPU核数+1;例如:CPU核数为4,则最多可开启5条线程
*/
int availableProcessors = Runtime.getRuntime().availableProcessors();
System.out.println("CPU核数是:"+availableProcessors);
int threadCount = 3;
/**
* 3. 计算每个线程要下载多少个字节
*/
int blockSize = totalSize/threadCount;
//每次循环启动一条线程下载
for(int threadId=0; threadId<3;threadId++){
/**
* 4.计算各个线程要下载的字节范围
*/
//开始索引
int startIndex = threadId * blockSize;
//结束索引
int endIndex = (threadId+1)* blockSize-1;
//如果是最后一条线程(因为最后一条线程可能会长一点)
if(threadId == (threadCount -1)){
endIndex = totalSize -1;
}
/**
* 5.启动子线程下载
*/
new DownloadThread(threadId,startIndex,endIndex).start();
}
}
//下载的线程类
private static class DownloadThread extends Thread{
private int threadId;
private int startIndex;
private int endIndex;
public DownloadThread(int threadId, int startIndex, int endIndex) {
super();
this.threadId = threadId;
this.startIndex = startIndex;
this.endIndex = endIndex;
}
@Override
public void run(){
System.out.println("第"+threadId+"条线程,下载索引:"+startIndex+"~"+endIndex);
//每条线程要去×××器拿取目标段的数据
try {
//创建一个URL对象
URL url = new URL(path);
//开启网络连接
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
//添加配置
connection.setConnectTimeout(5000);
/**
* 6.获取目标文件的[startIndex,endIndex]范围
*/
//告诉服务器,只要目标段的数据,这样就需要通过Http协议的请求头去设置(range:bytes=0-499 )
connection.setRequestProperty("range", "bytes="+startIndex+"-"+endIndex);
connection.connect();
//获取响应码,注意,由于服务器返回的是文件的一部分,因此响应码不是200,而是206
int responseCode = connection.getResponseCode();
//判断响应码的值是否为206
if (responseCode == 206) {
//拿到目标段的数据
InputStream is = connection.getInputStream();
/**
* 7:创建一个RandomAccessFile对象,将返回的字节流写到文件指定的范围
*/
//获取文件的信息
String fileName = getFileName(path);
//rw:表示创建的文件即可读也可写。
RandomAccessFile raf = new RandomAccessFile("d:/"+fileName, "rw");
/**
* 注意:让raf写字节流之前,需要移动raf到指定的位置开始写
*/
raf.seek(startIndex);
//将字节流数据写到file文件中
byte[] buffer = new byte[1024];
int len = 0;
while((len=is.read(buffer))!=-1){
raf.write(buffer, 0, len);
}
//关闭资源
is.close();
raf.close();
System.out.println("第 "+ threadId +"条线程下载完成 !");
} else {
System.out.println("下载失败,响应码是:"+responseCode);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
//获取文件的名称
private static String getFileName(String path){
//http://localhost:8080/test.txt
int index = path.lastIndexOf("/");
String fileName = path.substring(index+1);
return fileName ;
}
}
示例代码运行结果如下:
目标文件的总大小为:10B CPU核数是:4 第0个线程,下载索引:0~2 第1个线程,下载索引:3~5 第2个线程,下载索引:6~9 第1个线程下载完成! 第2个线程下载完成! 第0个线程下载完成!
好了,本文写到此为止。以上是我个人对多线程下载的初步理解,如有不妥之处,还望大家多多指点,感谢!让我们共同学习,一起进步。