Java多线程分块下载文件

目录

技术原理

完整代码实现

主程序类(Test.java)

下载线程类(DownThread.java)

关键代码解析

[1. 分块算法解析](#1. 分块算法解析)

[2. HTTP Range请求头](#2. HTTP Range请求头)

[3. RandomAccessFile的使用](#3. RandomAccessFile的使用)

运行结果


技术原理

多线程下载的核心思想是:将一个大文件分成若干块,每个线程负责下载其中一块,最后将所有块合并成完整文件。其关键技术点包括:

  1. 文件分割算法:将文件总大小平均分配给每个线程,计算每个线程负责下载的起始和结束位置。

  2. HTTP Range 请求 :通过 HTTP 协议的Range头字段,告知服务器只返回文件的特定片段(从 start 到 end 的字节范围)。

  3. 随机文件写入 :使用RandomAccessFile类,支持线程将下载的片段写入文件的指定位置,避免线程间的写入冲突。

  4. 多线程协同:多个线程并行工作,各自完成分配的下载任务。

完整代码实现

主程序类(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()方法定位到文件的任意位置。
  • 多个线程可以同时写入同一个文件的不同位置,确保每个线程写入的数据不会覆盖其他线程的内容。

运行结果

相关推荐
不秃的开发媛2 小时前
Java连接池详解:从Oracle到TiDB的随缘之旅
java·oracle·tidb
Pluchon2 小时前
硅基计划3.0 Map类&Set类
java·开发语言·数据结构·算法·哈希算法·散列表
Angelyb3 小时前
微服务保护和分布式事务
java·微服务·架构
42fourtytoo3 小时前
天津大学智算2026预推免机试第二批题目及代码c++
开发语言·c++·面试
七夜zippoe3 小时前
缓存三大劫攻防战:穿透、击穿、雪崩的Java实战防御体系(一)
java·开发语言·缓存
almighty273 小时前
C#WPF控制USB摄像头参数:曝光、白平衡等高级设置完全指南
开发语言·c#·wpf·usb相机·参数设置
起个昵称吧3 小时前
立即数、栈、汇编与C函数的调用
c语言·开发语言·汇编
帧栈3 小时前
开发避坑指南(46):Java Stream 对List的BigDecimal字段进行求和
java
子豪-中国机器人3 小时前
枚举算法和排序算法能力测试
开发语言·c++·算法