IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> OKhttp源码(3.10)流程梳理 -> 正文阅读

[网络协议]OKhttp源码(3.10)流程梳理

使用流程

        OkHttpClient okHttpClient = new OkHttpClient();
        //OkHttpClient okHttpClient1 = new OkHttpClient.Builder().build();
        Request request = new Request.Builder()
                .url("")
                .build();
        Call call = okHttpClient.newCall(request);
        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {

            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {

            }
        });

在这里插入图片描述

在使用OkHttp发起一次请求时,对于使用者最少存在 OkHttpClient 、 Request 与 Call 三个角色。其中
OkHttpClient 和 Request 的创建可以使用它为我们提供的 Builder (建造者模式)。而 Call 则是把 Request 交 给 OkHttpClient 之后返回的一个已准备好执行的请求。

建造者模式:将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。实例化
OKHttpClient和Request的时候,因为有太多的属性需要设置,而且开发者的需求组合千变万化,使用建造
者模式可以让用户不需要关心这个类的内部细节,配置好后,建造者会帮助我们按部就班的初始化表示对象

同时OkHttp在设计时采用的门面模式,将整个系统的复杂性给隐藏起来,将子系统接口通过一个客户端OkHttpClient统一暴露出来。OkHttpClient 中全是一些配置,比如代理的配置、ssl证书的配置等。而 Call 本身是一个接口,我们获得的实现为: RealCall

看一下这个方法:okHttpClient.newCall(request);

  @Override public Call newCall(Request request) {
    return RealCall.newRealCall(this, request, false /* for web socket */);
  }

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

Call 的 execute 代表了同步请求,而 enqueue 则代表异步请求。两者唯一区别在于一个会直接发起网络请求,而另一个使用OkHttp内置的线程池来进行。这就涉及到OkHttp的任务分发器。
得到call 之后,开始执行

// 异步请求
call.enqueue(new Callback() {...}

// RealCall 是 Call的实现类,直接查看RealCall的 enqueue 方法
  @Override 
  public void enqueue(Callback responseCallback) {
    synchronized (this) {
    // 同一个请求 只能执行一次,否则会报异常
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    captureCallStackTrace();
    eventListener.callStart(this);
    // 得到分发器,去Dispatcher 执行enqueue方法
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }

Dispatcher

public final class Dispatcher {
  private int maxRequests = 64; // 同时存在的最大请求数量
  private int maxRequestsPerHost = 5; // 同一个域名同时存在的最大请求数
  private @Nullable Runnable idleCallback;// 闲时任务,idle,延伸至 IdleHandler,也是闲时机制

  /** Executes calls. Created lazily. */
  private @Nullable ExecutorService executorService;

  //异步请求使用的线程池 
  private @Nullable ExecutorService executorService; 
  //异步请求等待执行队列 
  private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>(); 
  //异步请求正在执行队列 
  private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>(); 
  //同步请求正在执行队列 
  private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

  //  ...

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

  // 同步请求
    synchronized void executed(RealCall call) {
    runningSyncCalls.add(call);
  }

当正在执行的任务未超过最大限制64,同时 runningCallsForHost(call) < maxRequestsPerHost 同一Host的请求不超过5个,则会添加到正在执行队列,同时提交给线程池。否则先加入等待队列。

 // 线程池的创建
  public synchronized ExecutorService executorService() {
    if (executorService == null) {
      executorService = new ThreadPoolExecutor(
      0, // 核心线程数
      Integer.MAX_VALUE,// 最大线程数
      60, //线程存活时间
      TimeUnit.SECONDS,// 线程存活时间单位
      new SynchronousQueue<Runnable>(), // 阻塞队列
      Util.threadFactory("OkHttp Dispatcher", false));// 线程创建工厂
    }
    return executorService;
  }

在OkHttp的分发器中的线程池的创建如上,其实就和 Executors.newCachedThreadPool() 创建的线程一样。

  • 首先核心线程为0,表示线程池不会一直为我们缓存线程,线程池中所有线程都是在60s内没有工作就会被回收。
  • 而最大线程 Integer.MAX_VALUE 与等待队列 SynchronousQueue 的组合能够得到最大的吞吐量。即当需要线程池执行任务时,如果不存在空闲线程不需要等待,马上新建线程执行任务!等待队列的不同指定了线程池的不同排队机制。一般来说,等待队列BlockingQueue 有: ArrayBlockingQueue 、 LinkedBlockingQueue 与 SynchronousQueue 。

SynchronousQueue : 无容量的队列。
使用此队列意味着希望获得最大并发量。因为无论如何,向线程池提交任务,往队列提交任务都会失败。而失败后如果没有空闲的非核心线程,就会检查如果当前线程池中的线程数未达到最大线程,则会新建线程执行新提交的任务。完全没有任何等待,唯一制约它的就是最大线程数的个数。因此一般配合 Integer.MAX_VALUE 就实现了真正的无等待。

executorService().execute(call) 这里得到线程池,然后执行 call,call 即为 AsyncCall

 final class AsyncCall extends NamedRunnable {
    // ...
    // 任务开始
    @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) {
        if (signalledCallback) {
          // Do not signal the callback twice!
          Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
        } else {
          eventListener.callFailed(RealCall.this, e);
          responseCallback.onFailure(RealCall.this, e);
        }
      } finally {
      // 最终都会执行 finished 方法
        client.dispatcher().finished(this);
      }
    }
  }

加入线程池直接执行没啥好说的,但是如果加入等待队列后,就需要等待有空闲名额才开始执行。因此每次执行完一个请求后,都会调用分发器的 finished 方法

// 同步  异步 都会执行到这里
  private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {
    int runningCallsCount;
    Runnable idleCallback;
    synchronized (this) {
    //不管异步还是同步,执行完后都要从队列移除(runningSyncCalls/runningAsyncCalls)
      if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
      if (promoteCalls) promoteCalls();// 异步任务会走这里
      runningCallsCount = runningCallsCount();// 正在执行的同步+异步任务 之和
      idleCallback = this.idleCallback;
    }
// 没有任务执行  则执行闲置任务
    if (runningCallsCount == 0 && idleCallback != null) {
      idleCallback.run();
    }
  }
  public synchronized int runningCallsCount() {
    return runningAsyncCalls.size() + runningSyncCalls.size();
  }

  private void promoteCalls() {
  // 如果正在执行的队列满了,或者等待执行的队列是空的,就直接返回了
    if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.
    if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.

// 执行到这里,说明可以向正在执行的队列里可以添加任务
// 遍历等待执行的队列
    for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
      AsyncCall call = i.next();
      // 判断条件,满足之后,添加到执行队列,并启动线程去执行任务
      if (runningCallsForHost(call) < maxRequestsPerHost) {
        i.remove();// 添加过去之后,要移除等待队列,这个相当于剪切操作,一个任务只能在一个队列里
        runningAsyncCalls.add(call);
        executorService().execute(call);
      }

      if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.
    }
  }

OKhttp 的核心代码

真正的请求操作就是这个方法完成的
Response response = getResponseWithInterceptorChain();

责任链模式

为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。

拦截器流程

在这里插入图片描述

五大拦截器

  1. RetryAndFollowUpInterceptor
    第一个接触到请求,最后接触到响应;负责判断是否需要重新发起整个请求
  2. BridgeInterceptor
    补全请求,并对响应进行额外处理
  3. CacheInterceptor
    请求前查询缓存,获得响应并判断是否需要缓存
  4. ConnectInterceptor
    与服务器完成TCP连接
  5. CallServerInterceptor
    与服务器通信;封装请求数据与解析响应数据(如:HTTP报文)

一、重试及重定向拦截器

第一个拦截器: RetryAndFollowUpInterceptor ,主要就是完成两件事情:重试与重定向。
重试
请求阶段发生了 RouteException 或者 IOException会进行判断是否重新发起请求。

  @Override public Response intercept(Chain chain) throws IOException {
    // ...
     catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
        //  路由异常,连接未成功,请求还没发出去
        if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
        // 如果recover(...) 返回true,则允许重试
          throw e.getLastConnectException();
        }
        releaseConnection = false;
        continue;// 重试
      } 
      // ...
      catch (IOException e) {
        // // 如果recover(...) 返回true,则允许重试
        if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
        releaseConnection = false;
        continue;// 重试
      } 
    }
  private boolean recover(IOException e, StreamAllocation streamAllocation,
      boolean requestSendStarted, Request userRequest) {
    streamAllocation.streamFailed(e);

    // 1、在配置OkhttpClient是设置了不允许重试(默认允许),则一旦发生请求失败就不再重试
    if (!client.retryOnConnectionFailure()) return false;

    //  2、如果是RouteException,不用管这个条件, 
    // 如果是IOException,由于requestSendStarted只在http2的io异常中可能为false,所以主要是第二个条件
    if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody) return false;

    //  3、判断是不是属于重试的异常
    if (!isRecoverable(e, requestSendStarted)) return false;

    //  4、有没有可以用来连接的路由路线
    if (!streamAllocation.hasMoreRoutes()) return false;

    // For failure recovery, use the same route selector with a new connection.
    return true;
  }

所以首先使用者在不禁止重试的前提下,如果出现了某些异常,并且存在更多的路由线路,则会尝试换条线路进行请求的重试。其中某些异常是在 isRecoverable 中进行判断:

  private boolean isRecoverable(IOException e, boolean requestSendStarted) {
    //  出现协议异常,不能重试
    if (e instanceof ProtocolException) {
      return false;
    }

    // 如果不是超时异常,不能重试
    if (e instanceof InterruptedIOException) {
      return e instanceof SocketTimeoutException && !requestSendStarted;
    }

    //  SSL握手异常中,证书出现问题,不能重试
    if (e instanceof SSLHandshakeException) {
      if (e.getCause() instanceof CertificateException) {
        return false;
      }
    }
    // SSL握手未授权异常 不能重试
    if (e instanceof SSLPeerUnverifiedException) {
      return false;
    }
    return true;
  }

1、协议异常,如果是那么直接判定不能重试;(你的请求或者服务器的响应本身就存在问题没有按照http协议来定义数据,再重试也没用)
2、超时异常,可能由于网络波动造成了Socket连接的超时,可以使用不同路线重试。
3、SSL证书异常/SSL验证失败异常,前者是证书验证失败,后者可能就是压根就没证书,或者证书数据不正确,那还怎么重试?经过了异常的判定之后,如果仍然允许进行重试,就会再检查当前有没有可用路由路线来进行连接。简单来说,比如 DNS 对域名解析后可能会返回多个 IP,在一个IP失败后,尝试另一个IP进行重试。

重定向

 Request followUp = followUpRequest(response, streamAllocation.route());

      if (followUp == null) {// 如果是null,就不需要重定向了,
        if (!forWebSocket) {
          streamAllocation.release();
        }
        return response;// 直接返回 response
      }

如果请求结束后没有发生异常并不代表当前获得的响应就是最终需要交给用户的,还需要进一步来判断是否需要重定向的判断。重定向的判断位于 followUpRequest 方法,如果此方法返回空,那就表示不需要再重定向了,直接返回响应;但是如果返回非空,那就要重新请求返回的 Request ,但是需要注意的是,我们的 followup 在拦截器中定义的最大次数为20次。

RetryAndFollowUpInterceptor总结

  • 本拦截器是整个责任链中的第一个,这意味着它会是首次接触到 Request 与最后接收到 Response 的角色,在这个拦截器中主要功能就是判断是否需要重试与重定向。
  • 重试的前提是出现了 RouteException 或者 IOException 。一但在后续的拦截器执行过程中出现这两个异常,就会通过 recover 方法进行判断是否进行连接重试。
  • 重定向发生在重试的判定之后,如果不满足重试的条件,还需要进一步调用followUpRequest 根据 Response 的响应码(当然,如果直接请求失败, Response 都不存在就会抛出异常)。 followup 最大发生20次。

二、桥接拦截器

BridgeInterceptor ,连接应用程序和服务器的桥梁,我们发出的请求将会经过它的处理才能发给服务器,比如设
置请求内容长度,编码,gzip压缩,cookie等,获取响应后保存Cookie等操作。这个拦截器相对比较简单。
补全请求头:

请求头说明
Content-Type请求体类型,如: application/x-www-form-urlencoded
Content-LengthTransfer-Encoding 请求体解析方式
Host请求的主机站点
Connection: Keep-Alive保持长连接
Accept-Encoding: gzip接受响应支持gzip压缩
Cookiecookie身份辨别
User-Agent请求的用户信息,如:操作系统、浏览器等

在补全了请求头后交给下一个拦截器处理,得到响应后,主要干两件事情:
1、保存cookie,在下次请求则会读取对应的数据设置进入请求头,默认的 CookieJar 不提供实现
2、如果使用gzip返回的数据,则使用 GzipSource 包装便于解析。
小结
桥接拦截器的执行逻辑主要就是以下几点

  • 对用户构建的 Request 进行添加或者删除相关头部信息,以转化成能够真正进行网络请求的 Request
  • 将符合网络请求规范的Request交给下一个拦截器处理,并获取 Response 如果响应体经过了GZIP压缩,那就需要解压,再构建成用户可用的 Response 并返回

三、缓存拦截器

在这里插入图片描述
CacheInterceptor ,在发出请求前,判断是否命中缓存。如果命中则可以不请求,直接使用缓存的响应。 (只会存在Get请求的缓存)
步骤为:
1、从缓存中获得对应请求的响应缓存
2、创建 CacheStrategy ,创建时会判断是否能够使用缓存,在 CacheStrategy 中存在两个成员: networkRequest 与 cacheResponse 。他们的组合如下:

networkRequestcacheResponse说明
NullNot Null直接使用缓存
Not NullNull向服务器发起请求
NullNull直接gg,okhttp直接返回504
Not NullNot Null发起请求,若得到响应为304(无修改),则更新缓存响应并返回

3、交给下一个责任链继续处理
4、后续工作,返回304则用缓存的响应;否则使用网络响应并缓存本次响应(只缓存Get请求的响应)
缓存拦截器的工作说起来比较简单,但是具体的实现,需要处理的内容很多。在缓存拦截器中判断是否可以使用缓存,或是请求服务器都是通过 CacheStrategy 判断。

  @Override public Response intercept(Chain chain) throws IOException {
    // 通过url的md5数据 从文件缓存查找 (GET请求才有缓存)
    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 (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();
    }
	// 去发起请求
    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());
      }
    }

    // 服务器返回304无修改,那就使用缓存的响应修改了时间等数据后作为本次请求的响应
    if (cacheResponse != null) {
      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();

        // 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 (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }

      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;
  }

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

判断缓存的命中会使用get() 方法

    public CacheStrategy get() {
      CacheStrategy candidate = getCandidate();
	  //如果可以使用缓存,那networkRequest必定为null;指定了只使用缓存但是networkRequest又不为 null,冲突。那就gg(拦截器返回504)
      if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
        // We're forbidden from using the network and the cache is insufficient.
        return new CacheStrategy(null, null);
      }
      return candidate;
    }

方法中调用 getCandidate() 方法来完成真正的缓存判断

    private CacheStrategy getCandidate() {
      // cacheResponse 是从缓存中找到的响应,
      //如果为null,那就表示没有找到对应的缓存,创建的 CacheStrategy 实例对象只存在 networkRequest ,这代表了需要发起网络请求.
      if (cacheResponse == null) {
        return new CacheStrategy(request, null);
      }
	/* 继续往下走意味着 cacheResponse 必定存在,但是它不一定能用。后续进行有效性的一系列判断*/
      // Drop the cached response if it's missing a required handshake.
      // 如果本次请求是HTTPS,但是缓存中没有对应的握手信息,那么缓存无效,继续发起请求
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }

      // If this response shouldn't have been stored, it should never be used
      // as a response source. This check should be redundant as long as the
      // persistence store is well-behaved and the rules are constant.
      /** 响应码、响应头的判断,都在isCacheable方法里
      综合来看判定优先级如下:
	  1、响应码不为 200, 203, 204, 300, 301, 404, 405, 410, 414, 501, 308,302,307 缓存不可用; 
	  2、当响应码为302或者307时,未包含某些响应头,则缓存不可用; 
	  3、当存在 Cache-Control: no-store 响应头则缓存不可用。
		如果响应缓存可用,进一步再判断缓存有效性
	  */
      if (!isCacheable(cacheResponse, request)) {
        return new CacheStrategy(request, null);
      }

	  /* 走到这一步,OkHttp需要先对用户本次发起的 Request 进行判定,
	  如果用户指定了 Cache-Control: no-cache (不使用缓存)的请求头
	  或者请求头包含 If-Modified-Since 或 If-None-Match (请求验证),那么就不允许使用缓存
	  请求头 
	  Cache-Control: no- cache --》忽略缓存
	  If-Modified-Since: 时间---》值一般为 Data 或 lastModified ,服务器没有在指定的时间后修改请求对应资源,返回304(无修改)
	  If-None-Match:标记 --》值一般为 Etag ,将其与请求对应资源的 Etag 值进行比较;如果匹配,返回304

	 这意味着如果用户请求头中包含了这些内容,那就必须向服务器发起请求。
	 但是需要注意的是,OkHttp并不会缓存304的响应,
	 如果是此种情况,即用户主动要求与服务器发起请求,服务器返回的304(无响应体),则直接把304的响应返回给用户:“既然你主动要求,我就只告知你本次请求结果”。
	 而如果不包含这些请求头,那继续判定缓存有效性
	  */
	  // 用户请求配置
      CacheControl requestCaching = request.cacheControl();
      if (requestCaching.noCache() || hasConditions(request)) {
        return new CacheStrategy(request, null);
      }
	  //资源是否不变
      CacheControl responseCaching = cacheResponse.cacheControl();
      if (responseCaching.immutable()) {
      //如果缓存的响应中包含 Cache-Control: immutable ,
      //这意味着对应请求的响应内容将一直不会改变。此时就可以直接使用缓存。否则继续判断缓存是否可用
        return new CacheStrategy(null, cacheResponse);
      }

	  // 响应的缓存有效期-----↓↓↓↓↓↓↓↓-----
	  // 获得缓存的响应从创建到现在的时间
      long ageMillis = cacheResponseAge();
      // 获取这个响应有效缓存的时长
      long freshMillis = computeFreshnessLifetime();
      if (requestCaching.maxAgeSeconds() != -1) {
      // 如果请求中指定了 max-age 表示指定了能拿的缓存有效时长,就需要综合响应有效缓存时长与请求能拿缓存的 时长,获得最小的能够使用响应缓存的时长
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
      }
	  //  请求包含 Cache-Control:min-fresh=[秒] 能够使用还未过指定时间的缓存 (请求认为的缓存有效时间)
      long minFreshMillis = 0;
      if (requestCaching.minFreshSeconds() != -1) {
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
      }

//Cache-Control:must-revalidate 可缓存但必须再向源服务器进行确认
//Cache-Control:max-stale=[秒] 缓存过期后还能使用指定的时长 如果未指定多少秒,则表示无论过期 多长时间都可以;如果指定了,则只要是指定时间内就能使用缓存
// 前者会忽略后者,所以判断了不必须向服务器确认,再获得请求头中的max-stale
      long maxStaleMillis = 0;
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }
//不需要与服务器验证有效性 && 响应存在的时间+请求认为的缓存有效时间 小于 缓存有效时长+过期后还可以 使用的时间 
// 允许使用缓存
      if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        Response.Builder builder = cacheResponse.newBuilder();
        //如果已过期,但未超过 过期后继续使用时长,那还可以继续使用,只用添加相应的头部字段
        if (ageMillis + minFreshMillis >= freshMillis) {
          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
        }
        //如果缓存已超过一天并且响应中没有设置过期时间也需要添加警告
        long oneDayMillis = 24 * 60 * 60 * 1000L;
        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
          builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
        }
        return new CacheStrategy(null, builder.build());
      }

      // Find a condition to add to the request. If the condition is satisfied, the response body
      // will not be transmitted.
      // 缓存过期处理
      String conditionName;
      String conditionValue;
      if (etag != null) {
        conditionName = "If-None-Match";
        conditionValue = etag;
      } else if (lastModified != null) {
        conditionName = "If-Modified-Since";
        conditionValue = lastModifiedString;
      } else if (servedDate != null) {
        conditionName = "If-Modified-Since";
        conditionValue = servedDateString;
      } else {
        //意味着无法与服务器发起比较,只能重新请求
        return new CacheStrategy(request, null); // No condition! Make a regular request.
      }
	  //添加请求头
      Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
      Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

      Request conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build();
      return new CacheStrategy(conditionalRequest, cacheResponse);
    }

至此,缓存的判定结束,拦截器中只需要判断 CacheStrategy 中 networkRequest 与 cacheResponse 的不同组合就能够判断是否允许使用缓存。
但是需要注意的是,如果用户在创建请求时,配置了 onlyIfCached 这意味着用户这次希望这个请求只从缓存获得,不需要发起请求。那如果生成的 CacheStrategy 存在 networkRequest 这意味着肯定会发起请求,此时出现冲突!那会直接给到拦截器一个既没有 networkRequest 又没有 cacheResponse 的对象。拦截器直接返回用户 504 !
小结
1、如果从缓存获取的 Response 是null,那就需要使用网络请求获取响应;
2、如果是Https请求,但是又丢失了握手信息,那也不能使用缓存,需要进行网络请求;
3、如果判断响应码不能缓存且响应头有 no-store 标识,那就需要进行网络请求;
4、如果请求头有 no-cache 标识或者有 If-Modified-Since/If-None-Match ,那么需要进行网络请求;
5、如果响应头没有 no-cache 标识,且缓存时间没有超过极限时间,那么可以使用缓存,不需要进行网络请求;
6、如果缓存过期了,判断响应头是否设置 Etag/Last-Modified/Date ,没有那就直接使用网络请求否则需要考虑服务器返回304;
并且,只要需要进行网络请求,请求头中就不能包含 only-if-cached ,否则框架直接返回504!

缓存拦截器本身主要逻辑其实都在缓存策略中,拦截器本身逻辑非常简单,如果确定需要发起网络请求,则下一个拦截器为 ConnectInterceptor

四、连接拦截器

ConnectInterceptor ,打开与目标服务器的连接,并执行下一个拦截器;
这个拦截器中的所有实现都是为了获得一份与目标服务器的连接,在这个连接上进行HTTP数据的收发。

五、请求服务器拦截器

CallServerInterceptor ,利用 HttpCodec 发出请求到服务器并且解析生成 Response
在这个拦截器中就是完成HTTP协议报文的封装与解析。

总结

整个OkHttp功能的实现就在这五个默认的拦截器中,所以先理解拦截器模式的工作机制是先决条件。这五个拦截器分别为: 重试拦截器、桥接拦截器、缓存拦截器、连接拦截器、请求服务拦截器。每一个拦截器负责的工作不一样,就好像工厂流水线,最终经过这五道工序,就完成了最终的产品。
但是与流水线不同的是,OkHttp中的拦截器每次发起请求都会在交给下一个拦截器之前干一些事情,在获得了结果之后又干一些事情。整个过程在请求向是顺序的,而响应向则是逆序。
当用户发起一个请求后,会由任务分发起 Dispatcher 将请求包装并交给重试拦截器处理。
1、重试拦截器在交出(交给下一个拦截器)之前,负责判断用户是否取消了请求;在获得了结果之后,会根据响应码判断是否需要重定向,如果满足条件那么就会重启执行所有拦截器。
2、桥接拦截器在交出之前,负责将HTTP协议必备的请求头加入其中(如:Host)并添加一些默认的行为(如:GZIP压缩);在获得了结果后,调用保存cookie接口并解析GZIP数据。
3、缓存拦截器顾名思义,交出之前读取并判断是否使用缓存;获得结果后判断是否缓存。
4、连接拦截器在交出之前,负责找到或者新建一个连接,并获得对应的socket流;在获得结果后不进行额外的处理。
5、请求服务器拦截器进行真正的与服务器的通信,向服务器发送数据,解析读取的响应数据。在经过了这一系列的流程后,就完成了一次HTTP请求!

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-03-21 21:30:49  更:2022-03-21 21:33:20 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 5:26:30-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码