目录
[1. 分块算法解析](#1. 分块算法解析)
[2. HTTP Range请求头](#2. HTTP Range请求头)
[3. RandomAccessFile的使用](#3. RandomAccessFile的使用)
技术原理
多线程下载的核心思想是:将一个大文件分成若干块,每个线程负责下载其中一块,最后将所有块合并成完整文件。其关键技术点包括:
-
文件分割算法:将文件总大小平均分配给每个线程,计算每个线程负责下载的起始和结束位置。
-
HTTP Range 请求 :通过 HTTP 协议的
Range
头字段,告知服务器只返回文件的特定片段(从 start 到 end 的字节范围)。 -
随机文件写入 :使用
RandomAccessFile
类,支持线程将下载的片段写入文件的指定位置,避免线程间的写入冲突。 -
多线程协同:多个线程并行工作,各自完成分配的下载任务。
完整代码实现
主程序类(Test.java)
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);
// 设置请求方式为GET
conn.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)
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; // 目标文件的URL
private 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);
}
/**
* 线程执行体:下载指定范围的文件片段
*/
@Override
public 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. 分块算法解析
java
int block = (fileSize + threadNum - 1) / threadNum;
- 这是向上取整算法
- 确保所有块的大小总和能够覆盖整个文件
- 例如:文件100字节,3个线程 → (100+3-1)/3 = 34字节/块
2. HTTP Range请求头
java
conn.setRequestProperty("Range", "bytes=" + startPoint + "-" + endPoint);
- 这是分段下载的核心,通过 HTTP 的
Range
头告诉服务器只返回指定字节范围的数据。 - 服务器成功响应时会返回 206 状态码(部分内容)。
3. RandomAccessFile的使用
java
raf = new RandomAccessFile(targetFile, "rw");
raf.seek(startPoint); // 移动到起始位置
raf.write(buffer, 0, len); // 写入数据
RandomAccessFile
支持通过seek()
方法定位到文件的任意位置。- 多个线程可以同时写入同一个文件的不同位置,确保每个线程写入的数据不会覆盖其他线程的内容。
运行结果

