You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Java Commons IO的文件下载器速度受限至1MB/s+求助

网络文件下载器速度受限问题排查

近期我用Java结合Commons IO开发了一款网络文件下载器,但遇到了瓶颈:下载速度始终无法突破1MB/s,而同一文件用浏览器下载能稳定达到3MB/s。以下是相关代码,求解决思路:

Downloader类代码

package com.application.steammachine;

import com.github.junrar.Archive;
import com.github.junrar.Junrar;
import com.github.junrar.exception.RarException;
import com.github.junrar.rarfile.FileHeader;
import com.github.junrar.volume.FileVolumeManager;
import javafx.beans.property.SimpleStringProperty;
import javafx.concurrent.Task;
import org.apache.commons.io.IOUtils;
import org.ini4j.Wini;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URL;

public class Downloader extends Task<Void> {

    private URL url;
    private String fileName;
    private Game game;

    public Downloader(URL url, String fileName, Game game) {
        this.url = url;
        this.fileName = fileName;
        this.game = game;
    }

    public class ProgressListener implements ActionListener {

        private double bytes = 0;
        private double mbDownloaded = 0;
        private double fileSize = 0;
        private double lastMB = 0;
        private long initialTime;
        private double speed = 0;
        private String downloadedText = "";
        private String sizeText;

        public ProgressListener(double fileSize){
            this.fileSize = fileSize;
            initialTime = System.nanoTime();
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            bytes = ((DownloadCountingOutputStream) e.getSource()).getByteCount();
            updateProgress(bytes, fileSize);
            mbDownloaded = round(bytes/1e+6, 2);

            if(fileSize >= 1073741824){ //>= 1GB
                double temp = ((fileSize/1e+6)/1024);
                sizeText = round(temp,2) + " GB";
            }else {
                double temp = (fileSize/1e+6);
                sizeText = round(temp,2) + " MB";
            }

            if(mbDownloaded >= 1024){
                downloadedText = String.valueOf(round(mbDownloaded/1024,2));
            }else{
                downloadedText = String.valueOf(mbDownloaded);
            }

            if((System.nanoTime() - initialTime) >= (Math.pow(10, 9))){
                speed = round((mbDownloaded - lastMB), 3);
                initialTime = System.nanoTime();
                lastMB = mbDownloaded;
            }

            updateMessage(String.valueOf(speed)+"MB/s,"+String.valueOf(downloadedText + "/" + sizeText));
        }
    }

    @Override
    protected Void call() throws Exception {

        URL dl = this.url;
        File fl = null;
        String x = null;
        OutputStream os = null;
        InputStream is = null;

        try {

            updateMessage("Searching files...,---/---");

            fl = new File(Settings.getInstallPath() +"/"+ this.fileName);
            os = new FileOutputStream(fl);
            is = dl.openStream();

            DownloadCountingOutputStream dcount = new DownloadCountingOutputStream(os);

            double fileSize = Double.valueOf(dl.openConnection().getHeaderField("Content-Length"));

            ProgressListener progressListener = new ProgressListener(fileSize);
            dcount.setListener(progressListener);

            IOUtils.copy(is, dcount, 512000 );

            updateMessage("Concluding...,Almost finished");

        } catch (Exception e) {

            System.out.println(e);
            IOUtils.closeQuietly(os);
            IOUtils.closeQuietly(is);
            updateMessage(",");
            this.cancel(true);
            return null;

        } finally {

            IOUtils.closeQuietly(os);
            IOUtils.closeQuietly(is);


            updateMessage(",");
            updateProgress(0, 0);
            this.cancel(true);
            return null;

        }
    }


    protected static double round(double value, int places) {
        if (places < 0) throw new IllegalArgumentException();

        BigDecimal bd = new BigDecimal(Double.toString(value));
        bd = bd.setScale(places, RoundingMode.HALF_UP);
        return bd.doubleValue();
    }

}

DownloadCountingOutputStream类代码

import java.awt.event.ActionListener;
import java.io.IOException;
import java.io.OutputStream;

public class DownloadCountingOutputStream extends CountingOutputStream {

    private ActionListener listener = null;

    public DownloadCountingOutputStream(OutputStream out) {
        super(out);
    }

    public void setListener(ActionListener listener) {
        this.listener = listener;
    }

    @Override
    protected void afterWrite(int n) throws IOException {
        super.afterWrite(n);
        if (listener != null) {
            listener.actionPerformed(new ActionEvent(this, 0, null));
        }
    }

}

优化方案

1. 减少进度回调频率

当前afterWrite每次写入字节都会触发回调,频繁的UI更新和计算会占用大量CPU资源。修改DownloadCountingOutputStream,累计写入一定字节(比如100KB)后再触发回调:

import java.awt.event.ActionListener;
import java.io.IOException;
import java.io.OutputStream;

public class DownloadCountingOutputStream extends CountingOutputStream {

    private ActionListener listener = null;
    private long threshold = 102400; // 100KB
    private long accumulated = 0;

    public DownloadCountingOutputStream(OutputStream out) {
        super(out);
    }

    public void setListener(ActionListener listener) {
        this.listener = listener;
    }

    @Override
    protected void afterWrite(int n) throws IOException {
        super.afterWrite(n);
        accumulated += n;
        if (listener != null && accumulated >= threshold) {
            listener.actionPerformed(new ActionEvent(this, 0, null));
            accumulated = 0;
        }
    }

    // 下载结束时手动触发一次,确保进度更新到100%
    public void flushProgress() {
        if (listener != null) {
            listener.actionPerformed(new ActionEvent(this, 0, null));
        }
    }
}

同时在IOUtils.copy完成后调用dcount.flushProgress()。

2. 复用HTTP连接

避免两次建立连接,先获取连接再读取输入流和文件大小:

@Override
protected Void call() throws Exception {
    URL dl = this.url;
    File fl = null;
    OutputStream os = null;
    InputStream is = null;
    URLConnection conn = null;

    try {
        updateMessage("Searching files...,---/---");

        fl = new File(Settings.getInstallPath() +"/"+ this.fileName);
        // 用BufferedOutputStream优化文件写入
        os = new BufferedOutputStream(new FileOutputStream(fl));
        
        conn = dl.openConnection();
        conn.setConnectTimeout(5000); // 5秒连接超时
        conn.setReadTimeout(10000); // 10秒读取超时
        double fileSize = Double.valueOf(conn.getHeaderField("Content-Length"));
        
        is = conn.getInputStream();

        DownloadCountingOutputStream dcount = new DownloadCountingOutputStream(os);
        ProgressListener progressListener = new ProgressListener(fileSize);
        dcount.setListener(progressListener);

        // 增大缓冲区到1MB
        IOUtils.copy(is, dcount, 1048576);
        dcount.flushProgress(); // 刷新最终进度

        updateMessage("Concluding...,Almost finished");

    } catch (Exception e) {
        System.out.println(e);
        IOUtils.closeQuietly(os);
        IOUtils.closeQuietly(is);
        updateMessage(",");
        this.cancel(true);
        return null;
    } finally {
        IOUtils.closeQuietly(os);
        IOUtils.closeQuietly(is);
        updateMessage(",");
        updateProgress(0, 0);
        this.cancel(true);
        return null;
    }
}

3. 其他优化点

  • 检查是否有防火墙或代理限制Java程序的网络速度;
  • 尝试启用HTTP/2(如果服务器支持),可以使用OkHttp等库替代原生URLConnection,提升网络传输效率。

内容的提问来源于stack exchange,提问作者Mattia Mc Roll

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 09:35:28