在日常的开发中难免会遇到进行文件下载的需求,虽然有很多比较优秀的网络请求框架能够帮助我们实现文件的下载 例如XUtils,okhttp等,但是作为一个靠技术吃饭的程序员,也必定需要理解多线程下载的一些原理。废话少数,下面看效果:
下面需要明白一个问题,为什么使用多线程下载文件的速度比单线程的速度要快?
一般来说决定下载速度的因素一般有两个:一个是进行文件下载的客户端的带宽,另一个就是要去请求下载的服务器端的带宽。由于客户端的实际网速是不可控的,所以在进行下载的时候主要考虑到服务器端的带宽,由于服务器端下载的带宽不是根据进行下载的用户数来进行分配的,而是根据接入的线程数来平均分配下载带宽,因此当一个程序进行下载操作的时候运行的线程越多所获得的带宽就越多因此下载的速度就越快。
文件的拆分:
由于要进行多线程下载,这里我们在下载的时候就要对下载的文件进行拆分,把文件分成小块,让线程去负责每个子文件的下载,这里我们就要思考一个问题,如何给线程分配要下载的每小块文件长度,也就是下载文件的起始位置和结束位置。
可以通过下面的这个公式:
前n-1个线程的子部分文件的尺寸 : size = len/threadCount
最后一部分的大小 = len - (n-1)*size
由于无法平均分配每个线程要下载文件的长度(不能出现小数的情况),所以这里采取给前(n-1)个线程进行平均分配,让最后一个线程处理最后文件剩余部分的大小。
文件的存储:
当得到要下载资源文件的大小就要创建相应大小的文件来进行磁盘空间的占用(避免在下载的过程中出现空间不足的问题)。由于是进行多进程下载,下载的过程中就需要考虑如何将单个线程下载的文件拼成一个文件,这里我们就需要用到 RandomAccessFile 进行文件的创建和写入,可以通过seek(long pos)方法指定文件开始写入的位置,这样就实现了多个线程下载文件拼接的问题。
Range请求头:
由于每个线程获取的输入流不能是从文件的起始位置,而是对应当前线程所分配的文件块在整个文件中的起始位置与结束位置。这时候我们就需要在请求的时候加上这个Http请求头connection.setRequestProperty("Range","bytes="+startIndex+"-"+endIndex);
告诉服务器我当前需要的流的开始位置和结束位置。
上面几点就是进行多线程下载所涉及到的知识点,下面上代码:
/**
* Created by wangke on 2017/3/30.
* 实现多线程下载的功能
*/
public class MultiThreadDownLoad {
//psvm 补全Main方法快捷键
//sout 打印
private static final String filePath = "D:/Zootopia.mp4";
private static String url = "http://172.22.18.7:9090/Movie/Zootopia.mp4";
//下载文件的线程数
private static int mThreadCount = 8;
public static void main(String[] args) {
try {
//获取要进行下载文件的长度
URL mUrl = new URL(url);
HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
connection.setDoInput(true);
connection.setRequestMethod("GET");
long contentLength = connection.getContentLengthLong();
System.out.println("要下载文件的长度:"+contentLength);
//根据文件的长度创建一个空的文件
RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "rw");
//提前设置长度进行占位
randomAccessFile.setLength(contentLength);
// 计算每个子线程要进行下载的文件块的数据的起始位置和结束位置
for(int i=0;i<mThreadCount;i++){
long startIndex = i*(contentLength/(mThreadCount-1));
Long endIndex = (i+1)*(contentLength/(mThreadCount-1))-1;
//最后一个线程下载最后剩下的文件块
if(mThreadCount-1 == i){
endIndex = contentLength-1;
}
//创建线程进行分块下载
DownLoadThread downLoadThread = new DownLoadThread(startIndex, endIndex, MultiThreadDownLoad.url, "线程:" + i);
downLoadThread.setName("线程:"+i);
downLoadThread.start();
System.out.println("第"+i+"线程:"+"起始位置:"+startIndex+" 结束位置:"+endIndex);
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
}
}
static class DownLoadThread extends Thread{
private long startIndex;
private long endIndex;
private String downLoadUrl;
public DownLoadThread(long startIndex, long endIndex, String downLoadUrl, String threadName) {
this.startIndex = startIndex;
this.endIndex = endIndex;
this.downLoadUrl = downLoadUrl;
}
@Override
public void run() {
try {
HttpURLConnection connection = (HttpURLConnection) new URL(downLoadUrl).openConnection();
connection.setDoInput(true);
connection.setDoOutput(true);
connection.setRequestMethod("GET");
connection.setConnectTimeout(10000);
connection.setReadTimeout(10000);
//设置请求头,告知服务器从指定的位置返回数据
connection.setRequestProperty("Range","bytes="+startIndex+"-"+endIndex);
int responseCode = connection.getResponseCode();
System.out.println("服务器返回的响应码:"+responseCode);
if(responseCode == 206){
//获取要下载文件的流
InputStream is = connection.getInputStream();
RandomAccessFile AccessFile = new RandomAccessFile(filePath, "rw");
//指定文件写入的开始位置
AccessFile.seek(startIndex);
byte[] bytes = new byte[1024];
int len = -1;
int count = 0;
while ((len = is.read(bytes))!=-1){
AccessFile.write(bytes,0,len);
count+=bytes.length;
System.out.println(getName()+"的下载进度:"+count);
}
System.out.println(getName()+"下载完成!!!");
AccessFile.close();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
}
}
}
}
这样一个多线程下载的任务就完成了O(∩_∩)O哈哈~