【问题标题】:OKHTTP 3 Tracking Multipart upload progressOKHTTP 3 跟踪分段上传进度
【发布时间】:2016-06-02 09:50:38
【问题描述】:

如何在 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());
}

【问题讨论】:

标签: android okhttp3


【解决方案1】:

你可以装饰你的 OkHttp 请求体来统计写入时写入的字节数;为了完成此任务,请将您的 MultiPart RequestBody 包装在此 RequestBody 中,并使用 Listener 和 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);
    }
}

查看this link了解更多信息。

【讨论】:

  • @Saurabh 有时会在慢速网络上多次调用 write() 方法。所以实际百分比超过 100%。你遇到过这个问题吗?
  • 不是真的,“几次”是什么意思?多少次? 3-4 还是 40-60?
  • 您能否举例说明如何将我的 MultiPart RequestBody 包装在这个 RequestBody 中?我不知道我做的是否正确,但不知何故,进度在几毫秒内跳转到 100%,但实际文件上传大约需要 10 秒。
  • 我发现从我的客户中删除 HttpLoggingInterceptor 阻止了这种情况的发生。
  • 此代码示例不正确,使用它会导致程序崩溃。潜在的问题是 writeTo() 可能会被多次调用,但这个类不支持。 github.com/square/okhttp/issues/3842#issuecomment-364898908
【解决方案2】:

根据 Sourabh 的回答,我想告诉 CountingSink 的那个字段

private long bytesWritten = 0;

必须移入 ProgressRequestBody 类

【讨论】:

  • 为什么,当它在 CountingSink 类中时,你遇到过什么错误吗?
  • 我已经在 Android 上测试过这段代码。从 Listener.onProgress() 传入的百分比顺序错误:0、1、2、0、1、2 然后我得到了这个异常:java.net.SocketException: sendto failed: ECONNRESET (Connection reset by peer) Caused by android.system.ErrnoException: sendto failed: ECONNRESET ECONNRESET (Connection reset by peer)
  • 请不要使用答案来评论其他答案。这应该是对 Sourabh 的回答的评论。
【解决方案3】:

我无法得到任何适合我的答案。问题是在图像上传之前进度将运行到 100%,这暗示在通过网络发送数据之前,一些缓冲区已被填满。经过一番研究,我发现确实是这样,那个缓冲区就是 Socket 发送缓冲区。为 OkHttpClient 提供一个 SocketFactory 终于奏效了。我的 Kotlin 代码如下...

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

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 文件的风格,因此我的应用程序可以在没有网络的情况下运行,因此我可以在没有这种负担的情况下进行测试。要使此代码正常工作,Dispatcher 需要读取正文数据。我创建了这个 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 实例对我来说更容易。
猜你喜欢
  • 2014-11-15
  • 2018-10-16
  • 1970-01-01
  • 2012-01-01
  • 2014-06-03
  • 2013-05-19
  • 2016-04-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多