OKHTTP 3 追踪多部分上传进度

12

如何追踪OkHttp 3上传的进度?我可以找到v2的答案,但找不到v3的答案,类似于this

来自OkHttp食谱的示例Multipart请求

private static final String IMGUR_CLIENT_ID = "...";
private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");

private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
    // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
    RequestBody requestBody = new MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("title", "Square Logo")
            .addFormDataPart("image", "logo-square.png",
                    RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
            .build();

    Request request = new Request.Builder()
            .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
            .url("https://api.imgur.com/3/image")
            .post(requestBody)
            .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
}

OkHttp3示例中有一个配方,它展示了如何显示下载进度。如果您仔细查看,可能会创建一个上传进度监视器。在此处找到:https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java - Elvis Chweya
3个回答

13
您可以对OkHttp请求体进行装饰,以在编写它时计算写入的字节数;为了完成此任务,请在Listener实例的RequestBody中包装您的MultiPart RequestBody,然后Voila!

您可以装饰OkHttp请求体,以在编写时计算写入的字节数。为了完成此任务,请使用Listener实例包装您的MultiPart RequestBody作为RequestBody,然后Voila!

public class ProgressRequestBody extends RequestBody {

    protected RequestBody mDelegate;
    protected Listener mListener;
    protected CountingSink mCountingSink;

    public ProgressRequestBody(RequestBody delegate, Listener listener) {
        mDelegate = delegate;
        mListener = listener;
    }

    @Override
    public MediaType contentType() {
        return mDelegate.contentType();
    }

    @Override
    public long contentLength() {
        try {
            return mDelegate.contentLength();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return -1;
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        mCountingSink = new CountingSink(sink);
        BufferedSink bufferedSink = Okio.buffer(mCountingSink);
        mDelegate.writeTo(bufferedSink);
        bufferedSink.flush();
    }

    protected final class CountingSink extends ForwardingSink {
        private long bytesWritten = 0;
        public CountingSink(Sink delegate) {
            super(delegate);
        }
        @Override
        public void write(Buffer source, long byteCount) throws IOException {
            super.write(source, byteCount);
            bytesWritten += byteCount;
            mListener.onProgress((int) (100F * bytesWritten / contentLength()));
        }
    }

    public interface Listener {
        void onProgress(int progress);
    }
}

查看此链接以获取更多信息。


2
@Saurabh,write()方法有时会在慢速网络上被多次调用。因此实际的百分比超过了100%。你遇到过这个问题吗? - geekoraul
3
你能否提供一个例子来演示如何将我的 MultiPart RequestBody 包装在这个 RequestBody 中?我不确定自己是否做得正确,但进度条会在几毫秒内跳到 100%,而实际文件上传需要大约10秒钟。 - teck wei
1
我发现从我的客户端中删除 HttpLoggingInterceptor 可以阻止这种情况发生。 - IanField90
1
这个代码示例是不正确的,使用它会导致程序崩溃。根本问题在于writeTo()可能会被调用多次,但是这个类不支持这样做。https://github.com/square/okhttp/issues/3842#issuecomment-364898908 - Jesse Wilson
1
这段代码似乎没有在传输过程中测量进度。在我的情况下,所有数据都会被预先缓冲,然后大部分实际上传时间都花费在100%上。这段代码对于从Body读取数据的方式和时间做出了一些深层次的假设。因此,这不是一个好的稳定解决方案。已尝试在所有日志记录级别下使用,但没有成功。 - ptoinson
显示剩余11条评论

6

我无法使任何答案对我生效。问题在于,进度条在上传图像之前就已经到达了100%,这表明某些缓冲区在数据发送到服务器之前被填满了。经过一些研究,我发现确实存在这种情况,而且该缓冲区是Socket发送缓冲区。最终提供一个SocketFactory给OkHttpClient解决了这个问题。我的Kotlin代码如下...

首先,像其他人一样,我有一个CountingRequestBody用于包装MultipartBody。

class CountingRequestBody(var delegate: RequestBody, private var listener: (max: Long, value: Long) -> Unit): RequestBody() {

    override fun contentType(): MediaType? {
        return delegate.contentType()
    }

    override fun contentLength(): Long {
        try {
            return delegate.contentLength()
        } catch (e: IOException) {
            e.printStackTrace()
        }
        return -1
    }

    override fun writeTo(sink: BufferedSink) {
        val countingSink = CountingSink(sink)
        val bufferedSink = Okio.buffer(countingSink)
        delegate.writeTo(bufferedSink)
        bufferedSink.flush()
    }

    inner class CountingSink(delegate: Sink): ForwardingSink(delegate) {
        private var bytesWritten: Long = 0

        override fun write(source: Buffer, byteCount: Long) {
            super.write(source, byteCount)
            bytesWritten += byteCount
            listener(contentLength(), bytesWritten)
        }
    }
}

我正在使用Retrofit2。一般的用法如下:

val builder = MultipartBody.Builder()
// Add stuff to the MultipartBody via the Builder

val body = CountingRequestBody(builder.build()) { max, value ->
      // Progress your progress, or send it somewhere else.
}

到这个阶段,我已经取得了进展,但是每当数据正在上传时,我会看到100%,然后是长时间的等待。关键在于套接字在我的设置中默认配置为缓冲3145728字节的发送数据。嗯,我的图片大小稍小于此并且进度显示了填充套接字发送缓冲区的进度。为了减轻这种情况,需要为OkHttpClient创建一个SocketFactory。

class ProgressFriendlySocketFactory(private val sendBufferSize: Int = DEFAULT_BUFFER_SIZE) : SocketFactory() {

    override fun createSocket(): Socket {
        return setSendBufferSize(Socket())
    }

    override fun createSocket(host: String, port: Int): Socket {
        return setSendBufferSize(Socket(host, port))
    }

    override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket {
        return setSendBufferSize(Socket(host, port, localHost, localPort))
    }

    override fun createSocket(host: InetAddress, port: Int): Socket {
        return setSendBufferSize(Socket(host, port))
    }

    override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket {
        return setSendBufferSize(Socket(address, port, localAddress, localPort))
    }

    private fun setSendBufferSize(socket: Socket): Socket {
        socket.sendBufferSize = sendBufferSize
        return socket
    }

    companion object {
        const val DEFAULT_BUFFER_SIZE = 2048
    }
}

在配置过程中,设置它。

val clientBuilder = OkHttpClient.Builder()
    .socketFactory(ProgressFriendlySocketFactory())

正如其他人提到的那样,记录请求主体可能会影响性能并导致数据被多次读取。要么不记录主体,或者像我一样,在CountingRequestBody上关闭它。为此,我编写了自己的HttpLoggingInterceptor,并解决了这个问题和其他问题(例如记录MultipartBody)。但这超出了本问题的范围。

if(requestBody is CountingRequestBody) {
  // don't log the body in production
}

另一个问题与MockWebServer有关。 我有一个使用MockWebServer和json文件的flavor,以便我的应用程序可以在没有网络的情况下运行,这样我就可以进行测试而不会造成负担。 为了使此代码工作,Dispatcher需要读取body数据。 我创建了这个Dispatcher来完成这个任务。 然后将调度转发到另一个Dispatcher,例如默认的QueueDispatcher。

class BodyReadingDispatcher(val child: Dispatcher): Dispatcher() {

    override fun dispatch(request: RecordedRequest?): MockResponse {
        val body = request?.body
        if(body != null) {
            val sink = ByteArray(1024)
            while(body.read(sink) >= 0) {
                Thread.sleep(50) // change this time to work for you
            }
        }
        val response = child.dispatch(request)
        return response
    }
}

您可以在MockWebServer中使用以下内容:
var server = MockWebServer()
server.setDispatcher(BodyReadingDispatcher(QueueDispatcher()))

这是我项目中的所有工作代码,我提取出来只是为了举例。如果它不适用于你的情况,我很抱歉。


好的且深入的回答!解决了我的问题。我有多个拦截器,对我来说更容易的方法是为文件上传注入一个单独的OkHttp实例。 - oblakr24

0
根据Sourabh的回答,我想说的是CountingSink领域。
private long bytesWritten = 0;

必须移动到ProgressRequestBody类中


为什么,在CountingSink类内部时,你遇到了任何错误吗? - Sourabh
我已在Android上测试了此代码。从Listener.onProgress()传入的百分比顺序错误:0、1、2、0、1、2,然后我遇到了以下异常:java.net.SocketException: sendto failed: ECONNRESET (Connection reset by peer),由android.system.ErrnoException: sendto failed: ECONNRESET引起。ECONNRESET (Connection reset by peer)。 - Michael Gaev
请勿使用答案来评论其他答案。这应该是对Sourabh答案的评论。 - ptoinson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接