刚学了下多线程的下载,可能是初次接触的原因吧,理解起来觉得稍微有点难。所以想写一篇博客来记录下,加深自己理解的同时,也希望能够帮到一些刚接触的小伙伴。由于涉及到网络的传输,那么就会涉及到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个线程下载完成!


好了,本文写到此为止。以上是我个人对多线程下载的初步理解,如有不妥之处,还望大家多多指点,感谢!让我们共同学习,一起进步。