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();否则将生成缓存文件占用有限的缓存空间。

相关推荐
哎呦没15 分钟前
SpringBoot框架下的资产管理自动化
java·spring boot·后端
2401_8576009517 分钟前
SpringBoot框架的企业资产管理自动化
spring boot·后端·自动化
m0_571957582 小时前
Java | Leetcode Java题解之第543题二叉树的直径
java·leetcode·题解
魔道不误砍柴功4 小时前
Java 中如何巧妙应用 Function 让方法复用性更强
java·开发语言·python
NiNg_1_2344 小时前
SpringBoot整合SpringSecurity实现密码加密解密、登录认证退出功能
java·spring boot·后端
闲晨4 小时前
C++ 继承:代码传承的魔法棒,开启奇幻编程之旅
java·c语言·开发语言·c++·经验分享
Chrikk6 小时前
Go-性能调优实战案例
开发语言·后端·golang
幼儿园老大*6 小时前
Go的环境搭建以及GoLand安装教程
开发语言·经验分享·后端·golang·go
canyuemanyue6 小时前
go语言连续监控事件并回调处理
开发语言·后端·golang
杜杜的man6 小时前
【go从零单排】go语言中的指针
开发语言·后端·golang