0

从 okHttp 源代码来看,当 call.execute() 被调用时,主体从服务器传输到客户端。这是没有意义的,因为无法为 okio 设置截止日期,这意味着我不能给整个请求超时,而只有 readTimeout 和 connectTimeout 才有效,直到第一个字节准备好读取。

我在这里错过了什么吗?

4

2 回答 2

0

没有办法为整个请求指定最后期限。您应该对此提出功能请求!OkHttp 对 Okio 的使用是其差异化功能之一,通过 OkHttp 的 API 公开更多 Okio 功能是为 OkHttp 的用户提供更多权力的好方法。

于 2016-01-03T21:25:39.917 回答
0

这是 okhttp ( https://github.com/square/okhttp/issues/2840 ) 的下一个版本的计划,但现在我们通过在我们的 Call 中子类化成功实现了请求和响应正文读取的截止日期生产应用:

package com.pushd.util;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.internal.http2.StreamResetException;
import okio.Buffer;
import okio.BufferedSource;
import okio.ForwardingSource;
import okio.Okio;

/**
 * An okhttp3.Call with a deadline timeout from the start of isExecuted until ResponseBody.source() is closed or unused.
 */
public class DeadlineCall implements Call {
    private final static Logger LOGGER = Logger.getLogger(DeadlineCall.class.getName());

    private static AtomicInteger sFutures = new AtomicInteger();
    private static final ScheduledExecutorService sHTTPCancelExecutorService = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r, "DeadlineCallCancel");
            t.setDaemon(true);
            t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    });

    private final Call mUnderlying;
    private final int mDeadlineTimeout;
    private volatile ScheduledFuture mDeadline;
    private volatile boolean mDeadlineHit;
    private volatile boolean mCancelled;
    private volatile BufferedSource mBodySource;

    DeadlineCall(Call underlying, int deadlineTimeout) {
        mUnderlying = underlying;
        mDeadlineTimeout = deadlineTimeout;
    }

    /**
     * Factory wrapper for OkHttpClient.newCall(request) to create a new DeadlineCall scheduled to cancel its underlying Call after the deadline. 
     * @param client
     * @param request
     * @param deadlineTimeout in ms
     * @return Call
     */
    public static DeadlineCall newDeadlineCall(@NonNull OkHttpClient client, @NonNull Request request, int deadlineTimeout) {
        final Call underlying = client.newCall(request);
        return new DeadlineCall(underlying, deadlineTimeout);
    }

    /**
     * Shuts down thread that cancels calls when their deadline is hit.
     */
    public static void shutdownNow() {
        sHTTPCancelExecutorService.shutdownNow();
    }

    @Override
    public Request request() {
        return mUnderlying.request();
    }

    /**
     * Response MUST be closed to clean up deadline even if body is not read, e.g. on !isSuccessful
     * @return
     * @throws IOException
     */
    @Override
    public Response execute() throws IOException {
        startDeadline();

        try {
            return wrapResponse(mUnderlying.execute());
        } catch (IOException e) {
            cancelDeadline();
            throw wrapIfDeadline(e);
        }
    }

    /**
     * Deadline is removed when onResponse returns unless response.body().source() or a method using
     * it is called synchronously from onResponse to indicate caller's committment to close it themselves.
     * This includes peekBody so prefer DeadlineResponseBody.peek unless you explicitly close after peekBody.
     * @param responseCallback
     */
    @Override
    public void enqueue(final Callback responseCallback) {
        startDeadline();

        mUnderlying.enqueue(new Callback() {
            @Override
            public void onFailure(Call underlying, IOException e) {
                cancelDeadline(); // there is no body to read so no need for deadline anymore
                responseCallback.onFailure(DeadlineCall.this, wrapIfDeadline(e));
            }

            @Override
            public void onResponse(Call underlying, Response response) throws IOException {
                try {
                    responseCallback.onResponse(DeadlineCall.this, wrapResponse(response));
                    if (mBodySource == null) {
                        cancelDeadline(); // remove deadline if body was never opened
                    }
                } catch (IOException e) {
                    cancelDeadline();
                    throw wrapIfDeadline(e);
                }
            }
        });
    }

    private IOException wrapIfDeadline(IOException e) {
        if (mDeadlineHit && isCancellationException(e)) {
            return new DeadlineException(e);
        }

        return e;
    }

    public class DeadlineException extends IOException {
        public DeadlineException(Throwable cause) {
            super(cause);
        }
    }

    /**
     * Wraps response to cancelDeadline when response closed and throw correct DeadlineException when deadline happens during response reading.
     * @param response
     * @return
     */
    private Response wrapResponse(final Response response) {
        return response.newBuilder().body(new DeadlineResponseBody(response)).build();
    }

    public class DeadlineResponseBody extends ResponseBody {
        private final Response mResponse;

        DeadlineResponseBody(final Response response) {
            mResponse = response;
        }

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

        @Override
        public long contentLength() {
            return mResponse.body().contentLength();
        }

        /**
         * @return the body source indicating it will be closed later by the caller to cancel the deadline
         */
        @Override
        public BufferedSource source() {
            if (mBodySource == null) {
                mBodySource = Okio.buffer(new ForwardingSource(mResponse.body().source()) {
                    @Override
                    public long read(Buffer sink, long byteCount) throws IOException {
                        try {
                            return super.read(sink, byteCount);
                        } catch (IOException e) {
                            throw wrapIfDeadline(e);
                        }
                    }

                    @Override
                    public void close() throws IOException {
                        cancelDeadline();
                        super.close();
                    }
                });
            }

            return mBodySource;
        }

        /**
         * @return the body source without indicating it will be closed later by caller, e.g. to peekBody on unsucessful requests
         */
        public BufferedSource peekSource() {
            return mResponse.body().source();
        }

        /**
         * Copy of https://square.github.io/okhttp/3.x/okhttp/okhttp3/Response.html#peekBody-long- that uses peekSource() since Response class is final
         * @param byteCount
         * @return
         * @throws IOException
         */
        public ResponseBody peek(long byteCount) throws IOException {
            BufferedSource source = peekSource();
            source.request(byteCount);
            Buffer copy = source.buffer().clone();

            // There may be more than byteCount bytes in source.buffer(). If there is, return a prefix.
            Buffer result;
            if (copy.size() > byteCount) {
                result = new Buffer();
                result.write(copy, byteCount);
                copy.clear();
            } else {
                result = copy;
            }

            return ResponseBody.create(mResponse.body().contentType(), result.size(), result);
        }
    }

    private void startDeadline() {
        mDeadline = sHTTPCancelExecutorService.schedule(new Runnable() {
            @Override
            public void run() {
                mDeadlineHit = true;
                mUnderlying.cancel(); // calls onFailure or causes body read to throw
                LOGGER.fine("Deadline hit for " + request()); // should trigger a subsequent wrapIfDeadline but if we see this log line without that it means     the caller orphaned us without closing
            }
        }, mDeadlineTimeout, TimeUnit.MILLISECONDS);

        LOGGER.fine("started deadline for " + request());

        if (sFutures.incrementAndGet() == 1000) {
            LOGGER.warning("1000 pending DeadlineCalls, may be leaking due to not calling close()");
        }
    }

    private void cancelDeadline() {
        if (mDeadline != null) {
            mDeadline.cancel(false);
            mDeadline = null;
            sFutures.decrementAndGet();
            LOGGER.fine("canceled deadline for " + request());
        } else {
            LOGGER.info("deadline already canceled for " + request());
        }
    }

    @Override
    public void cancel() {
        mCancelled = true;

        // should trigger onFailure or raise from execute or responseCallback.onResponse which will cancelDeadline
        mUnderlying.cancel();
    }

    @Override
    public boolean isExecuted() {
        return mUnderlying.isExecuted();
    }

    @Override
    public boolean isCanceled() {
        return mCancelled;
    }

    @Override
    public Call clone() {
        return new DeadlineCall(mUnderlying.clone(), mDeadlineTimeout);
    }

    private static boolean isCancellationException(IOException e) {
        // okhttp cancel from HTTP/2 calls
        if (e instanceof StreamResetException) {
            switch (((StreamResetException) e).errorCode) {
                case CANCEL:
                    return true;
            }
        }

        // https://android.googlesource.com/platform/external/okhttp/+/master/okhttp/src/main/java/com/squareup/okhttp/Call.java#281
        if (e instanceof IOException &&
                e.getMessage() != null && e.getMessage().equals("Canceled")) {
            return true;
        }

        return false;
    }
}

请注意,我们还有一个单独的拦截器来超时 DNS,因为即使我们的截止日期也不包括:

/**
 * Based on http://stackoverflow.com/questions/693997/how-to-set-httpresponse-timeout-for-android-in-java/31643186#31643186
 * as per https://github.com/square/okhttp/issues/95
 */
private static class DNSTimeoutInterceptor implements Interceptor {
    long mTimeoutMillis;

    public DNSTimeoutInterceptor(long timeoutMillis) {
        mTimeoutMillis = timeoutMillis;
    }

    @Override
    public Response intercept(final Chain chain) throws IOException {
        Request request = chain.request();
        Log.SplitTimer timer = (request.tag() instanceof RequestTag ? ((RequestTag) request.tag()).getTimer() : null);

        // underlying call should timeout after 2 tries of 5s:  https://android.googlesource.com/platform/bionic/+/android-5.1.1_r38/libc/dns/include/resolv_private.h#137
        // could use our own Dns implementation that falls back to public DNS servers:  https://garage.easytaxi.com/tag/dns-android-okhttp/
        if (!DNSResolver.isDNSReachable(request.url().host(), mTimeoutMillis)) {
            throw new UnknownHostException("DNS timeout");
        }
        return chain.proceed(request);
    }

    private static class DNSResolver implements Runnable {
        private String mDomain;
        private InetAddress mAddress;

        public static boolean isDNSReachable(String domain, long timeoutMillis) {
            try {
                DNSResolver dnsRes = new DNSResolver(domain);

                Thread t = new Thread(dnsRes, "DNSResolver");
                t.start();
                t.join(timeoutMillis);
                return dnsRes.get() != null;
            }  catch(Exception e)  {
                return false;
            }
        }

        public DNSResolver(String domain) {
            this.mDomain = domain;
        }

        public void run() {
            try {
                InetAddress addr = InetAddress.getByName(mDomain);
                set(addr);
            } catch (UnknownHostException e) {
            }
        }

        public synchronized void set(InetAddress inetAddr) {
            this.mAddress = inetAddr;
        }
        public synchronized InetAddress get() {
            return mAddress;
        }
    }
}
于 2017-02-28T18:56:05.063 回答