【Java】网络编程——多线程下载文件

本文详细介绍了一种利用多线程技术加速文件下载的方法。通过将文件分割为多个部分,每个部分由单独的线程负责下载,从而实现并行下载,提高下载速度。文章提供了完整的代码示例,包括如何计算每个线程的下载范围、设置HTTP请求的Range头以及使用RandomAccessFile进行文件的随机写入。

前言

多线程下载文件,比单线程要快,当然,线程不是越多越好,这和获取的源文件还有和网速有关。

原理:在请求服务器的某个文件时,我们能得到这个文件的大小长度信息,我们就可以下载此长度的某一个片段,来达到多线程下载的目的!每条线程分别下载他们自己的片段!

下载流程(代码片段)

1.  根据访问的URL路径调用openConnection()获得HttpURLConnection对象,接着调用getContentLengthLong()方法获得文件的  字节大小,然后通过RandomAccessFile对象调用setLength()设置本地文件的长度。(这个文件是null数据文件,通过多线程进行对RandomAccessFile对象的本地文件随机位置写入数据)

URL url = new URL(str_url);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
long fileLength = conn.getContentLengthLong(); // 得到需要下载的文件大小
RandomAccessFile file = new RandomAccessFile(storagePath, "rwd");
file.setLength(fileLength); // 关键方法 : 设置本地文件长度
file.close();
conn.disconnect();

2. 根据获得的文件长度,计算每条线程下载的起始位置与结束位置,因为不一定平均分,所以最后一条线程下载剩余的字节

long oneThreadReadByteLength = fileLength / threadNumber;
for (int i = 0; i < threadNumber; i++) {
	long startPosition = i * oneThreadReadByteLength;
	long endPosition = i == threadNumber - 1 ? fileLength : (i + 1) * oneThreadReadByteLength - 1;
}

3. 每条线程请求的范围参数设置,请求头参数 : Range:bytes=0-length

URL url = new URL(str_url);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("Range", "bytes=" + startPosition + "-" + endPosition); // 关键方法: 每条线程请求的字节范围

4. 每条线程存储数据到文件的起始位置设置(RandomAccessFile的seek()方法),以及响应码206判断

if (conn.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) { // 关键响应码 :206,请求成功 + 请求数据字节范围成功
	RandomAccessFile file = new RandomAccessFile(storagePath, "rwd");
	file.seek(startPosition); // 关键方法 :每条线程起始写入文件的位置
	InputStream in = conn.getInputStream();
	byte[] buf = new byte[8192];
	int len;
	while ((len = in.read(buf)) > 0) {
		file.write(buf, 0, len);
	}
}

完整代码

main.java

public class Main {
	
	public static void main(String[] args) throws Exception {
		
		String path = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1588346633185&di=8f8b2b357c8461d232fcce9e0c476f3a&imgtype=0&src=http%3A%2F%2Fi0.hdslb.com%2Fbfs%2Farticle%2Fe79d2c22d40a801e7b02183dee9db3e5c71514af.jpg";
		MultiThreadDownload mtd = new MultiThreadDownload(path, "G:\\LeiMus.jpg", 3);
		mtd.download();
		
	}

}

MultiThreadDownload.java

package com.bin.demo;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.net.HttpURLConnection;
import java.net.URL;

public class MultiThreadDownload {
	
	private String str_url;
	private String storagePath;
	private int threadNumber;
	private static long downloadByteCount;
	
	MultiThreadDownload(String str_url, String storagePath, int threadNumber) {
		this.str_url = str_url;
		this.storagePath = storagePath;
		this.threadNumber = threadNumber;
	}
	
	public void download() throws IOException, InterruptedException {
		long startTime = System.currentTimeMillis();
		System.out.println("Download......");
		
		/*
		 *  首先设置本地文件的大小
		 *  当然这是个null数据的文件
		 *  这样才能通过RandomAccessFile的数组下标机制达到随机位置写入
		 */
		URL url = new URL(str_url);
		HttpURLConnection conn = (HttpURLConnection) url.openConnection();
		conn.setConnectTimeout(10000);
		conn.setRequestMethod("GET");
		long fileLength = conn.getContentLengthLong(); // 得到需要下载的文件大小
		conn.disconnect();
		RandomAccessFile file = new RandomAccessFile(storagePath, "rwd");
		file.setLength(fileLength); // 关键方法 : 设置本地文件长度
		file.close();
		
		/*
		 *  计算每条线程下载的字节数,以及每条线程起始下载位置与结束的下载位置,
		 *  因为不一定平均分,所以最后一条线程下载剩余的字节
		 *  然后创建线程任务并启动
		 *  Main线程等待每条线程结束(join()方法)
		 */
		long oneThreadReadByteLength = fileLength / threadNumber;
		for (int i = 0; i < threadNumber; i++) {
			long startPosition = i * oneThreadReadByteLength;
			long endPosition = i == threadNumber - 1 ? fileLength : (i + 1) * oneThreadReadByteLength - 1;
			Thread t = new Thread(new Task(startPosition, endPosition));
			t.start();
			t.join();
		}
		
		/*
		 *  检查文件是否下载完整,不完整则删除
		 */
		if (downloadByteCount == fileLength) {
			System.out.println("ALL Thread Download OK.");
			System.out.println("time = " + ((System.currentTimeMillis() - startTime) / 1000) + " S");
		} else {
			System.out.println("Download Error.");
			new File(storagePath).delete();
		}
	}
	
	class Task implements Runnable {
		
		private long startPosition;
		private long endPosition;
		
		Task(long startPosition, long endPosition) {
			this.startPosition = startPosition;
			this.endPosition = endPosition;
		}

		@Override
		public void run() {
			try {
				URL url = new URL(str_url);
				HttpURLConnection conn = (HttpURLConnection) url.openConnection();
				conn.setConnectTimeout(10000);
				conn.setRequestMethod("GET");
				conn.setRequestProperty("Range", "bytes=" + startPosition + "-" + endPosition); // 关键方法: 每条线程请求的字节范围
				if (conn.getResponseCode() == HttpURLConnection.HTTP_PARTIAL) { // 关键响应码 :206,请求成功 + 请求数据字节范围成功
					RandomAccessFile file = new RandomAccessFile(storagePath, "rwd");
					file.seek(startPosition); // 关键方法 :每条线程起始写入文件的位置
					InputStream in = conn.getInputStream();
					byte[] buf = new byte[8192];
					int len;
					while ((len = in.read(buf)) > 0) {
						file.write(buf, 0, len);
						downloadByteCount += len;
					}
					// 关闭网络连接及本地流
					in.close();
					file.close();
					conn.disconnect();
					System.out.println(Thread.currentThread().getName() + ": download OK");
				}
			} catch (IOException e) {
				System.out.println(Thread.currentThread().getName() + "_Error : " + e);
			}
		}
		
	}

}

输出:

Download......
Thread-0: download OK
Thread-1: download OK
Thread-2: download OK
ALL Thread Download OK.
time = 1 S

我下载的是百度图片,另外多线程下载的结果时间和网速和文件大小有关。

附加我的结果图片:

1.得到服务器下载文件的大小,然后在本地设置一个临时文件和服务器端文件大小一致 a)获得访问网络地址 b)通过URL对象的openConnection()方法打开连接,返回一个连接对象 c)设置请求头 i.setRequestMethod ii.setConnectTimeout iii.setReadTimeout d)判断是否响应成功 e)获取文件长度(getContentLength()) f)随机访问文件的读取与写入RandomAccessFile(file, mode) g)设置临时文件与服务器文件大小一致(setLength()) h)关闭临时文件 2.计算出每个线程下载的大小(开始位置,结束位置) a)计算出每个线程下载的大小 b)for循环,计算出每个线程的开始、结束位置 c)最后一个线程处理 3.每创建好一次就要开启线程下载 a)构造方法 b)通过URL对象的openConnection()方法打开连接,返回一个连接对象 c)设置请求头 i.setRequestMethod ii.setConnectTimeout d)判断是否响应成功(206) e)获取每个线程返回的流对象 f)随机访问文件的读取与写入RandomAccessFile(file, mode) g)指定开始位置 h)循环读取 i.保存每个线程下载位置 ii.记录每次下载位置 iii.关闭临时记录位置文件 iv.随机本地文件写入 v.记录已下载大小 i)关闭临时文件 j)关闭输入流 4.为了杀死线程还能继续下载的情况下,从本地文件上读取已经下载文件的开始位置 a)创建保存记录结束位置的文件 b)读取文件 c)将流转换为字符 d)获取记录位置 e)把记录位置赋给开始位置 5.当你的n个线程都下载完毕的时候我进行删除记录下载位置的缓存文件 a)线程下载完就减去 b)当没有正在运行的线程时切文件存在时删除文件
评论 14
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

虚妄狼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值