OKHTTP 3 Отслеживание процесса многокомпонентной загрузки

Как я могу отслеживать ход загрузки в OkHttp 3? Я могу найти ответы для v2, но не для v3, например это

Пример составного запроса из рецептов OkHttp

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

person Sourabh    schedule 20.02.2016    source источник
comment
В OkHttp3 образцах есть рецепт. Он показывает, как показать прогресс загрузки. Если вы просмотрите его, вы сможете создать монитор прогресса загрузки. Найдите его здесь https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java   -  person Elvis Chweya    schedule 20.02.2016


Ответы (3)


Вы можете украсить тело запроса OkHttp, чтобы подсчитать количество записанных байтов при его записи; чтобы выполнить эту задачу, оберните свой MultiPart RequestBody в этот RequestBody экземпляром Listener и вуаля!

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);
    }
}

Проверьте эта ссылка для получения дополнительной информации.

person Sourabh    schedule 28.03.2016
comment
@Saurabh метод write() иногда вызывается несколько раз в медленной сети. Таким образом, фактический % превышает 100%. Вы столкнулись с этой проблемой? - person geekoraul; 18.04.2017
comment
Не совсем, что ты имеешь ввиду под несколько раз? Сколько раз? 3-4 или 40-60? - person Sourabh; 18.04.2017
comment
Ну, это не фиксированное количество раз, как я уже говорил, это зависит от сети. Итак, скажем, общий процент иногда достигает 115%, а иногда достигает 400%. Вопрос в том, сталкивались ли вы с таким поведением? - person geekoraul; 18.04.2017
comment
Не могли бы вы привести пример, как обернуть мой MultiPart RequestBody в этот RequestBody? Я не знаю, правильно ли я делаю, но каким-то образом прогресс подскочил до 100% за несколько миллисекунд, но загрузка фактического файла заняла около 10 секунд. - person teck wei; 21.04.2017
comment
Я обнаружил, что удаление HttpLoggingInterceptor из моего клиента остановило это. - person IanField90; 15.08.2017
comment
@teckwei У меня именно та проблема, которую вы указали. Вы нашли решение? - person muthuraj; 05.09.2017
comment
@muthuraj В конечном итоге я использую firebase в качестве своего бэкэнда. Но я нашел рабочий метод, который вам нужен, включая Header RequestBody и ваш new File() внутри метода addPart, иначе ваш сторонний сервер не получит размер файла. - person teck wei; 05.09.2017
comment
@ IanField90 Какой уровень ведения журнала у вас был с HttpLoggingInterceptor? Кроме того, вы пробовали с другими уровнями? - person Louis CAD; 26.12.2017
comment
@LouisCAD У меня был установлен уровень журнала Body. Я не пробовал другие уровни, нет. - person IanField90; 03.01.2018
comment
@IanField90 IanField90 У меня была похожая проблема (но с использованием двоичного тела, а не составного) в ходе выполнения, и я больше не видел ее после установки уровня ведения журнала для заголовков вместо тела - person Louis CAD; 03.01.2018
comment
Этот пример кода неверен, и его использование приведет к сбою вашей программы. Основная проблема заключается в том, что writeTo() может вызываться несколько раз, но этот класс этого не поддерживает. https://github.com/square/okhttp/issues/3842#issuecomment-364898908 - person Jesse Wilson; 12.02.2018
comment
@JesseWilson Как тогда можно отслеживать ход загрузки? У вас есть образец? - person Vadim Kotov; 13.11.2019
comment
Этот код, по-видимому, не измеряет продвижение по сети. В моем случае все данные буферизуются заранее, а затем большая часть фактического времени загрузки тратится на 100%. Этот код делает довольно глубокие предположения о том, как и когда данные считываются из тела. Таким образом, это не является хорошим стабильным решением. Пробовал на всех уровнях ведения журнала, но безрезультатно. - person ptoinson; 28.04.2020
comment
Не стесняйтесь добавлять свой собственный ответ :) Я считаю, что это действительно старый ответ и поэтому устарел, и okhttp добавил для этого лучший механизм. - person Sourabh; 29.04.2020
comment
привет, можете ли вы сказать, если вы обернули тело запроса через перехваченное или bofor передачу тела в запрос? - person MRamzan; 24.04.2021
comment
Привет, я сделал через перехватчик, это сработало, спасибо - person MRamzan; 24.04.2021

Я не смог заставить ни один из ответов работать на меня. Проблема заключалась в том, что прогресс достигал 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 байт отправляемых данных. Ну, мои изображения были как раз под этим, и прогресс показывал ход заполнения этого буфера отправки сокета. Чтобы смягчить это, создайте SocketFactory для OkHttpClient.

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, такому как 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()))

Это весь рабочий код в моем проекте. Я вытащил его из иллюстративных целей. Если у вас не работает из коробки, прошу прощения.

person ptoinson    schedule 29.04.2020

Согласно ответу Сураба, я хочу сказать, что это поле CountingSink

private long bytesWritten = 0;

должен быть перемещен в класс ProgressRequestBody

person Michael Gaev    schedule 26.08.2016
comment
Почему вы столкнулись с какой-либо ошибкой, когда она была внутри класса CountingSink? - person Sourabh; 26.08.2016
comment
Я протестировал этот код на Android. Процент, входящий от Listener.onProgress(), был в неправильном порядке: 0, 1, 2, 0, 1, 2, а затем я получил это исключение: java.net.SocketException: sendto failed: ECONNRESET (сброс соединения узлом), вызванный android.system.ErrnoException: ошибка отправки: ECONNRESET ECONNRESET (соединение сброшено одноранговым узлом) - person Michael Gaev; 26.08.2016
comment
Пожалуйста, воздержитесь от использования ответов, чтобы комментировать другие ответы. Это должен быть комментарий к ответу Сурабха. - person ptoinson; 28.04.2020