阅读 1554

OKHttp源码分析

1.HTTP基础

1.1 HTTP介绍

http是客户端与服务端进行通讯的一种协议,它属于tcp/ip协议族中的应用层。

http是无状态协议,即http不对之前请求和响应的状态进行管理。也就是说,无法根据之前的状态进行本次的请求处理。比如,用户登录到一家购物网站,即使他跳转到该站的其他页面后,也需要能继续保持登录状态。针对这个实例,网站为了能够掌握是谁送出去的请求,需要保存用户的状态,http1.1虽然是无状态协议,但为了实现期望的保持状态功能,于是引入Cookie技术。

Cookie技术通过在请求和响应报文中写入Cookie来控制客户端的状态。Cookie会根据从服务端发送的响应报文内的一个叫做Set-Cookie的首部字段信息,通知客户端保存Cookie。在下次请求中,客户端会自动在请求报文中加上保存的Cookie信息,服务端收到了会对比服务器上的记录,然后得到之前的状态信息。

1.1.1 HTTP1.0和HTTP1.1的一些区别

  1. 缓存处理

在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。

  1. 持久连接

HTTP1.0中每进行一次http通讯就要断开一次tcp连接。为此HTTP1.1引入持久连接技术,也称为了HTTP keep-alive。(持久连接旨在建立一次tcp连接后进行多次请求和响应的交互)持久连接的特点是,只要任意一端没有明确提出断开连接,则保持tcp连接状态。在HTTP1.1中,所有连接默认都是持久连接。持久化连接的好处在于减少tcp连接的重复建立和断开所造成的额外开销(毕竟每次都要经历三次握手和四次挥手),减轻了服务端的负载。使Web页面的显示速度也就响应的提高了。

  1. 带宽优化及网络连接的使用

HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1则在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。

  1. Host头处理

在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。

1.1.2 HTTP2.0和HTTP1.X相比的新特性

  1. 新的二进制格式(Binary Format)

HTTP1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮。

  1. 多路复用(MultiPlexing)

即连接共享,即每一个request都是是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的 id将request再归属到各自不同的服务端请求里面。

  1. header压缩

HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。

  1. 服务端推送(server push)

HTTP2.0具有server push功能

1.1.3 HTTP状态码

状态代码有三位数字组成,第一个数字定义了响应的类别,共分五种类别:

1xx:信息性状态码--接受的请求正在处理

2xx:成功状态码--请求正常处理完

3xx:重定向状态码--需要进行附加操作以完成请求

4xx:客户端错误状态码--请求有语法错误或请求无法实现

5xx:服务器错误状态码--服务器处理请求出错

常用状态码

200 从客服端发来的请求在服务器端被正常处理了

204 请求处理成功,但没有资源返回

206 表示客户端进行了范围请求,比如断点下载

301 表示永久性重定向,如果把资源对应的URI保存为书签则会更新书签的URI。比如输入的URI支持https,当输入http时,会返回301。Location指明URI变为https开头

302 临时性重定向。该状态码表示请求的资源已被分配了新的URI,希望用户(本次)能使用新的URI访问。和301不同的是不会去更新书签。

303 该状态码表示由于请求对应的资源存在着另一个URI,应使用GET方法定向获取请求的资源。303和302状态码有着相同的功能,但303状态码明确客户端应当采用GET方法获取资源,这点与302状态码有区别。

304 服务端资源未改变,可直接使用客户端未过期的缓存

307 临时重定向。该状态码与302有着相同的含义。尽管302标准禁止POST变换成GET,但实际使用时大家并不遵守。307会遵循浏览器标准,不会从POST变为GET。

400 该状态码表示请求报文中存在语法错误

401 该状态码表示发送的请求需要有通过HTTP认证(BASIC认证)

403 该状态码表明对请求资源的访问被服务器拒绝了。未获得文件系统的访问授权,访问权限出现某些问题(从未授权的发送源IP地址视图访问)等列举的情况都可能是发生403的原因

404 该状态码表明服务器上无法找到请求的资源。

500 该状态码表明服务器端在执行请求时发生了错误

503 该状态码标明服务器暂时处于超负载或者正在进行停机维护,现在无法处理请求。如果事先得知解除以上状况需要的时间,最好写入Retry-After首部字段再返回给客户端

1.1.4 HTTP缓存

缓存相关header介绍

  • Expires

响应头,代表该资源的过期时间。

  • Cache-Control

请求/响应头,缓存控制字段,精确控制缓存策略。

它有以下常用值

  1. no-store:不缓存包括“中间”缓存服务器
  2. no-cache:防止从缓存中返回过期的资源。客户端发送的请求中如果包含no-cache指令,则表示客户端将不会接受缓存的响应。于是,代理服务器必须把客户端请求转发给源服务器。源服务器会确认缓存是否过期,如果未过期则返回304。如果服务器返回的响应中包含no-cache指令,那么代理服务器不能对资源进行缓存。原服务器以后也将不再对缓存服务器请求中的提出的资源有效性确认,且禁止对响应资源进行缓存操作。
  3. max-age=x(单位秒) 请求缓存后的X秒内不再发起请求,属于http1.1属性,Expires(http1.0属性)类似,但优先级要比Expires高。
  4. s-maxage=x(单位秒) 该指令的功能和max-age指令相同,它们的不同点是s-maxage指令只适用于供多为用户使用的公共缓存资源服务器。也就是说,对于向同一用户重复返回响应的服务器来说,这个指令没有任何作用。另外,当使用s-maxage指令后,则直接忽略对Expires首部字段及max-age指令的处理
  5. public 客户端和代理服务器都可缓存
  6. private 只有客户端可以缓存
  7. min-fresh=x(单位秒)

如果当前时间加上x的值,超了该缓存的过期时间.则要给我一个新的,其功能上有点和max-age类似.但是更大的是语义上的区别。 8. max-stale=x(单位秒) 如果指令未指定参数值,那么无论多久,客户端都会接收响应;如果指令中指定了具体的数值,那么即使过期,只要仍处于max-stale指定的时间内,仍旧会被客户端接收。 9. only-if-cached 表示客户端仅在缓存服务器本地缓存目标资源的情况下才会要求其返回。换言之,该指令要求缓存服务器不重新加载响应,也不会再次确认资源有效性质。若发生请求缓存服务器的本地缓存无响应,则返回状态码504。 10. must-revalidate 使用该指令,代理会向源服务器再次验证即将返回的响应缓存目前是否仍然有效。若代理无法连通源服务器再次获取有效资源的话,缓存必须给客户端一条540状态码。另外,使用该指令会忽略请求的max-stale指令。 11. no-transform 该指令规定不无论是在请求还是响应中,缓存都不能改变实体主体的媒体类型。这样做可防止缓存或代理压缩图片等类似操作。

  • Last-Modified

响应头,资源最近修改时间,由服务器告诉浏览器。

  • Etag

响应头,资源标识,由服务器告诉浏览器。

  • If-None-Match

请求头,缓存资源标识,由浏览器告诉服务器。

配对使用的字段:

  • Last-Modified 和 If-Modified-Since
  • Etag 和 If-None-Match

几种缓存header的使用方式

  1. 服务器和浏览器约定资源过期时间

服务器和浏览器约定资源过期时间,用 Expires 字段来控制,时间是 GMT 格式的标准时间,如 Tue, 12 Jan 2021 11:32:54 GMT。

浏览器第一次请求服务器获取资源,服务器返回Expires字段告知浏览器只要没超过这个时间就不要再发起请求,那么后续浏览器在请求该资源时不会再发起http请求,而是直接使用缓存,直到Expires过期,浏览器此时不会使用缓存而是请求服务器,服务器返回资源和最新的Expires。

缺点:

如果缓存未过期,但是资源已发生变化。这时浏览器还一直使用缓存;Expirse在浏览器端可以任意修改。

  1. 服务器告诉浏览器资源上次修改时间

浏览器第一次请求服务器获取资源,服务器返回Last-Modified,也就是文件最近修改的时间。等下次浏览器再获取资源时会将上次记录的Last-Modified的值赋值给If-Modified-Since。用于服务器判断文件是否修改过。如果修改过则返回最新资源和新的Last-Modified,如果未修改过则返回304。告知浏览器资源未过期你直接可以使用

缺点:

极端情况下会出现服务器1S之内修改过文件(在浏览器请求服务器的前半秒修改过文件,则在后半秒浏览器携带If-Modified-Since请求),文件是否过期出现时间上误差。

  1. 增加文件内容对比,引入Etag
  • 浏览器第一次请求服务器获取资源时,服务器会对资源生成一个标识符并返回在响应头Etag中并将Expirse和max-age以及Last-Modified一并返回,浏览器存储返回响应头的值。
  • 下次再请求时如果缓存未过期,则浏览器会使用缓存。(max-age优先级比Expirse高,max-age指的是相对过期时间,比如max-age=10。再过10S就过期了)。
  • 如果缓存过期会将存储的Etag放在If-None-Match请求头中,将Last-Modified放在If-Modified-Since请求头中。服务器收到后发现Etag和If-Modified-Since都有值,会处理Etag,忽略If-Modified-Since。毕竟Etag更准确。此时对比当前资源的Etag和浏览器发过来的Etag是否相等,相等则表示资源未发生变化,服务器返回304。不相等,服务器返回200,并返回最新的资源和该资源的Etag。

md5/hash缓存

通过不缓存html,为静态文件添加MD5或者hash标识,解决浏览器无法跳过缓存过期时间主动感知文件变化的问题。

为什么这么做?实现原理是什么? 服务器与浏览器的文件修改时间对比,文件内容标识对比(Etag),前提基础都是建立在两者文件路径完全相同的情况下。 module/js/a-hash1.js与module/js/a-hash2.js是两个完全不同的文件,假想浏览器第一次加载页面,请求并缓存了module/js/a-hash1.js,第二次加载,文件指向变成了module/js/a-hash2.js,浏览器会直接重新请求a-hash2.js,因为这就是两个完全不同的文件,哪里还有什么http缓存文件对比,通过这种做法,我们就可以从根本上解决过期时间没到浏览器无法主动请求服务器的问题。因此我们只需要在项目每次发布迭代将修改过的静态文件添加不同的MD5或hash标识就好啦。

webpack提供了webpack-md5-hash插件,可以帮助开发者在项目发布时自动修改文件标识。

1.2 HTTPS

HTTPS介绍

HTTPS并非是应用层的一种新协议。只是HTTP通讯接口部分用SSL和TLS协议代替。通常,HTTP直接和TCP通讯。当使用SSL时,则演变成先和SSL通信,再由SSL和TCP通信了。简言之,所以HTTPS,其实就是身披SSL协议这层外壳的HTTP。

HTTP存在的一些安全隐患

  • 通讯使用明文(不加密),内容可能会被窃听
  • 不验证通讯方的身份,因此有可能遭遇伪装
  • 无法证明明文的完整性,所以有可能已遭篡改

一般防止数据被窃听我们会将数据进行加密,加密方式分为两种,对称性加密和非对称加密。

  1. 对称性加密:客户端和服务器端使用相同的秘钥进行加解密。优点加解密效率高,缺点是只要获取任意一方秘钥就能窃听内容。典型加密算法(AES DES)
  2. 非对称性加密:客户端使用公钥服务器端使用密钥,公钥加密的数据只能被私钥解密,反过来也成立。优点是安全性比对称性加密高,缺点加解密效率低。典型加密算法(RSA)

看似非对称性加密可以解决数据被窃听风险,但是浏览器如和服务器端怎样协商使用什么密钥呢?在下文中 HTTPS如何保证安全 会对其进行说明。

HTTPS如何保证安全

这时候就需要证书机构的协助了。服务器端将公钥以及自己的网站信息发给证书机构,证书机构则用它的私钥对其信息签名然后制作成证书然后颁发给服务器端。其中证书中包含服务器的host和证书有效期以及服务器公钥的等。浏览器请求服务器,这时服务器会将证书发送给浏览器,浏览器会根据系统已有的证书的公钥对其返回的证书签名进行验证。如果验证通过说明该证书是合法有效的,在合法有效的基础上可以对证书中的host进行校验,校验通过则说明该证书就能证明该服务器就是你想要访问的服务器。从而解决了 “不验证通讯方的身份,因此有可能遭遇伪装” 问题。 在已确认对方身份的基础上就可以获取证书中的公钥来对后续发送的数据进行加密,而服务器端使用私钥进行解密。从而解决了 “浏览器如和服务器端怎样协商使用什么密钥” 问题。在SSL的建立过程中使用证书公钥和服务器端私钥协商数据的加密密钥,即用非对称性加密来协商对称性加密的密钥,SSL建立完成后的通讯数据都由对称性密钥来加解密。来从而保证了 “通讯使用明文(不加密),内容可能会被窃听” 问题。 数据遭到篡改是在SSL建立完成后的通讯数据遭到篡改。而SSL的建立成功保证了该次连接是安全可靠的。所以SSL的连接成功解决了 “无法证明明文的完整性,所以有可能已遭篡改” 问题。

综上所述就是对HTTPS如何保证安全做了简单介绍。

1.3 一次完整的HTTP求过程

当我们在web浏览器的地址栏中输入:www.baidu.com,具体发生了什么?

  1. 对www.baidu.com这个网址进行DNS域名解析,得到对应的IP地址
  2. 根据这个IP,找到对应的服务器,发起TCP的三次握手
  3. 建立TCP连接后发起HTTP请求
  4. 服务器响应HTTP请求,浏览器得到html代码
  5. 浏览器解析html代码,并请求html代码中的资源(如js、css、图片等)(先得到html代码,才能去找这些资源)
  6. 浏览器对页面进行渲染呈现给用户
  7. 服务器关闭关闭TCP连接

2.相关设计模式

责任链

责任链,顾名思义,就是用来处理相关事务的一条执行链,执行链上有多个节点,每个节点都有机会(条件匹配)处理请求事务,如果某个节点处理完了就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。

工厂模式

“我们不生产水,我们只是大自然的搬运工”

  1. 简单工厂

我们在创建对象时不会对客户端直接暴露创建逻辑,而是 通过使用一个共同的接口根据不同的条件来指向具体想要创建的对象。

  1. 工厂方法

定义工厂父类负责定义创建对象的公共接口,而子类则负责生成具体的对象。

将类的实例化(具体产品的创建)延迟到工厂类的子类(具体工厂)中完成,即由子类来决定应该实例化(创建)哪一个类,相比简单工厂更符合开闭原则。

  1. 抽象工厂 详细介绍

是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。比如手机和耳机属于多个等级的产品,生产手机和耳机的公司有华为和小米。如要增加另一个公司来生产这些产品,则不需要修改原代码,满足开闭原则。当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。当系统中只存在一个等级结构的产品时,抽象工厂模式将退化到工厂方法模式

build模式

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

比如定义一个手机类,创建一个手机需要传入手机颜色,大小,android版本,预装软件等等。客户端可以根据不同参数构建不同的手机对象。体现了定义中的“同样的构建过程可以创建不同的表示”

3.Okhttp特点

  • 支持HTTP/2,允许对同一主机的所有请求共享一个套接字。
  • 连接池减少了请求延迟(如果HTTP/2不可用)。
  • 透明GZIP缩小数据传输大小。
  • 响应缓存完全避免网络重复请求。

4.请求响应流程

请求响应流程分为异步和同步,这里先看异步流程。

  • 异步请求
private final OkHttpClient client = new OkHttpClient();
  public void run() throws Exception {
    Request request = new Request.Builder()
        .url("http://publicobject.com/helloworld.txt")
        .build();
    client.newCall(request).enqueue(new Callback() {
      @Override public void onFailure(Call call, IOException e) {
        //请求失败
      }
      @Override public void onResponse(Call call, Response response) throws IOException {
        if (!response.isSuccessful()) {
           //请求成功
            ...
        }else{
           //请求失败
           ...
        }
      }
    });
  }
复制代码

最主要的还是client.newCall(request).enqueue(new Callback())还是这一行代码。

newCall最终返回的是RealCall,那么其实就是调用RealCall#enqueue()。

RealCall创建过程

public Call newCall(Request request) {
    return new RealCall(this, request, false);
}

RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    ...
    this.client = client;
    this.originalRequest = originalRequest;
    //创建重定向和请求失败后的重试拦截器
    this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
  }

复制代码

RealCall#enqueue()过程

public void enqueue(Callback responseCallback) {
    ...
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
复制代码

client.dispatcher()返回的是Dispatcher,所以代码调用Dispatcher#enqueue(new AsyncCall(responseCallback)), AsyncCall是一个Runnable。

Dispatcher中有三个重要的请求队列。

//异步请求等待队列
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
//正在运行的异步请求队列
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
//正在运行同步请求队列
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
复制代码

Dispatcher#enqueue

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

其中maxRequests默认为64,表示最大请求数量;maxRequestPerhost默认为5,表示同一主机最大请求数量。即同一主机最大请求数不超过5,不同主机最大请求数不超过64。则放到正在运行的异步请求队列,超过则放到异步请求等待队列中。等请求完成后从正在运行的异步请求队列中删除本次请求,从等待队列中取出等待的请求放入到异步请求队列中并执行。

AsyncCall执行逻辑

protected void execute() {
  try {
    Response response = getResponseWithInterceptorChain();
    if (retryAndFollowUpInterceptor.isCanceled()) {
      responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
    } else {
      responseCallback.onResponse(RealCall.this, response);
    }
  } catch (IOException e) {
    ...
    responseCallback.onFailure(RealCall.this, e);
  } finally {
    //执行队列中的请求移动。从runningAsyncCalls删除并从readyAsyncCalls中获取请求并添加到runningAsyncCalls中然后执行。
    client.dispatcher().finished(this);
  }
}
复制代码

从getResponseWithInterceptorChain()返回值可以看出该方法是获取响应的实现。

getResponseWithInterceptorChain()实现

  Response getResponseWithInterceptorChain() throws IOException {
    List<Interceptor> interceptors = new ArrayList<>();
    //OkhttpClient配置阶段自定义的拦截器
    interceptors.addAll(client.interceptors());
    //创建RealCall时创建的从定向以及连接重试拦截器
    interceptors.add(retryAndFollowUpInterceptor);
    //附加请求头的拦截器以及解压缩gzip拦截器
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    //缓存拦截器
    interceptors.add(new CacheInterceptor(client.internalCache()));
    //创建连接拦截器
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      //OkhttpClient配置阶段自定义的NetworkInterceptor
      interceptors.addAll(client.networkInterceptors());
    }
    //执行请求的拦截器
    interceptors.add(new CallServerInterceptor(forWebSocket));
    //将每个拦截器串成一条链,并依次执行。
    Interceptor.Chain chain = new RealInterceptorChain(
        interceptors, null, null, null, 0, originalRequest);
    return chain.proceed(originalRequest);
  }
  
复制代码

整个执行过程时序图

5.缓存机制

5.1 如何自定义缓存

  1. 配置OkHttpClient阶段设置缓存文件
 File cacheFile = new File(CommonApplication.getApplication().getCacheDir(), "http_cache");
        Cache cache = new Cache(cacheFile, 10L * 1024L * 1024L);
 okHttpClient = new OkHttpClient.Builder()
                .cache(cache)
                ...
                .build();
复制代码
  1. 在使用时指定本次Request缓存策略
Request request = new Request.Builder()
                        .cacheControl(CacheControl.FORCE_NETWORK)
                        .url("http://publicobject.com/helloworld.txt")
                        .build();
execute(request);


private void execute(final Request request) {
    new Thread() {
        @Override
        public void run() {
            try {
                Response response = OkManager.getInstance().getClient().newCall(request).execute();
                if (response.cacheResponse() != null) {
                    //表示从缓存中返回
                    Log.d(TAG, "cacheResponse:" + response.cacheResponse().toString());
                } else {
                    Log.d(TAG, "cacheResponse: null");
                }
                if (response.networkResponse() != null) {
                    //表示从网络中返回
                    Log.d(TAG, "networkResponse:" + response.networkResponse().toString());
                } else {
                    Log.d(TAG, "networkResponse: null");
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }.start();
}
复制代码
  • CacheControl.FORCE_NETWORK 表示不使用过期缓存,其原理是在Cache-Control中设置no-cache。
  • CacheControl.FORCE_CACHE 表示只使用缓存,不管缓存是否过期,只要有客户端都接受。如果不存在缓存,则返回504。其原理是在Cache-Control中设置only-if-cached和max-stale=Integer.MAX_VALUE
  • 如果缓存未过期使用缓存,过期后从网络获取。CacheControl还定义其他Cache-Control指令,比如max-age,max-stale,min-fresh等等,可以自由搭配使用。另外CacheControl中对于Cache-Control指令定义完全符合RFC2616,本文在http缓存这一小节中已对Cache-Control其他指令已进行说明。

5.2 缓存的命中与写入

在Okhttp请求响应基本流程中分析得知缓存的命中和写入处理在CacheInterceptor中实现。

5.2.1 缓存的命中

CacheInterceptor#intercept

 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);
    }
    
    ...

    //如果缓存未命中并且请求时设置了强制使用缓存(only-if-cached),则返回504
    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(Util.EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();
    }

    //缓存命中则返回缓存
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }
}
复制代码

5.2.2 缓存的写入

CacheInterceptor#intercept

  @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;
    ...省略缓存命中的代码
    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 (cacheResponse != null) {
      //如果响应状态码为304,说明缓存未过期,更新本地缓存除了body以外的其他值,并返回缓存。
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
             //合并headers
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
             ...
            .build();
        networkResponse.body().close();
        ...
        //更新除了body以外的其他数据
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }
    
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();
    //如果配置了存储缓存的文件    
    if (cache != null) {
      //如果响应中有body并且可以被客户端缓存
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        //将从网络获取的响应写入到缓存中
        //这里分为两步
        //1.先写除了body外的其他数据
        CacheRequest cacheRequest = cache.put(response);
        //2.将body的写入缓存与外部从response中读取数据绑定在一起。也就是客户端读取多少body中的数据,缓存就写入多少body中的数据。如果客户端读完,那么缓存数据也写完。如果客户端读的过程中出现异常,则缓存就写入一条失败记录。
        return cacheWritingResponse(cacheRequest, response);
      }
      //如果不是GET请求
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        //如果本地存在缓存,则删除它
        cache.remove(networkRequest);
      }
    }
    return response;
  }
复制代码

5.2.3 缓存机制流程图

Okhttp缓存机制

6.建立连接

在Okhttp中,与服务器连接的创建与建立在ConnectInterceptor中实现,连接建立执行流如下: image

6.1 连接建立的相关类介绍

  • Address

与服务器的连接规范。包含要连接的服务器的主机名和端口。如果请求设置代理也包括代理信息。对于连接安全的地址还包括SSL套接字工厂、主机名验证器和证书。其中相同的Address可以共享连接。

  • RouteSelector

连接服务器的选择路由,此类的作用是通过Dns域名查找ip地址集,将按顺序从ip地址集中取出ip进行连接,如果本次ip连接失败,将重试与下一个ip进行连接,直到 连接已建立或者ip地址集已用完。所以它内部提供一个next()用于返回ip集中可用的ip地址并将ip地址和端口以及Address封装到Route中返回。

  • Route

封装了了Address以及ip和端口,用于创建创建连接时使用。

  • RealConnection

真正发起连接的类,内部实现了TCP连接和TLS的建立。

  • ConnectionPool

连接池,管理HTTP和HTTP/2连接的重用,以减少网络延迟。相同的Address可以共享一个连接,也就是RealConnection对象。该类中定义最大支持5个空闲连接连以及每个连接的保持5分钟时间,超过则会清理(要么清理超过了连接的保持时间,要么超过了空闲连接的数)。

  • StreamAllocation

这个类协调三个实体之间的关系:请求、连接、流。HTTP通信执行网络"请求"需要在"连接"上建立一个新的"流",所以StreamAllocation将管理并协调它们。它负责为一次"请求"寻找"连接"并建立"流",从而完成HTTP通信。

  • HttpCodec

Http1Codec和Http2Codec的顶层接口,主要是HTTP请求编码和响应解码操作。从Request中编码成流通过Socket传输到服务器,从服务器响应中流中解码成Response返回给客户端。

6.2 连接的复用和清理机制

连接的复用和清理机制定义在ConnectionPool中。

6.2.1 连接的复用

private final Deque<RealConnection> connections = new ArrayDeque<>();

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

只要连接池中有保存的连接就会遍历connections,当isEligible()返回true时就会复用。

isEligible()中定义如下:

  1. 如果这个连接已超最大并发流或者在该连接上不能创建新的流则不能复用(HTTP1.1 中一个连接可支持最大并发流为1)
  2. 如果该连接上存储的Address与请求的Address不同,则不能复用(比较的是两个Address,包含域名,dns,协议,代理等等)
  3. 如果是HTTP2,如果该连接上存储的ip与请求的ip不同则不能复用;证书和域名不匹配,也不能复用。
  4. 其他情况则可以复用

6.2.2 连接的清理

连接的清理任务被放到一个线程中执行,在连接池为空,往连接池中添加连接时开启。

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

cleanupRunning被赋值一次,往后都不会执行cleanupRunnable。连接池中没有连接时会重置cleanupRunning状态并且会退出清理任务,直到下次put时再次开启。

private final Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
  while (true) {
    //计算需要等待多久再检查连接池中连接是否需要清理
    long waitNanos = cleanup(System.nanoTime());
    //连接池中没有连接会退出清理任务,下次put时会开启
    if (waitNanos == -1) return;
    if (waitNanos > 0) {
      long waitMillis = waitNanos / 1000000L;
      waitNanos -= (waitMillis * 1000000L);
      synchronized (ConnectionPool.this) {
        try {
          //等待waitMillis毫秒后再次检查连接是否需要清理
          ConnectionPool.this.wait(waitMillis, (int) waitNanos);
        } catch (InterruptedException ignored) {
        }
      }
    }
  }
}
};
复制代码

cleanup()中实现如下

  • 如果连接正在使用则等待5分钟后再次检查
  • 如果连接空闲,找出连接池中最长空闲时间的连接,如果该连接空闲时间超过5分钟则从连接池中删除并关闭该连接;如果该连接空闲时间未超过5分钟,则计算下次需要清理的等待时间。
  • 如果连接池中存储的空闲连接数量超过指定的最大空闲连接数,则删除连接池中最长空闲时间的连接并关闭该连接

那么整个清理任务可以总结如下:

清理任务就是异步执行的,遵循两个指标,最大空闲连接数量和最大空闲时长,满足其一则清理空闲时长最大的那个连接,然后循环执行,要么等待一段时间,要么继续清理下一个连接,直到清理所有连接,清理任务才结束,下一次put的时候,如果已经停止的清理任务则会被再次触发。

7.addInterceptor和addNetworkInterceptor区别

从源码上addInterceptor添加的拦截器位于整个拦截链的头部,最先执行的拦截器;addNetworkInterceptor添加的拦截器位于ConnectInterceptor和CallServerInterceptor之间,连接建立后执行的拦截器以及解码后第一个拿到响应的拦截器。

这就导致他们的作用不一样。

addInterceptor 添加的拦截器

  1. 不需要担心中间过程的响应,如重定向和重试
  2. 总是只调用一次,即使HTTP响应是从缓存中获取
  3. 观察应用程序的初衷. 不关心OkHttp注入的头信息如: If-None-Match
  4. 允许短路而不调用 Chain.proceed(),即中止调用
  5. 允许重试,使 Chain.proceed()调用多次

addNetworkInterceptor 添加的拦截器

  1. 可以操作中间过程的响应,如重定向和重试。
  2. 当网络短路而返回缓存响应时不被调用
  3. 只观察在网络上传输的数据

8.如何实现抓包和模拟弱网环境功能

由于OkHttp拦截器的特性,所以很容易干涉从请求到响应的过程。又由于抓包这个功能我们只关心结果,不关心网络请求的中间过程(比如重定向,重试)所以抓包使用的是addInterceptor添加的拦截器;而模拟弱网环境,使用的是addNetworkInterceptor添加的拦截器。

8.1 抓包

实现思路:自定义一个拦截器实现Interceptor,在intercept中获得Request和Response并存储,最后将该拦截器使用addInterceptor添加到拦截器链中。

实现代码:

NetworkCaptureSelf

DoraemonKit

8.2 模拟弱网环境

实现思路: 自定义一个拦截器实现Interceptor,在intercept中对不同类型进行处理,并返回Response

  1. 超时
/**
 * 模拟超时
 *
 * @param chain url
 */
public Response simulateTimeOut(Interceptor.Chain chain) throws IOException {
    SystemClock.sleep(mTimeOutMillis);
    final Response response = chain.proceed(chain.request());
    ResponseBody responseBody = ResponseBody.create(response.body().contentType(), "");
    Response newResponse = response.newBuilder()
            .code(400)
            .message(String.format("failed to connect to %s  after %dms", chain.request().url().host(), mTimeOutMillis))
            .body(responseBody)
            .build();
    return newResponse;
}
复制代码
  1. 断网
/**
 * 模拟断网
 */
public Response simulateOffNetwork(Interceptor.Chain chain) throws IOException {
    final Response response = chain.proceed(chain.request());
    ResponseBody responseBody = ResponseBody.create(response.body().contentType(), "");
    Response newResponse = response.newBuilder()
            .code(400)
            .message(String.format("Unable to resolve host %s: No address associated with hostname", chain.request().url().host()))
            .body(responseBody)
            .build();
    return newResponse;
}
复制代码
  1. 限速

上传限速:上传限速即请求内容的写入限速,也就是写入请求内容的过程做限速。

下载限速:下载限速即响应内容对读取限速,也就是将读取响应内容的过程做限速。

实现思路:包装RequestBody和ResponseBody,对写入和读取时操作的数量做下处理,本来每次写入8192L,改为每次写入设置的字节数,读取同理。

实现代码: DoraemonKit

参考链接

文章分类
Android
文章标签