说明
在项目中对第三方服务的调用,使用了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将对请求和响应做以下操作:
- 为了请求的准确性和效率,OkHttp在进行实际请求前会重写发送者的请求(Rewriting Requests)。
- 当使用透明传时或者使用缓存时,OkHttp将重写响应(Rewriting Responses)。
- 当请求地址改变,响应302时,OkHttp将进行重定向的后续请求(Follow-up Requests)。
- 当连接失败时,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