OkHttpClient源码:连接池与响应缓存

《OkHttpClient源码:同步/异步请求》中,介绍了同步/异步请求流程。现在我们一起来看看OkHttp中连接池和缓存实现。

一、连接池

RealConnectionPool类

OkHttpClient.Builder()中,为Client创建了连接池,起初其中没有任何连接(尚未发生任何http请求)。 ConnectionPool是装饰类,真正的连接池是RealConnectionPool,默认最大空闲连接数为5,存活时间为5分钟。 RealConnectionPool的关键属性如下,使用ArrayDeque保存连接,队列最大尺寸为Integer.MAX_VALUE(相当于无限制)。

java 复制代码
public final class RealConnectionPool {
  // 后台线程,用于清除连接池中过期的连接。
  private static final Executor executor = new ThreadPoolExecutor(0 ,
      Integer.MAX_VALUE , 60L , TimeUnit.SECONDS,
      new SynchronousQueue<>(), Util.threadFactory("OkHttp ConnectionPool", true));

  // 最大空闲连接数,默认为5
  private final int maxIdleConnections;
  // 存活时间,默认为5分钟
  private final long keepAliveDurationNs;
  // 清理逻辑
  private final Runnable cleanupRunnable;
  // 连接队列
  private final Deque<RealConnection> connections = new ArrayDeque<>();
  // 进行清理标志,防止并发
  boolean cleanupRunning;
  
  // 省略......
}

RealConnection类

它的关键属性如下,其中可以看到熟悉的java.net.Socket对象,该连接指向的目标地址即route。因为只能对访问同一主机的多个HTTP请求间,从能复用同一个socket对象。

java 复制代码
public final class RealConnection extends Http2Connection.Listener implements Connection {
  public final RealConnectionPool connectionPool;
  // 即目标服务器Addresses
  private final Route route;

  // TCP层面的Socket
  private Socket rawSocket;

  // application层面的Socket,比如SSLSocket
  private Socket socket;
  private Protocol protocol;
  private Http2Connection http2Connection;

  // 失败计数
  int routeFailureCount;

  // 成功计数
  int successCount;

  // 关联的transmitter
  final List<Reference<Transmitter>> transmitters = new ArrayList<>();
  
  // 省略......
}

可以通过HTTP proxy或IP address来创建Route,指明要访问的Web Server。

java 复制代码
public final class Route {
  final Address address;
  final Proxy proxy;
  final InetSocketAddress inetSocketAddress;
}

ConnectInterceptor类

当执行一个同步或异步请求,拦截器链执行到ConnectInterceptor时,会为该请求创建一个Exchange对象,此时需要先创建ExchangeCodec对象。

java 复制代码
// 先获取一个连接;如果是http 2.0版本,则返回Http2ExchangeCodec;否则返回Http1ExchangeCodec
ExchangeCodec codec = exchangeFinder.find(client, chain, doExtensiveHealthChecks);
Exchange result = new Exchange(this, call, eventListener, exchangeFinder, codec);

ExchangeFinder会从连接池中获取一个与当前请求Route相同的健康连接。

  • 先尝试从连接池中获取连接:遍历连接池所有连接,返回第一个匹配的连接来复用。
java 复制代码
  if (result == null) {
	// 从连接池获取连接
	if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
	  foundPooledConnection = true;
	  result = transmitter.connection;
	} else {
	  selectedRoute = previousRoute;
	}
  }

匹配逻辑RealConnection.isEligible():对于HTTP1.0请求,只检查host是否相同。

java 复制代码
// 本次请求的host,与RealConnection的route属性匹配
if (address.url().host().equals(this.route().address().url().host())) {
  return true;
}

如果从池中没有找到可复用的,则创建一个新的RealConnection对象,调用RealConnection.connect()与目标主机建立Socket连接,并放入连接池。 将获取的连接赋值给Transmitter.connection属性,将在最后一个拦截器CallServerInterceptor中,用来完成网络I/O。

类图

注意,处理每一个Request时,都会创建一个Transmitter、Interceptor.Chain。

二、响应缓存

启用缓存功能

OkHttp实现了一个可选的、默认关闭的缓存,在创建OkHttpClient时可以启用它。

java 复制代码
public static final File CACHE_DIRECTORY = new File("D:/okhttp/cache");
// 50MB
public static final long MAX_SIZE = 50L * 1024L * 1024L;

public static final OkHttpClient client = new OkHttpClient.Builder()
    .cache(new Cache(CACHE_DIRECTORY, MAX_SIZE))
    .build();

缓存内容保存在本地磁盘,驱逐策略是LRU。

java 复制代码
public final class Cache implements Closeable, Flushable {
  // 缓存url、响应行、响应头的文件的后缀
  private static final int ENTRY_METADATA = 0;
  // 缓存请求体的文件后缀
  private static final int ENTRY_BODY = 1;
  private static final int ENTRY_COUNT = 2;
  // 缓存(用磁盘文件)
  final DiskLruCache cache;
  
  Cache(File directory, long maxSize, FileSystem fileSystem) {
    this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
  }
  // 省略......
}

缓存的实现

先来看看缓存现象。当执行一个查询请求时,如http://localhost:8010/user/queryAll,该接口返回用户信息。会发现目录中创建了这些文件。其中,dac35e41fc4294e5d0737d4916452906是根据request.url()计算获得的key。 dac35e41fc4294e5d0737d4916452906.0中内容包括请求url、响应行、响应头,如下。 dac35e41fc4294e5d0737d4916452906.1中内容是响应体,如下。

再来看源码,Cache类承担着新增、删除、驱逐、查找缓存等功能。

我们来看看查找缓存的逻辑。

java 复制代码
  // 计算key
  public static String key(HttpUrl url) {
    return ByteString.encodeUtf8(url.toString()).md5().hex();
  }

  // 查找缓存,命中时返回Response
  Response get(Request request) {
	// 用url计算key
    String key = key(request.url());
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
      // 查找缓存
      snapshot = cache.get(key);
      if (snapshot == null) {
        return null;
      }
    } catch (IOException e) {
      // Give up because the cache cannot be read.
      return null;
    }

    try {
      // 从key.0文件读取响应行、响应头
      entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    } catch (IOException e) {
      Util.closeQuietly(snapshot);
      return null;
    }
    // 从key.1文件读取响应体
    Response response = entry.response(snapshot);

    if (!entry.matches(request, response)) {
      Util.closeQuietly(response.body());
      return null;
    }

    return response;
  }

GET请求命中缓存

在处理一个请求的拦截器链中,在ConnectInterceptor之前是CacheInterceptor new CacheInterceptor()的入参是对InternalCache接口的一个匿名实现,被Cache类创建和持有,所有功能都由Cache类承载。 CacheInterceptor的拦截逻辑如下:

  • 根据请求url查找响应缓存,创建缓存使用策略;
  • 命中缓存且无需验证其有效性时,用缓存响应构建Response并返回;
  • 未命中缓存,或命中了但需要向服务器验证其是否有效时,向服务器发起http请求;
  • 如果状态码为304,即本地缓存未过期,将使用缓存作为响应;
  • 如果状态码不是304,此时服务器会返回最新资源,则更新本地缓存。
java 复制代码
  @Override 
  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 (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    Response networkResponse = null;
    try {
      // 发起Http请求
      networkResponse = chain.proceed(networkRequest);
    }

    
    if (cacheResponse != null) {
      // 服务器响应304: Not Modified。缓存仍有效
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();
	      // 维护计数
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }

    // 构建响应
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (cache != null) {
      // 响应有响应体,且允许使用缓存(状态码满足要求、请求头和响应头Cache-Control不是no-store)时,将响应缓存
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
      // 非GET请求则移除缓存
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        cache.remove(networkRequest);
      }
    }

    return response;
  }

通过分析可知,OkHttp的缓存实现与浏览器端缓存大体一样,通过响应头Cache-control判断是否缓存、如何使用缓存,通过请求头If-Modified-Since、If-None-Match验证缓存是否有效。这一点在官网介绍中也有说明:

翻译:OkHttp实现了一个可选的,默认关闭的缓存。OkHttp的目标是RFC正确和实用的缓存行为,遵循现实世界中常见的浏览器(如Firefox/Chrome)和服务器行为

对浏览器缓存感兴趣的小伙伴,可以浏览这篇文章《看懂浏览器缓存》

GET请求缓存策略

CacheControl类实现了对HTTP协议Cache-control请求头的封装。在创建某个请求时,可以设置不同的缓存策略。看看该类的属性,是不是与Cache-control头几乎一样。

  • 跳过缓存,直接从服务器获取数据时,可以使用no-cache指令:
scss 复制代码
Request request = new Request.Builder()
    // 或者使用CacheControl.FORCE_NETWORK
    .cacheControl(new CacheControl.Builder().noCache().build())
    .url("http://publicobject.com/helloworld.txt")
    .build();
  • 需要使用缓存时强制向服务器验证缓存有效性时,可以使用max-age=0指令,它与no-cache不同之处在于,当服务器发现请求资源无更新时,响应状态码304即Not Modified且无响应体,提示客户端放心地使用缓存。
scss 复制代码
Request request = new Request.Builder()
    .cacheControl(new CacheControl.Builder().maxAge(0, TimeUnit.SECONDS).build())
    .url("http://publicobject.com/helloworld.txt")
    .build();
  • 要限制请求只能使用本地缓存的资源时,可以添加only-if-cached指令:
scss 复制代码
Request request = new Request.Builder()
    .cacheControl(new CacheControl.Builder().onlyIfCached().build())
    .url("http://publicobject.com/helloworld.txt")
    .build();
Response forceCacheResponse = client.newCall(request).execute();
if (forceCacheResponse.code() != 504) {
  // 命中缓存,使用它
} else {
  // 资源未被缓存
}
  • 当允许使用过期缓存作为响应时,使用max-stale指令,指定最大过期时间:
scss 复制代码
Request request = new Request.Builder()
    .cacheControl(new CacheControl.Builder().maxStale(10, TimeUnit.DAYS).build())
    .url("http://publicobject.com/helloworld.txt")
    .build();

当一个GET请求不设置缓存策略时,okhttp会根据请求头来生成CacheControl属性,其中部分属性默认值如图,可知此时也会保存响应、生成缓存文件

非GET请求是否缓存

对于非GET请求设置CacheControl,或者不主动设置CacheControl(此时noStore=false)时,是否会生成缓存文件呢?答案是不会的,来看源码。

Don't cache non-GET responses. We're technically allowed to cache HEAD requests and some POST requests, but the complexity of doing so is high and the benefit is low.

翻译:不要缓存非get响应。从技术上讲,我们可以缓存HEAD请求和一些POST请求,但是这样做的复杂性很高,而收益却很低。

当启用缓存功能时,对获取动态资源到请求,可主动为请求设置CacheControl.FORCE_NETWORK或new CacheControl.Builder().noStore().build();否则将生成缓存文件占用有限的缓存空间。

相关推荐
用户21411832636029 分钟前
dify案例分享-零代码搞定 DIFY 插件开发:小白也能上手的文生图插件实战
后端
计算机程序员小杨19 分钟前
计算机专业的你懂的:大数据毕设就选贵州茅台股票分析系统准没错|计算机毕业设计|数据可视化|数据分析
java·大数据
y1y1z23 分钟前
EasyExcel篇
java·excel
DokiDoki之父43 分钟前
多线程—飞机大战排行榜功能(2.0版本)
android·java·开发语言
高山上有一只小老虎1 小时前
走方格的方案数
java·算法
whatever who cares1 小时前
Java 中表示数据集的常用集合类
java·开发语言
石小石Orz1 小时前
效率提升一倍!谈谈我的高效开发工具链
前端·后端·trae
JavaArchJourney2 小时前
TreeMap 源码分析
java
孟永峰_Java2 小时前
凌晨线上崩盘:NoClassDefFoundError血案纪实!日志里这行「小字」才是救世主
后端·代码规范
whitepure2 小时前
万字详解Java中的IO及序列化
java·后端