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
相关产品推荐
相关产品推荐

