OkHttp使用踩坑记录总结(二):OkHttp同步异步请求和连接池线程池

说明

在项目中对第三方服务的调用,使用了OkHttp进行http请求,这当中踩了许多坑。本篇博文将对OkHttp使用过程遇到的问题进行总结记录。

正文

同步请求SyncRequest 异步请求AsyncRequest

通过简单示例了解OkHttp如何进行http请求:

SyncRequest:

private static void syncRequest(String url) throws IOException {
    Request request = new Request.Builder().url(url).build();
    Response response = okHttpClient.newCall(request).execute();
    System.out.println(response.body().string());
}

AsyncRequest

private static void asyncRequest(String url) {
    Request request = new Request.Builder().url(url).build();
    okHttpClient.newCall(request).enqueue(new Callback() {
        @Override
        public void onFailure(Call call, IOException e) {
            e.printStackTrace();
        }

        @Override
        public void onResponse(Call call, Response response) throws IOException {
            if (!response.isSuccessful()) {
                throw new RuntimeException("Unexpected code " + response);
            } else {
                System.out.println(response.body().string());
            }
        }
    });
}

在上示代码中,okHttpClient调用newCall方法生成Call对象,再调用不同的方法进行不同的请求。为什么不直接请求,而是通过Call对象,该对象的作用是什么?通过官方文档 Calls 进行了解。

在文档中说明了OkHttp将对请求和响应做以下操作:

  1. 为了请求的准确性和效率,OkHttp在进行实际请求前会重写发送者的请求(Rewriting Requests)。
  2. 当使用透明传时或者使用缓存时,OkHttp将重写响应(Rewriting Responses)。
  3. 当请求地址改变,响应302时,OkHttp将进行重定向的后续请求(Follow-up Requests)。
  4. 当连接失败时,OkHttp将重新尝试建立连接(Retrying Requests)。

发送者的简单请求可能会产生以上的多个中间请求和响应,OkHttp使用Call来进行任务,实现中间的多个请求响应。Call的执行有两种方式:

  • 同步 请求线程会阻塞直到响应可读
  • 异步 请求进队列,通过另一个线程获取回调读取结果

Call的执行可以在任何线程中取消,这会导致未完成的请求失败并抛出IOException。

请求转发Dispatch
对于同步请求,我们需要控制同时请求的线程数量,太多同时请求的连接会浪费资源,太少请求则会增加延迟。

对于异步请求,OkHttp使用Dispathcer类依据某种策略实现对同时请求数的控制。你可以通过参数maxRequestPreHost设置每个服务端最多的同时请求数,默认值为5,还可以通过参数maxRequests设置okHttpClient最多可以同时请求的数量,默认值是64。

在了解了OkHttp的同步异步请求是通过Call完成的后,再看OkHttp的连接池和线程池。

连接池ConnectionPool 线程池ThreadPool

在之前提到,在创建OkHttpClient对象时,创建了ConnecitonPool对象,但是该对象中并没有设置连接池的大小,而只是设置了最大空闲连接数和空闲存活时长,同时了解OkHttp发送请求分为同步、异步两种方式,并且都是通过Call这个类完成的,在请求转发时又通过了Dispatcher这个类。

这里我们通过源码来了解其背后的原理。

同步请求

public Response execute() throws IOException {
    synchronized(this) {
        if (this.executed) {
            throw new IllegalStateException("Already Executed");
        }

        this.executed = true;
    }

    Response var2;
    try {
        this.client.dispatcher().executed(this); // 将本次请求放入Dispatcher类中的正在运行的同步请求队列 runningSyncCalls
        Response result = this.getResponseWithInterceptorChain(); // 拦截链处理并进行请求,获取响应
        if (result == null) {
            throw new IOException("Canceled");
        }

        var2 = result;
    } finally {
        this.client.dispatcher().finished(this); //将执行完的请求从runingSyncCalls队列删除,并统计所有在执行请求的数量
    }

    return var2;
}

继续跟踪getResponseWithInterceptorChain()方法,了解如何实现同步请求。

private Response getResponseWithInterceptorChain() throws IOException {
    List<Interceptor> interceptors = new ArrayList();
    interceptors.addAll(this.client.interceptors());
    interceptors.add(this.retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(this.client.cookieJar()));
    interceptors.add(new CacheInterceptor(this.client.internalCache()));
    interceptors.add(new ConnectInterceptor(this.client));
    if (!this.retryAndFollowUpInterceptor.isForWebSocket()) {
        interceptors.addAll(this.client.networkInterceptors());
    }

    interceptors.add(new CallServerInterceptor(this.retryAndFollowUpInterceptor.isForWebSocket()));
    Chain chain = new RealInterceptorChain(interceptors, (StreamAllocation)null, (HttpStream)null, (Connection)null, 0, this.originalRequest);
    return chain.proceed(this.originalRequest);
}

通过以上代码,我们可以看到除了我们在创建client对象时配置的拦截器外,还会自动添加五个拦截器,分别是:RetryAndFollwUpInterceptor, BridgeInterceptor, CacheInterceptor, ConnectInterceptor和CallServerInterceptor。

在生成拦截链Chain后,调用proceed方法处理请求。该方法在拦截链执行过程中被递归调用。

经过断点追踪,发现这五个拦截器的执行顺序为 RetryAndFollwUpInterceptor -> BridgeInterceptor -> CacheInterceptor -> ConnectInterceptor -> CallServerInterceptor。每个拦截器的作用这里简单说明下,更详细的内容感兴趣的同学可以自己追踪下源码。

RetryAndFollwUpInterceptor 处理连接失败重试和重定向的后续请求。当没有中断cancel请求时,会循环调用proceed方法。

BridgeInterceptor 处理请求头参数和响应头参数,当没有设置时,会添加Host, Connetion, Accept-Encoding, User-Agent请求头参数,这里我们可以看到OkHttp建立连接时默认是长连接 Keep-Alive。

if (userRequest.header("Connection") == null) {
    requestBuilder.header("Connection", "Keep-Alive");
}

CacheInterceptor 根据缓存策略处理响应结果。

ConnectInterceptor 连接拦截器, 从StreamAllocation获取连接。

public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain)chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    HttpStream httpStream = streamAllocation.newStream(this.client, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();
    return realChain.proceed(request, streamAllocation, httpStream, connection);
}

可以看到StreamAllocatio获取连接前,调用newStream创建了HttpStream对象。追踪其源码可以看到获取连接的源码:

 private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout, boolean connectionRetryEnabled) throws IOException {
    ConnectionPool var6 = this.connectionPool;
    Route selectedRoute;
    synchronized(this.connectionPool) {
        if (this.released) {
            throw new IllegalStateException("released");
        }

        if (this.stream != null) {
            throw new IllegalStateException("stream != null");
        }

        if (this.canceled) {
            throw new IOException("Canceled");
        }

        RealConnection allocatedConnection = this.connection;
        if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
            return allocatedConnection;
        }

        RealConnection pooledConnection = Internal.instance.get(this.connectionPool, this.address, this);  // 根据Adress从连接池获取连接
        if (pooledConnection != null) {
            this.connection = pooledConnection;
            return pooledConnection;
        }

        selectedRoute = this.route; // 若连接池没有该Adress的连接,则选择Route尝试建立连接
    }

    if (selectedRoute == null) {
        selectedRoute = this.routeSelector.next();
        var6 = this.connectionPool;
        synchronized(this.connectionPool) {
            this.route = selectedRoute;
            this.refusedStreamCount = 0;
        }
    }

    RealConnection newConnection = new RealConnection(selectedRoute);
    this.acquire(newConnection); 
    ConnectionPool var16 = this.connectionPool;
    synchronized(this.connectionPool) {
        Internal.instance.put(this.connectionPool, newConnection); // 将新连接添加到连接池
        this.connection = newConnection;
        if (this.canceled) {
            throw new IOException("Canceled");
        }
    }

    newConnection.connect(connectTimeout, readTimeout, writeTimeout, this.address.connectionSpecs(), connectionRetryEnabled);  // RealConnection的connect创建连接
    this.routeDatabase().connected(newConnection.route());
    return newConnection;
}

在connect方法中使用Route创建连接:

if (this.route.requiresTunnel()) {
    this.buildTunneledConnection(connectTimeout, readTimeout, writeTimeout, connectionSpecSelector);
} else {
    this.buildConnection(connectTimeout, readTimeout, writeTimeout, connectionSpecSelector);
}

以上源码证明了之前说明的OkHttp建立连接的过程。同时也证明了,对于同步请求,要控制连接池的大小,需要请求发送者控制同时请求的数量。

CallServerInterceptor 在该拦截器中完成请求获取响应,并且在请求结束后根据请求头和响应头的头部参数Connection判断是否关闭连接

if ("close".equalsIgnoreCase(response.request().header("Connection")) || "close".equalsIgnoreCase(response.header("Connection"))) {
    streamAllocation.noNewStreams();
}

通过ConnectInterceptor源码,我们已经知道OkHttp默认使用长连接,但是这里也会判断服务端的响应头Connection参数值,所以证明了在解决connection reset所说的客户端的服务端要一致,要么都用长连接,要么都用短连接。

异步请求

public void enqueue(Callback responseCallback) {
    synchronized(this) {
        if (this.executed) {
            throw new IllegalStateException("Already Executed");
        }

        this.executed = true;
    }

    this.client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback));
}

可以看到在进行异步请求时,根据回调函数又创建了RealCall内部类AsynCall对象。接着调用了Dispatcher对象的enqueue方法,在该方法中对请求数量根据之前的配置进行了控制。

synchronized void enqueue(AsyncCall call) {
    if (this.runningAsyncCalls.size() < this.maxRequests && this.runningCallsForHost(call) < this.maxRequestsPerHost) {
        this.runningAsyncCalls.add(call); // 放入正在异步请求的队列  方便统计所有的请求
        this.executorService().execute(call); // 使用线程池处理请求
    } else {
        this.readyAsyncCalls.add(call); // 超过请求数限制,放入异步请求队列
    }

}

根据源码可以看到根据之前配置的maxRequests和maxRequestsPerHost参数值对请求进行了限制,若不超过则使用了线程池处理请求,否则添加到异步请求队列中

针对线程池的配置,我们也可以从源码看到,线程池最大线程数为2147483647

public synchronized ExecutorService executorService() {
    if (this.executorService == null) {
        this.executorService = new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue(), Util.threadFactory("OkHttp Dispatcher", false));
    }

    return this.executorService;
}

在执行异步请求时,调用AsyncCall的execute()方法:

protected void execute() {
    boolean signalledCallback = false;

    try {
        Response response = RealCall.this.getResponseWithInterceptorChain();
        if (RealCall.this.retryAndFollowUpInterceptor.isCanceled()) {
            signalledCallback = true;
            this.responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
            signalledCallback = true;
            this.responseCallback.onResponse(RealCall.this, response);
        }
    } catch (IOException var6) {
        if (signalledCallback) {
            Platform.get().log(4, "Callback failure for " + RealCall.this.toLoggableString(), var6);
        } else {
            this.responseCallback.onFailure(RealCall.this, var6);
        }
    } finally {
        RealCall.this.client.dispatcher().finished(this);
    }

}

该方法进行请求获取响应与同步请求一样,都是通过getResponseWithInterceptorChain()方法,这里不再赘述。同样的在请求完成后,调用了Dispatcher的finished方法。但是该方法与同步请求不同,该方法的promoteCalls参数值设置为了true。

void finished(AsyncCall call) {
    this.finished(this.runningAsyncCalls, call, true);
}

finished方法:

 private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
    int runningCallsCount;
    Runnable idleCallback;
    synchronized(this) {
        if (!calls.remove(call)) {
            throw new AssertionError("Call wasn't in-flight!");
        }

        if (promoteCalls) {
            this.promoteCalls(); // 将准备执行的异步请求放到线程池处理
        }

        runningCallsCount = this.runningCallsCount();
        idleCallback = this.idleCallback;
    }

    if (runningCallsCount == 0 && idleCallback != null) {
        idleCallback.run();
    }

}

可以看到,异步请求在调用finished方法时,会调用promoteCalls()方法。在该方法中,会判断如果当前进行的请求数小于maxRequests并且请求的host上的请求数小于masRequestsPerHost,则会处理请求

private void promoteCalls() {
    if (this.runningAsyncCalls.size() < this.maxRequests) { // 判断正在进行的异步请求数是否小于总数限制
        if (!this.readyAsyncCalls.isEmpty()) {
            Iterator i = this.readyAsyncCalls.iterator();

            do {
                if (!i.hasNext()) {
                    return;
                }

                AsyncCall call = (AsyncCall)i.next();
                if (this.runningCallsForHost(call) < this.maxRequestsPerHost) { // 判断请求的host上的正在进行的请求数是否小于限制
                    i.remove();
                    this.runningAsyncCalls.add(call);
                    this.executorService().execute(call); // 使用线程池处理请求
                }
            } while(this.runningAsyncCalls.size() < this.maxRequests);

        }
    }
}

至此,通过以上源码我们可以看到,对于异步请求,想要控制连接池和线程池的大小,需要发送者设置恰当的maxRequests和maxRequestsPerHost参数值

本篇文章从client单例, 长连接,同步异步请求,连接池 线程池几个方面了解了OkHttp,其他方面更多内容请查看官方文档。

参考资料:
https://square.github.io/okhttp/connections/
https://blog.insightdatascience.com/learning-about-the-http-connection-keep-alive-header-7ebe0efa209d?gi=a5b2d74099c7
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive
https://tomcat.apache.org/connectors-doc/common_howto/timeouts.html
https://stackoverflow.com/questions/33281810/java-okhttp-reuse-keep-alive-connection
https://github.com/square/okhttp/issues/2031
https://www.jianshu.com/p/da5c303d1df4?tdsourcetag=s_pcqq_aiomsg
https://www.vogella.com/tutorials/JavaLibrary-OkHttp/article.html
https://juejin.im/post/5e156c80f265da5d3c6de72a
https://mp.weixin.qq.com/s/fy84edOix5tGgcvdFkJi2w
https://stackoverflow.com/questions/49069297/okhttpclient-connection-pool-size-dilemma/49070993#49070993

  • 12
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值