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

HttpURLConnection与OkHttp无法追踪上传进度及缓冲问题求助

解决HttpURLConnection与OkHttp上传进度虚高的问题

我太懂这种 frustration 了——明明进度条跑的飞快,但路由器带宽图表显示实际传输速率慢得可怜,这都是因为默认的HTTP客户端缓冲机制在搞鬼:你统计的是写入本地缓冲区的速度,而不是实际发往服务器的速率。下面分别给你两个客户端的解决方案:

针对HttpURLConnection的修复

要绕过缓冲,必须手动启用流式传输,并跟踪真实的网络写入字节数:

  • 开启流式模式:调用 setFixedLengthStreamingMode(已知文件大小)或 setChunkedStreamingMode(0)(未知大小),这会强制HttpURLConnection不缓冲数据,而是边写边发:
    HttpURLConnection conn = (HttpURLConnection) new URL(uploadUrl).openConnection();
    conn.setDoOutput(true);
    conn.setRequestMethod("POST");
    // 已知文件大小用固定长度模式
    conn.setFixedLengthStreamingMode(file.length());
    // 未知大小用分块模式
    // conn.setChunkedStreamingMode(0);
    
  • 自定义进度跟踪输出流:包装原始的 OutputStream,每次写入时统计真实发送的字节数:
    class ProgressOutputStream extends FilterOutputStream {
        private final long totalSize;
        private long sentBytes = 0;
        private final ProgressCallback callback;
    
        public ProgressOutputStream(OutputStream out, long totalSize, ProgressCallback callback) {
            super(out);
            this.totalSize = totalSize;
            this.callback = callback;
        }
    
        @Override
        public void write(int b) throws IOException {
            super.write(b);
            sentBytes++;
            callback.onProgress((int) (sentBytes * 100 / totalSize));
        }
    
        @Override
        public void write(byte[] b, int off, int len) throws IOException {
            super.write(b, off, len);
            sentBytes += len;
            callback.onProgress((int) (sentBytes * 100 / totalSize));
        }
    }
    
    // 使用方式
    try (OutputStream os = new ProgressOutputStream(conn.getOutputStream(), file.length(), progress -> {
        // 更新UI进度,比如progressBar.setProgress(progress);
    })) {
        // 用FileInputStream读取文件,写入os
        byte[] buffer = new byte[4096];
        int read;
        try (FileInputStream fis = new FileInputStream(file)) {
            while ((read = fis.read(buffer)) != -1) {
                os.write(buffer, 0, read);
            }
        }
    }
    
    这样统计的进度就是实际发往网络的字节数,和路由器的带宽图表就能对应上了。

针对OkHttp的修复

OkHttp默认也会缓冲上传数据,要跟踪真实进度,需要自定义 RequestBody 来拦截写入操作:

  • 编写带进度的RequestBody:
    class ProgressRequestBody extends RequestBody {
        private final RequestBody originalBody;
        private final ProgressCallback callback;
        private long totalSize;
        private long sentBytes = 0;
    
        public ProgressRequestBody(RequestBody originalBody, ProgressCallback callback) {
            this.originalBody = originalBody;
            this.callback = callback;
            try {
                this.totalSize = originalBody.contentLength();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public MediaType contentType() {
            return originalBody.contentType();
        }
    
        @Override
        public long contentLength() throws IOException {
            return totalSize;
        }
    
        @Override
        public void writeTo(BufferedSink sink) throws IOException {
            // 用ForwardingSink拦截写入操作,统计真实发送的字节
            Sink progressSink = new ForwardingSink(sink) {
                @Override
                public void write(Buffer source, long byteCount) throws IOException {
                    super.write(source, byteCount);
                    sentBytes += byteCount;
                    callback.onProgress((int) (sentBytes * 100 / totalSize));
                }
            };
            originalBody.writeTo(Okio.buffer(progressSink));
        }
    }
    
    // 使用方式
    RequestBody fileBody = RequestBody.create(file, MediaType.parse("application/octet-stream"));
    RequestBody progressBody = new ProgressRequestBody(fileBody, progress -> {
        // 更新UI进度
    });
    
    OkHttpClient client = new OkHttpClient();
    Request request = new Request.Builder()
            .url(uploadUrl)
            .post(progressBody)
            .build();
    
    client.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
            // 处理失败
        }
    
        @Override
        public void onResponse(Call call, Response response) throws IOException {
            // 处理成功响应
        }
    });
    
    这里的关键是用 ForwardingSink 包装原始的Sink,确保每次写入的字节数都是实际发往服务器的,进度统计就不会虚高了。

额外提醒

  • 不要用缓冲区的写入速度做统计:不管哪种客户端,默认缓冲都会让进度条“提前跑完”,必须拦截真实的网络写入操作。
  • 大文件上传必用流式模式:避免把整个文件加载到内存,防止OOM。
  • 测试验证:你用路由器限制带宽的方法很靠谱,调整完代码后再测一次,看看进度和路由器的带宽图表是否匹配。

内容的提问来源于stack exchange,提问作者Daniel F

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.27 03:43:25