一、项目概述
多线程下载器通过将大文件分割成多个部分,使用多个线程同时下载不同部分,最后合并成一个完整文件,可以显著提高下载速度。本教程将实现一个功能完整的Java多线程下载器。
二、项目结构设计
/src /main /java /com /downloader MultiThreadDownloader.java # 主下载器类 DownloadTask.java # 下载任务类 ProgressBar.java # 进度条显示类 FileUtils.java # 文件工具类 /resources /test /java /com /downloader DownloadTest.java # 测试类
三、核心组件实现
1. 主下载器类 (MultiThreadDownloader.java)
package com.downloader; import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class MultiThreadDownloader { private String fileUrl; private String savePath; private int threadCount; private long fileSize; private ProgressBar progressBar; public MultiThreadDownloader(String fileUrl, String savePath, int threadCount) { this.fileUrl = fileUrl; this.savePath = savePath; this.threadCount = threadCount; this.progressBar = new ProgressBar(); } public void download() throws Exception { // 获取文件大小 fileSize = getFileSize(); System.out.println("文件大小: " + FileUtils.formatFileSize(fileSize)); // 创建空文件 RandomAccessFile file = new RandomAccessFile(savePath, "rw"); file.setLength(fileSize); file.close(); // 计算每个线程下载的字节范围 long chunkSize = fileSize / threadCount; // 创建线程池 ExecutorService executor = Executors.newFixedThreadPool(threadCount); // 提交下载任务 for (int i = 0; i < threadCount; i++) { long startByte = i * chunkSize; long endByte = (i == threadCount - 1) ? fileSize - 1 : (i + 1) * chunkSize - 1; executor.execute(new DownloadTask(fileUrl, savePath, i, startByte, endByte, progressBar)); } // 显示进度条 progressBar.start(fileSize); // 关闭线程池 executor.shutdown(); // 等待所有任务完成 try { executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("下载被中断", e); } // 停止进度条 progressBar.stop(); System.out.println("n下载完成! 文件保存至: " + savePath); } private long getFileSize() throws IOException { URL url = new URL(fileUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("HEAD"); int responseCode = conn.getResponseCode(); if (responseCode != HttpURLConnection.HTTP_OK) { throw new IOException("服务器返回错误响应: " + responseCode); } String contentLength = conn.getHeaderField("Content-Length"); if (contentLength == null) { throw new IOException("无法获取文件大小"); } return Long.parseLong(contentLength); } }
2. 下载任务类 (DownloadTask.java)
package com.downloader; import java.io.IOException; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.URL; public class DownloadTask implements Runnable { private String fileUrl; private String savePath; private int threadId; private long startByte; private long endByte; private ProgressBar progressBar; public DownloadTask(String fileUrl, String savePath, int threadId, long startByte, long endByte, ProgressBar progressBar) { this.fileUrl = fileUrl; this.savePath = savePath; this.threadId = threadId; this.startByte = startByte; this.endByte = endByte; this.progressBar = progressBar; } @Override public void run() { InputStream inputStream = null; RandomAccessFile file = null; try { // 创建HTTP连接 URL url = new URL(fileUrl); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("Range", "bytes=" + startByte + "-" + endByte); int responseCode = conn.getResponseCode(); if (responseCode != HttpURLConnection.HTTP_PARTIAL) { throw new IOException("服务器不支持范围请求,响应码: " + responseCode); } // 获取输入流 inputStream = conn.getInputStream(); // 随机访问文件写入数据 file = new RandomAccessFile(savePath, "rw"); file.seek(startByte); byte[] buffer = new byte[4096]; int bytesRead; long totalRead = 0; while ((bytesRead = inputStream.read(buffer)) != -1) { file.write(buffer, 0, bytesRead); totalRead += bytesRead; // 更新进度 progressBar.update(bytesRead); } System.out.println("线程 " + threadId + " 下载完成: " + (endByte - startByte + 1) + " 字节"); } catch (IOException e) { System.err.println("线程 " + threadId + " 下载出错: " + e.getMessage()); } finally { try { if (inputStream != null) inputStream.close(); if (file != null) file.close(); } catch (IOException e) { System.err.println("关闭资源出错: " + e.getMessage()); } } } }
3. 进度条显示类 (ProgressBar.java)
package com.downloader; public class ProgressBar { private volatile long totalDownloaded = 0; private long totalSize; private boolean running = false; private Thread displayThread; public void start(long totalSize) { this.totalSize = totalSize; this.running = true; displayThread = new Thread(() -> { while (running) { printProgress(); try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } printProgress(); // 最终打印一次 }); displayThread.start(); } public void stop() { running = false; try { displayThread.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } public synchronized void update(long bytes) { totalDownloaded += bytes; } private synchronized void printProgress() { double percent = (double) totalDownloaded / totalSize * 100; int filledLength = (int) (50 * totalDownloaded / totalSize); StringBuilder bar = new StringBuilder("["); for (int i = 0; i < 50; i++) { if (i "); } else { bar.append(" "); } } bar.append("]"); System.out.printf("r%s %.2f%% (%s/%s)", bar.toString(), percent, FileUtils.formatFileSize(totalDownloaded), FileUtils.formatFileSize(totalSize)); } }
4. 文件工具类 (FileUtils.java)
package com.downloader; public class FileUtils { public static String formatFileSize(long size) { if (size < 1024) { return size + " B"; } else if (size < 1024 * 1024) { return String.format("%.2f KB", size / 1024.0); } else if (size < 1024 * 1024 * 1024) { return String.format("%.2f MB", size / (1024.0 * 1024)); } else { return String.format("%.2f GB", size / (1024.0 * 1024 * 1024)); } } }
四、使用示例
1. 创建测试类 (DownloadTest.java)
package com.downloader; import java.util.Scanner; public class DownloadTest { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("=== Java多线程下载器 ==="); System.out.print("请输入下载URL: "); String url = scanner.nextLine(); System.out.print("请输入保存路径: "); String savePath = scanner.nextLine(); System.out.print("请输入线程数(建议1-8): "); int threadCount = scanner.nextInt(); try { MultiThreadDownloader downloader = new MultiThreadDownloader(url, savePath, threadCount); downloader.download(); } catch (Exception e) { System.err.println("下载失败: " + e.getMessage()); e.printStackTrace(); } finally { scanner.close(); } } }
2. 运行示例
=== Java多线程下载器 === 请输入下载URL: https://example.com/largefile.zip 请输入保存路径: C:/Downloads/largefile.zip 请输入线程数(建议1-8): 4 文件大小: 256.78 MB [=================> ] 45.32% (116.42 MB/256.78 MB)
五、高级功能扩展
1. 断点续传实现
通过保存每个线程的下载进度,可以在程序重启后继续未完成的下载:
// 在DownloadTask中添加断点续传支持 private long currentByte; public void run() { // 读取保存的进度 long startPoint = readDownloadProgress(); // 修改Range请求 conn.setRequestProperty("Range", "bytes=" + startPoint + "-" + endByte); // 下载过程中保存进度 saveDownloadProgress(startPoint + bytesRead); }
2. 下载速度限制
添加下载速度控制功能,避免占用过多带宽:
public class SpeedLimiter { private final long maxBytesPerSecond; private long bytesThisSecond; private long startTime; public SpeedLimiter(long maxBytesPerSecond) { this.maxBytesPerSecond = maxBytesPerSecond; this.bytesThisSecond = 0; this.startTime = System.currentTimeMillis(); } public void acquire(int bytes) throws InterruptedException { bytesThisSecond += bytes; long elapsed = System.currentTimeMillis() - startTime; if (elapsed >= 1000) { resetCounter(); return; } long expectedTime = (long) (bytesThisSecond / (maxBytesPerSecond / 1000.0)); long sleepTime = expectedTime - elapsed; if (sleepTime > 0) { Thread.sleep(sleepTime); } } private void resetCounter() { bytesThisSecond = 0; startTime = System.currentTimeMillis(); } }
3. 代理服务器支持
添加代理服务器支持,方便在企业网络环境下使用:
public class ProxySupport { public static HttpURLConnection createConnection(String url, Proxy proxy) throws IOException { if (proxy != null) { return (HttpURLConnection) new URL(url).openConnection(proxy); } else { return (HttpURLConnection) new URL(url).openConnection(); } } }
六、异常处理与优化
1. 网络异常重试机制
public class RetryMechanism { public static <T> T executeWithRetry(Callable<T> task, int maxRetries) throws Exception { int retries = 0; while (true) { try { return task.call(); } catch (Exception e) { if (retries >= maxRetries) { throw e; } retries++; System.out.println("操作失败,第" + retries + "次重试..."); Thread.sleep(1000 * retries); // 指数退避 } } } }
2. 资源清理确保
// 使用try-with-resources确保资源正确释放 try (InputStream inputStream = conn.getInputStream(); RandomAccessFile file = new RandomAccessFile(savePath, "rw")) { // 下载逻辑 } catch (IOException e) { // 异常处理 }
七、项目构建与运行
1. Maven依赖配置
<dependencies> <!-- 本项目无需额外依赖,使用标准Java库 --> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.2.0</version> <configuration> <archive> <manifest> <mainClass>com.downloader.DownloadTest</mainClass> </manifest> </archive> </configuration> </plugin> </plugins> </build>
2. 编译与打包
# 使用Maven打包 mvn clean package # 运行程序 java -jar downloader.jar
八、总结
本教程详细介绍了如何使用Java实现一个功能完整的多线程文件下载器。通过这个项目,你学习了:
- Java多线程编程和线程池管理
- HTTP范围请求的处理方法
- 文件分块下载与合并技术
- 进度显示和用户交互实现
- 异常处理和资源管理
这个下载器不仅具有实际使用价值,也是学习Java并发和网络编程的绝佳项目。你可以在此基础上继续扩展功能,如添加GUI界面、支持下载队列管理、集成到更大系统中等。