(4.2.36.4)HTTP之OkHttp(四): OkHttp源码解析

文章目录

  • 一、OkHttp优点
  • 二、源码涉及的主要几个对象
    • 2.1 OkHttp网络请求流程
    • 2.2 Dispatcher任务调度
      • 2.2.1 Dispatcher的enqueue方法
      • 2.2.2 RealCall执行任务
      • 2.2.3 拦截器
        • RetryAndFollowUpInterceptor:进行连接失败重新连接,以及重定向
        • BridgeInterceptor:对请求头和响应头进行修改
        • CacheInterceptor:读取缓存和更新缓存的操作
        • ConnectInterceptor:与服务器进行连接
        • CallServerInterceptor:发送请求和接收数据
        • 总结
    • 2.3 OkHttp的复用连接池
      • 2.3.1 主要变量与构造方法
      • 2.3.2 缓存操作
      • 2.3.3 自动回收连接
      • 2.3.4 引用计数
      • 2.3.5 总结
  • 参考文献

源码开始之前我先贴一段OkHttp请求网络的实例

OkHttpClient mOkHttpClient = new OkHttpClient();

final Request request = new Request.Builder()
		.url("https://www.jianshu.com/u/b4e69e85aef6")
		.addHeader("user_agent","22222")
		.build();
		
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback() {
	@Override
	public void onFailure(Call call, IOException e) {

	}

	@Override
	public void onResponse(Call call, Response response) throws IOException {
		if(response != null )
		Log.i(TAG, "返回服务端数据:"+ String.valueOf(response.body().string()));
	}
});

支持的最大并发请求数量64,最多通道5

  • mOkHttpClient.newCall首先会new一个Call对象出来,但其实真正new出来的对象是NewCall对象
  • 然后会执行NewCall的enqueue方法
  • NewCall#enqueue该方法中首先判断请求有没有被执行,如果请求已经执行,那么直接抛出异常,如果请求没有执行,就会执行Dispatcher对象的enqueue方法
  • Dispatcher的enqueue方法:
    1. 如果正在运行的异步请求数量小于最大的并发数,且正在运行的客户端实际数量请求小于规定的每个主机最大请求数量,那么就把该请求放进正在运行的异步请求队列。否则就把该请求放进将要执行的异步请求队列中
  • RealCall执行任务
    1. RealCall通过执行getResponseWithInterceptorChain()返回Response,如果请求被取消则在进行OnFailue回调,如果请求成功则进行onResponse的回调。
      1. 在配置 OkHttpClient 时设置的 interceptors ()
      2. 负责失败重试以及重定向的RetryAndFollowUpInterceptor
      3. 负责把用户构造的请求转换为发送到服务器的请求、把服务器返回的响应转为用户友好的响应的 BridgeInterceptor
      4. 负责读取缓存直接返回、更新缓存的 CacheInterceptor
      5. 负责和服务器建立连接的 ConnectInterceptor
      6. 配置 OkHttpClient 时设置的 networkInterceptors
      7. 负责向服务器发送请求数据、从服务器读取响应数据的 CallServerInterceptor
      8. 在 return chain.proceed(originalRequest),中开启链式调用
    2. client.dispatcher().finished(this); 执行完成后移除任务,并触发 readyAsyncCalls 的执行

一、OkHttp优点

OkHttp是一个高效的Http客户端,有如下的特点:

  • 支持HTTP2/SPDY黑科技
  • socket自动选择最好路线,并支持自动重连
  • 拥有自动维护的socket连接池,减少握手次数
  • 拥有队列线程池,轻松写并发
  • 拥有Interceptors轻松处理请求与响应(比如透明GZIP压缩,LOGGING)
  • 基于Headers的缓存策略(不仅可以缓存数据,就连响应头都给缓存了)

二、源码涉及的主要几个对象

  • Call:对请求的封装,有异步请求和同步请求。
  • Dispatcher:任务调度器
  • Connection:是RealConnection的父类接口,表示对JDK中的物理socket进行了引用计数封装,用来控制socket连接
  • HttpCodec:对Http请求进行编码,对Http响应进行解码,由于Http协议有基于HTTP1.0和Http2.0的两种情况,Http1Code代表基于Http1.0协议的方式,Http2Code代表基于Http2.0协议的方式。
  • StreamAllocation: 用来控制Connections/Streams的资源分配与释放
  • RouteDatabase:用来保存连接的错误路径,以便能提升连接的效率。
  • RetryAndFollowUpInterceptor 负责失败重试以及重定向的拦截器
  • BridgeInterceptor: 负责把用户构造的请求转换为发送到服务器的请求、把服务器返回的响应转为用户友好的响应的
  • CacheInterceptor: 负责读取缓存直接返回、更新缓存
  • ConnectInterceptor: 负责和服务器建立连接的
  • CallServerInterceptor:负责向服务器发送请求数据、从服务器读取响应数据

2.1 OkHttp网络请求流程

Call call = mOkHttpClient.newCall(request);
call.enqueue(xxx);

  1. 首先会new一个Call对象出来,但其实真正new出来的对象是NewCall对象
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    // Safely publish the Call instance to the EventListener.
    RealCall call = new RealCall(client, originalRequest, forWebSocket);
    call.eventListener = client.eventListenerFactory().create(call);
    return call;
  }
  1. 然后会执行call的enqueue方法
@Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }
  1. 该方法中首先判断请求有没有被执行,如果请求已经执行,那么直接抛出异常,如果请求没有执行,就会执行Dispatcher对象的enqueue方法

2.2 Dispatcher任务调度

Dispatcher的各个参数的说明如下

//支持的最大并发请求数量
private int maxRequests = 64;
//每个主机的最大请求数量
private int maxRequestsPerHost = 5;

//请求线程池
private @Nullable ExecutorService executorService;

//将要运行的异步请求队列
private final Deque readyAsyncCalls = new ArrayDeque<>();

//正在运行的异步请求队列
private final Deque runningAsyncCalls = new ArrayDeque<>();

//正在运行的同步请求队列
private final Deque runningSyncCalls = new ArrayDeque<>();


2.2.1 Dispatcher的enqueue方法

回到之前说的,call的enqueue方法其实执行的使Dispatcher的enqueue方法,Dispatcher之后会把call放进请求队列中,最终执行由线程池来执行请求任务。
}

synchronized void enqueue(AsyncCall call) {
    if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
      runningAsyncCalls.add(call);
      executorService().execute(call);
    } else {
      readyAsyncCalls.add(call);
    }
  }

  }

如果正在运行的异步请求数量小于最大的并发数,且正在运行的客户端实际数量请求小于规定的每个主机最大请求数量,那么就把该请求放进正在运行的异步请求队列中,否则就把该请求放进将要执行的异步请求队列中。

继续看看Dispatcher的executorService方法,如下:

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

Dispatcher初始化了一个线程池,核心线程的数量为0 ,最大的线程数量为Integer.MAX_VALUE,空闲线程存在的最大时间为60秒,这个线程类似于CacheThreadPool,比较适合执行大量的耗时比较少的任务。同时我们Dispatcher也可以来设置自己线程池。

2.2.2 RealCall执行任务

下面来看看RealCall里究竟执行了什么任务:

@Override protected void execute() {
      boolean signalledCallback = false;
      try {
        Response response = getResponseWithInterceptorChain();
        if (retryAndFollowUpInterceptor.isCanceled()) {
          signalledCallback = true;
          responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
        } else {
          signalledCallback = true;
          responseCallback.onResponse(RealCall.this, response);
        }
      } catch (IOException e) {
       ...
      } finally {
        client.dispatcher().finished(this);
      }
    }
  }

  1. RealCall通过执行getResponseWithInterceptorChain()返回Response,如果请求被取消则在进行OnFailue回调,如果请求成功则进行onResponse的回调。
    1. 请求如果被取消,其回调实在onFailue中进行回调的
    2. enqueue方法的回调是在子线程中完成的
  2. client.dispatcher().finished(this); 执行完成后移除任务,并触发 readyAsyncCalls 的执行

2.2.3 拦截器

那么RealCall 的getResponseWithInterceptorChain方法中究竟干了些什么呢,它是如何返回Response的呢?

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());//1
    interceptors.add(retryAndFollowUpInterceptor);//2
    interceptors.add(new BridgeInterceptor(client.cookieJar()));//3
    interceptors.add(new CacheInterceptor(client.internalCache()));//4
    interceptors.add(new ConnectInterceptor(client));//5
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());//6 
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));//7

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);//8
  }

  1. 在配置 OkHttpClient 时设置的 interceptors ()
  2. 负责失败重试以及重定向的RetryAndFollowUpInterceptor
  3. 负责把用户构造的请求转换为发送到服务器的请求、把服务器返回的响应转为用户友好的响应的 BridgeInterceptor
  4. 负责读取缓存直接返回、更新缓存的 CacheInterceptor
  5. 负责和服务器建立连接的 ConnectInterceptor
  6. 配置 OkHttpClient 时设置的 networkInterceptors
  7. 负责向服务器发送请求数据、从服务器读取响应数据的 CallServerInterceptor
  8. 在 return chain.proceed(originalRequest),中开启链式调用

RealInterceptorChain的proceed方法源码如下:

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();

    calls++;

    // If we already have a stream, confirm that the incoming request will use it.
    if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must retain the same host and port");
    }

    // If we already have a stream, confirm that this is the only call to chain.proceed().
    if (this.httpCodec != null && calls > 1) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must call proceed() exactly once");
    }

    // Call the next interceptor in the chain.
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
        connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
        writeTimeout);
    Interceptor interceptor = interceptors.get(index);
    Response response = interceptor.intercept(next);

    // Confirm that the next interceptor made its required call to chain.proceed().
    if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
      throw new IllegalStateException("network interceptor " + interceptor
          + " must call proceed() exactly once");
    }

    // Confirm that the intercepted response isn't null.
    if (response == null) {
      throw new NullPointerException("interceptor " + interceptor + " returned null");
    }

    if (response.body() == null) {
      throw new IllegalStateException(
          "interceptor " + interceptor + " returned a response with no body");
    }

    return response;
}

public interface Interceptor {
  Response intercept(Chain chain) throws IOException;

   interface Chain {
		Request request();

		Response proceed(Request request) throws IOException;

	}
}


首先来了解一下拦截器吧,拦截器是一种能够监控、重写,重试调用的机制。通常情况下,拦截器用来添加、移除、转换请求和响应的头部信息。比如将域名替换为IP地址,在请求头中移除添加host属性;也可以添加我们应用中的一些公共参数,比如设备id、版本号,等等。

事实上OkHttp就是通过定义许多拦截器一步一步地对Request进行拦截处理(从头至尾),直到请求返回网络数据,后面又倒过来,一步一步地对Response进行拦截处理,最后拦截的结果就是回调的最终Response。(从尾至头)

回头再看RealInterceptorChain的proceed方法,通过顺序地传入一个拦截器的集合,创建一个RealInterceptorChain,然后拿到之前OkHttp创建的各种拦截器,并调用其interrupt方法,并返回Response对象。其调用顺序如下:

在这里插入图片描述
【图】

RetryAndFollowUpInterceptor:进行连接失败重新连接,以及重定向

@Override public Response intercept(Chain chain) throws IOException {
	Request request = chain.request();
	RealInterceptorChain realChain = (RealInterceptorChain) chain;
	Call call = realChain.call();
	...

	followUpCount = 0;
	Response priorResponse = null;
	while (true) {
		if (canceled) {
		  streamAllocation.release();
		  throw new IOException("Canceled");
		}

		Response response;
		boolean releaseConnection = true;
		try {
		  response = realChain.proceed(request, streamAllocation, null, null);//【递归】
		...

		Request followUp = followUpRequest(response);   
		if (followUp == null) {
			  if (!forWebSocket) {
				streamAllocation.release();
		}
		  return response;
		}

		...

		 if (++followUpCount > MAX_FOLLOW_UPS) {
		  streamAllocation.release();
		 throw new ProtocolException("Too many follow-up requests: " + followUpCount); }
		...
			
		 request = followUp;
		  priorResponse = response;
		  }
	}
}	

整段代码就是在一个死循环

  1. 可以看出重连接的次数最多为20次
  2. 重定向功能的逻辑在followUpRequest方法中,这个方法会根据响应头中的location字段获取重定向的url,并通过requestBuilder重新new一个Request对象,并改变request的response的值,然后重新进行拦截。

BridgeInterceptor:对请求头和响应头进行修改

主要流程如下

1、获取请求,请求的body,根据body的长度进行相关的 头

2、其他一些头的设置,如"User-Agent"、"Connection"等

3、调用chain.proceed(requestBuilder.build());传递给下一个拦截器进行处理,并返回Response

4、是否有cookie,响应是否使用了gzip,如果有的话重新设置响应的body

这样BridgeInterceptor就执行完了,从代码中也可以看到,他主要是在请求之前对响应头做了一些检查,并添加一些头,然后在请求之后对响应做一些处理

public Response intercept(Chain chain) throws IOException {
    Request userRequest = chain.request();
    Request.Builder requestBuilder = userRequest.newBuilder();
 
    RequestBody body = userRequest.body();
    if (body != null) {
      MediaType contentType = body.contentType();
      if (contentType != null) {
        requestBuilder.header("Content-Type", contentType.toString());
      }
 
      long contentLength = body.contentLength();
      if (contentLength != -1) {
        requestBuilder.header("Content-Length", Long.toString(contentLength));
        requestBuilder.removeHeader("Transfer-Encoding");
      } else {
        requestBuilder.header("Transfer-Encoding", "chunked");
        requestBuilder.removeHeader("Content-Length");
      }
    }
 
    if (userRequest.header("Host") == null) {
      requestBuilder.header("Host", hostHeader(userRequest.url(), false));
    }
 
    if (userRequest.header("Connection") == null) {
      requestBuilder.header("Connection", "Keep-Alive");
    }
 
    // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
    // the transfer stream.
    boolean transparentGzip = false;
    if (userRequest.header("Accept-Encoding") == null) {
      transparentGzip = true;
      requestBuilder.header("Accept-Encoding", "gzip");
    }
 
    List cookies = cookieJar.loadForRequest(userRequest.url());
    if (!cookies.isEmpty()) {
      requestBuilder.header("Cookie", cookieHeader(cookies));
    }
 
    if (userRequest.header("User-Agent") == null) {
      requestBuilder.header("User-Agent", Version.userAgent());
    }
 
    Response networkResponse = chain.proceed(requestBuilder.build());
 
    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
 
    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);
 
    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
    }
 
    return responseBuilder.build();
  }

CacheInterceptor:读取缓存和更新缓存的操作

具体流程大概如下:

  1. 首先判断用户是否有设置cache,如果有的话,则从用户的cache中获取当前请求的缓存,用户可以通过如下方式设置cache
    • InternalCache是不能直接使用的,而Cache可以直接使用,它里面自己实现了InternalCache
  2. 然后根据前面是否有缓存已经当前请求构造一个CacheStrategy,它的networkRequest代表当前请求,cacheResponse代表当前缓存响应
  3. 如果networkRequest为空则说明不需要网络请求,直接返回当前缓存
  4. 调用 networkResponse = chain.proceed(networkRequest)处理当前请求
  5. 如果缓存不为空,调用validate进行验证,是否需要更新缓存
  6. 如果缓存为空,则保存当前缓存
public Response intercept(Chain chain) throws IOException {
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;
 
    long now = System.currentTimeMillis();
 
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;
 
    if (cache != null) {
      cache.trackResponse(strategy);
    }
 
    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
    }
 
    // If we're forbidden from using the network and the cache is insufficient, fail.
    if (networkRequest == null && cacheResponse == null) {
      return new Response.Builder()
          .request(chain.request())
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(EMPTY_BODY)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();
    }
 
    // If we don't need the network, we're done.
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }
 
    Response networkResponse = null;
    try {
      networkResponse = chain.proceed(networkRequest);
    } finally {
      // If we're crashing on I/O or otherwise, don't leak the cache body.
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }
 
    // If we have a cache response too, then we're doing a conditional get.
    if (cacheResponse != null) {
      if (validate(cacheResponse, networkResponse)) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();
 
        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }
 
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();
 
    if (HttpHeaders.hasBody(response)) {
      CacheRequest cacheRequest = maybeCache(response, networkResponse.request(), cache);
      response = cacheWritingResponse(cacheRequest, response);
    }
 
    return response;
  }

ConnectInterceptor:与服务器进行连接

@Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }


  1. 首先获取前面创建的StreamAllocation,然后调用它的newStream创建一条连接
  2. 把前面创建的RealConnection作为参数继续调用

实际上建立连接就是创建了一个 HttpCodec 对象,它将在后面的步骤中被使用,那它又是何方神圣呢?它是对 HTTP 协议操作的抽象,有两个实现:Http1Codec和 Http2Codec,顾名思义,它们分别对应 HTTP/1.1 和 HTTP/2 版本的实现。

在 Http1Codec中,它利用 Okio 对 Socket 的读写操作进行封装,它对 java.io和 java.nio 进行了封装,让我们更便捷高效的进行 IO 操作。

而创建 HttpCodec 对象的过程涉及到 StreamAllocation、RealConnection代码较长,这个过程概括来说

就是找到一个可用的 RealConnection,再利用 RealConnection 的输入输出(BufferedSource 和 BufferedSink)创建 HttpCodec 对象,供后续步骤使用。

CallServerInterceptor:发送请求和接收数据

 @Override public Response intercept(Chain chain) throws IOException {
    HttpStream httpStream = ((RealInterceptorChain) chain).httpStream();
    StreamAllocation streamAllocation = ((RealInterceptorChain) chain).streamAllocation();
    Request request = chain.request();
 
    long sentRequestMillis = System.currentTimeMillis();
    httpStream.writeRequestHeaders(request);
 
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      Sink requestBodyOut = httpStream.createRequestBody(request, request.body().contentLength());
      BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
      request.body().writeTo(bufferedRequestBody);
      bufferedRequestBody.close();
    }
 
    httpStream.finishRequest();
 
    Response response = httpStream.readResponseHeaders()
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();
 
    if (!forWebSocket || response.code() != 101) {
      response = response.newBuilder()
          .body(httpStream.openResponseBody(response))
          .build();
    }
 
    if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
      streamAllocation.noNewStreams();
    }
 
    int code = response.code();
    if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
      throw new ProtocolException(
          "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
    }
 
    return response;
  }

  1. 首先会写请求头
  2. 查看是否运行当前方法带body,运行则写body
  3. 调用httpStream.finishRequest();刷新
  4. 读取响应头和body并设置Response
  5. 是否需要关闭连接
  6. 返回Response

总结

这里我们可以看到,核心工作都由 HttpCodec 对象完成,而 HttpCodec 实际上利用的是 Okio,而 Okio 实际上还是用的 Socket,所以没什么神秘的,只不过一层套一层,层数有点多。

其实 Interceptor的设计也是一种分层的思想,每个 Interceptor 就是一层。为什么要套这么多层呢?分层的思想在 TCP/IP 协议中就体现得淋漓尽致,分层简化了每一层的逻辑,每层只需要关注自己的责任(单一原则思想也在此体现),而各层之间通过约定的接口/协议进行合作(面向接口编程思想),共同完成复杂的任务,这是典型的责任链设计模式

(4.2.36.4)HTTP之OkHttp(四): OkHttp源码解析_第1张图片
【okhttp_full_process.png】

2.3 OkHttp的复用连接池

Http有一种叫做keepalive connections的机制,而okHttp支持5个并发socket连接,默认keepalive时间为5分钟,接下来我们学习okHttp是如何复用连接的。

2.3.1 主要变量与构造方法

连接池的类位于okHttp.ConnectionPool,它的主要变量如下:


public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
    this.maxIdleConnections = maxIdleConnections;
    this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);

    // Put a floor on the keep alive duration, otherwise cleanup will spin loop.
    if (keepAliveDuration <= 0) {
      throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
    }
  }


private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
      Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
      new SynchronousQueue(), Util.threadFactory("OkHttp ConnectionPool", true));

/** The maximum number of idle connections for each address. */
private final int maxIdleConnections;
private final long keepAliveDurationNs;
private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
  while (true) {
	long waitNanos = cleanup(System.nanoTime());
	if (waitNanos == -1) return;
	if (waitNanos > 0) {
	  long waitMillis = waitNanos / 1000000L;
	  waitNanos -= (waitMillis * 1000000L);
	  synchronized (ConnectionPool.this) {
		try {
		  ConnectionPool.this.wait(waitMillis, (int) waitNanos);
		} catch (InterruptedException ignored) {
		}
	  }
	}
  }
}
};

private final Deque connections = new ArrayDeque<>();//
final RouteDatabase routeDatabase = new RouteDatabase();
boolean cleanupRunning;

通过构造方法可以看出CollectionPool默认空闲的socket最大连接数为5个,socket的keepalive时间为5分钟。CollectionPool实在OkHttpClient实例化的时候创建的

主要变量说明一下:

  • executor线程池:类似于CachedThreadPool,需要注意的是这种线程池的工作队列采用了没有容量的SynchronousQueue。
  • Deque 双向队列:双端队列同时具有队列和栈的性质,经常在缓存中被使用,里面维护了RealConnection也就是Socket物理连接的包装。
  • RouteDatabase :它用来记录连接失败的路线名单,当连接失败时就会把失败的路线加进去。

2.3.2 缓存操作

ConnectionPool提供对Deque进行操作的方法分别为put,get,connectionBecameIdle和evictAll

这几个操作,分别对应放入连接,获取连接,移除连接和移除所有连接操作

只举例说明put和get操作。

void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }

再添加到Deque之前首先要清理空闲线程,这个后面会讲到。再来看看get操作:

  @Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection, true);
        return connection;
      }
    }
    return null;
  }

遍历connections缓存列表。当某个连接计数小于限制的大小,并且request的地址和缓存列表中此连接的地址完全匹配时,则直接复用缓存列表中的connection作为request的连接。

2.3.3 自动回收连接

OkHttp时根据StreamAllocation引用计数是否为0来实现自动回收连接的。我们在put操作前首先要调用executor.execute(cleanupRunnable)来清理闲置的线程。

我们来查看cleanupRunnable到底做了什么?

线程不断地调用clearup方法进行清理,并返回下次需要清理的间隔时间,然后调用wait方法进行等待以释放锁与时间片。当等待时间到了后,再次进行清理,并返回下次需要清理的间隔时间,如此循环下去

接下来看看clearup方法,如下所示:

long cleanup(long now) {
    int inUseConnectionCount = 0;
    int idleConnectionCount = 0;
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

   
    synchronized (this) {
      for (Iterator i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

     
        if (pruneAndGetAllocationCount(connection, now) > 0) {//注释<1>
          inUseConnectionCount++;
          continue;
        }

        idleConnectionCount++;

      
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }

      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {//注释<2>
     
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
    
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
    
        return keepAliveDurationNs;
      } else {
        // No connections, idle or in use.
        cleanupRunning = false;
        return -1;//注释<3>
      }
    }

    closeQuietly(longestIdleConnection.socket());
    return 0;
  }

clearup方法所做的事情非常简单总结就是,根据连接中的引用计数来计算空闲连接数和活跃连接数,然后标记空闲的连接。

  1. 注释<2>:如果空闲连接keepAlive时间超过5分钟,或者空闲连接数超过5个,则从Deque中移除此连接。接下来更具空闲连接或者活跃连接来返回下次需要清理的时间数:
    1. 如果空闲连接大于0,则返回此连接即将到期的时间;
    2. 如果都是活跃连接且大于0,则返回默认的keepAlive时间5分钟;
  2. 注释<3>:如果没有任何连接,则跳出循环并返回-1;
  3. 注释<1>:通过pruneAndGetAllocationCount方法来判断连接是否闲置。如果pruneAndGetAllocationCount方法的返回值大于0则是活跃连接,否则就是空闲连接。
private int pruneAndGetAllocationCount(RealConnection connection, long now) {
    List> references = connection.allocations;
    for (int i = 0; i < references.size(); ) {
      Reference reference = references.get(i);

      if (reference.get() != null) {
        i++;
        continue;
      }

      // We've discovered a leaked allocation. This is an application bug.
      StreamAllocation.StreamAllocationReference streamAllocRef =
          (StreamAllocation.StreamAllocationReference) reference;
      String message = "A connection to " + connection.route().address().url()
          + " was leaked. Did you forget to close a response body?";
      Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace);

      references.remove(i);
      connection.noNewStreams = true;

      // If this was the last allocation, the connection is eligible for immediate eviction.
      if (references.isEmpty()) {//注释<1>
        connection.idleAtNanos = now - keepAliveDurationNs;
        return 0;
      }
    }

    return references.size();
  }

2.3.4 引用计数

在OkHttp的高层代码调用中,使用了类似于引用计数的方式跟踪socket流的调用。这里的计数对象是StreamAllocation,它被反复执行acquire和release操作,这两个方法其实是在改变RealConnection中 List的大小。acquire方法和release方法,如下所示:

public void acquire(RealConnection connection, boolean reportedAcquired) {
    assert (Thread.holdsLock(connectionPool));
    if (this.connection != null) throw new IllegalStateException();

    this.connection = connection;
    this.reportedAcquired = reportedAcquired;
    connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
  }

RealConnection是socket物理连接的包装,它里面维护了
List的引用。List中StreamAllocation的数量也是socket被引用的计数。如果计数为0,则说明此连接没有被复用,也就是空闲的,需要通过下文的算法实现回收;如果计数不为0,则表示上层代码仍然在引用,就无需关闭连接。

2.3.5 总结

可以看出此连接池复用的核心就是用Deque来存储连接,通过put,getconnectionBecameIdle和evictAll几个操作来对Deque进行操作,另外通过判断连接中的计数对象StreamAllocation来进行自动回收连接。

参考文献

  • OkHttp源码解析

你可能感兴趣的:(4.2-android开源组件)