Java多线程分块下载文件
目录
技术原理
完整代码实现
主程序类(Test.java)
下载线程类(DownThread.java)
关键代码解析
1. 分块算法解析
2. HTTP Range请求头
3. RandomAccessFile的使用
运行结果
技术原理
多线程下载的核心思想是:将一个大文件分成若干块,每个线程负责下载其中一块,最后将所有块合并成完整文件。其关键技术点包括:
-
文件分割算法:将文件总大小平均分配给每个线程,计算每个线程负责下载的起始和结束位置。
-
HTTP Range 请求:通过 HTTP 协议的
Range
头字段,告知服务器只返回文件的特定片段(从 start 到 end 的字节范围)。 -
随机文件写入:使用
RandomAccessFile
类,支持线程将下载的片段写入文件的指定位置,避免线程间的写入冲突。 -
多线程协同:多个线程并行工作,各自完成分配的下载任务。
完整代码实现
主程序类(Test.java)
package com.splitfile;import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;/*** 多线程分段下载文件的主程序* 功能:将一个网络文件分割成多个部分,使用指定数量的线程并行下载*/
public class Test {/*** savePath 本地保存路径(包含文件名)* fileUrl 目标文件的网络URL* threadNum 下载线程数量*/public static void downFile(String savePath, String fileUrl, int threadNum) {// 创建本地文件及父目录File targetFile = new File(savePath);if (!targetFile.getParentFile().exists()) {targetFile.getParentFile().mkdirs(); // 确保父目录存在}try {// 创建URL对象并打开连接URL url = new URL(fileUrl);HttpURLConnection conn = (HttpURLConnection) url.openConnection();// 设置连接超时时间(30秒)conn.setConnectTimeout(30000);// 设置请求方式为GETconn.setRequestMethod("GET");// 检查连接是否成功(响应码200表示成功)if (conn.getResponseCode() == 200) {System.out.println("连接到文件服务器成功!");// 获取文件总大小(单位:字节)int fileSize = conn.getContentLength();System.out.println("文件总大小:" + fileSize + " 字节");// 计算每个线程需要下载的字节块大小// 算法:(总大小 + 线程数 - 1) / 线程数 → 实现向上取整,避免最后一个线程分配过多int blockSize = (fileSize + threadNum - 1) / threadNum; System.out.println("每个线程下载块大小:" + blockSize + " 字节");// 启动指定数量的下载线程for (int i = 0; i < threadNum; i++) {new DownThread(blockSize, i, url, targetFile,fileSize).start();}} else {System.out.println("连接失败,响应码:" + conn.getResponseCode());}} catch (MalformedURLException e) {System.out.println("URL格式错误:" + e.getMessage());e.printStackTrace();} catch (IOException e) {System.out.println("网络IO异常:" + e.getMessage());e.printStackTrace();}}public static void main(String[] args) {// 测试下载:保存路径、文件URL、线程数量downFile("./mp3/a.mp3", "http://127.0.0.1:8099/audio/relax1.mp3", 3);}
}
下载线程类(DownThread.java)
package com.splitfile;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 DownThread extends Thread {private int blockSize; // 每个线程的下载块大小private int threadIndex; // 线程索引(0开始)private URL fileUrl; // 目标文件的URLprivate File targetFile; // 本地保存的文件private int fileSize; // 文件大小private int startPoint; // 本线程下载的起始位置(字节)private int endPoint; // 本线程下载的结束位置(字节)/*** blockSize 每个线程的下载块大小* threadIndex 线程索引* url 目标文件URL* file 本地保存文件*/public DownThread(int blockSize, int threadIndex, URL url, File file, int fileSize) {this.blockSize = blockSize;this.threadIndex = threadIndex;this.fileUrl = url;this.targetFile = file;this.fileSize = fileSize;// 计算每个线程的起始下载位置this.startPoint = blockSize * threadIndex;// 计算每个线程的结束下载位置,因为blockSize是向上取整得到的,所以最后一个线程取小的字节数下载this.endPoint = Math.min(blockSize * (threadIndex + 1) - 1, fileSize - 1);}/*** 线程执行体:下载指定范围的文件片段*/@Overridepublic void run() {// 打印当前线程的下载范围System.out.println(Thread.currentThread().getName() + " 下载范围:" + startPoint + " - " + endPoint + " 字节");RandomAccessFile raf = null;InputStream in = null;try {// 打开与服务器的连接HttpURLConnection conn = (HttpURLConnection) fileUrl.openConnection();conn.setConnectTimeout(30000);conn.setRequestMethod("GET");// 设置Range请求头:告知服务器只返回指定范围的字节conn.setRequestProperty("Range", "bytes=" + startPoint + "-" + endPoint);// 服务器返回206表示部分内容请求成功(Range生效)if (conn.getResponseCode() == 206) {// 获取输入流(仅包含指定范围的文件内容)in = conn.getInputStream();// 创建RandomAccessFile用于随机写入文件raf = new RandomAccessFile(targetFile, "rw");// 移动文件指针到本线程的起始位置raf.seek(startPoint);// 缓冲区:提高读写效率byte[] buffer = new byte[1024];int len;// 读取数据并写入文件while ((len = in.read(buffer)) != -1) {raf.write(buffer, 0, len);}System.out.println(Thread.currentThread().getName() + " 下载完成!");} else {System.out.println(Thread.currentThread().getName() + " 范围请求失败,响应码:" + conn.getResponseCode());}} catch (IOException e) {System.out.println(Thread.currentThread().getName() + " 下载异常:" + e.getMessage());e.printStackTrace();} finally {// 关闭资源try {if (in != null) in.close();if (raf != null) raf.close();} catch (IOException e) {e.printStackTrace();}}}
}
关键代码解析
1. 分块算法解析
int block = (fileSize + threadNum - 1) / threadNum;
- 这是向上取整算法
- 确保所有块的大小总和能够覆盖整个文件
- 例如:文件100字节,3个线程 → (100+3-1)/3 = 34字节/块
2. HTTP Range请求头
conn.setRequestProperty("Range", "bytes=" + startPoint + "-" + endPoint);
- 这是分段下载的核心,通过 HTTP 的
Range
头告诉服务器只返回指定字节范围的数据。 - 服务器成功响应时会返回 206 状态码(部分内容)。
3. RandomAccessFile的使用
raf = new RandomAccessFile(targetFile, "rw");
raf.seek(startPoint); // 移动到起始位置
raf.write(buffer, 0, len); // 写入数据
RandomAccessFile
支持通过seek()
方法定位到文件的任意位置。- 多个线程可以同时写入同一个文件的不同位置,确保每个线程写入的数据不会覆盖其他线程的内容。