开源框架 | OkHttp 请求流程源码解析

2020-09-01  本文已影响0人  南子李

1. 基本使用

1.1 创建 OkHttpClient

首先创建 OkHttpClient 用于配置网络请求时连接时长,读/写数据时长,缓存路径等参数信息:

        OkHttpClient mOkHttpClient;
        OkHttpClient.Builder builder = new OkHttpClient.Builder()
                .connectTimeout(15, TimeUnit.SECONDS) //连接最大时长
                .writeTimeout(20, TimeUnit.SECONDS) //客户端写数据最大时长
                .readTimeout(20, TimeUnit.SECONDS) //服务端读数据最大时长
                .cache(new Cache(sdCache.getAbsoluteFile(), cacheSize)); //配置缓存路径即缓存大小限制
        mOkHttpClient = builder.build();
1.2 创建 Request

创建 Request 用于设置连接的地址 url,请求方法(post/get),请求头等信息:

        //get请求
        Request request = new Request.Builder()
                .method("GET",null)
                .url(url)
                .build();

        //post请求
        Request request = new Request.Builder()
                .url(url)
                .post(requestBody)
                .build();
1.3 创建 Call

通过 OkHttpClient 和 Request 创建 Call 用于处理请求的回调:

        Call call = mOkHttpClient.newCall(request);
1.4 发起请求
        try {
            final Response execute = call.execute();
        } catch (IOException e) {
            e.printStackTrace();
        }
        call.enqueue(new Callback() {
            @Override
            public void onFailure(@NotNull Call call, @NotNull IOException e) {
                    //处理请求失败
            }

            @Override
            public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
                  //处理请求成功
            }
        });

2. 源码流程分析

OkHttp请求流程
2.1 同步请求

同步请求直接执行 RealCall 的 execute() 方法,然后调用调度器 Dispatcher 的 execute() 方法将当前同步请求加入同步请求队列 runningSyncCalls 中,接着调用 getResponseWithInterceptorChain() 方法进行拦截器的链式调用。

2.2 异步请求

异步请求会执行 RealCall 的 enqueue() 方法,然后通过调度器 Dispatcher 调度异步请求,调度器中有三个队列 readyAsyncCalls、runningAsyncCalls、runningSyncCalls 分别用于存储将要执行的异步请求、正在执行的异步请求以及正在执行的同步请求,如果当前正在执行的最大请求数小于最大请求数 maxRequests(默认为64)且未达到同一个主机名的最大请求数 maxRequestsPerHost(默认为5)则将当前异步请求加入正在执行的异步请求队列 runningAsyncCalls 中,然后通过线程池执行当前异步请求。

异步请求执行的是 RealCall 的内部类 AsyncCall 的 run() 方法,该方法中调用 getResponseWithInterceptorChain() 方法进行拦截器的链式调用。

2.3 总结:

3. 拦截器

3.1 RetryAndFollowUpInterceptor

实现失败重连和重定向的请求:


RetryAndFollowUpInterceptor拦截流程.png
3.2 BridgeInterceptor

将用户构造的请求转换为发送到服务器的请求,把服务器返回的响应转换为用户友好的请求,是从程序代码到网络代码的桥梁,主要实现了请 求头 header 的封装和响应内容的解压:

  1. 设置请求内容类型: Content-Type、内容长度:Content-Length以及请求内容编码格式:Transfer-Encoding;
  2. 设置 Host、User-Agent ,设置 Connection 为 Keep-Alive;
  3. 添加 Cookie;
  4. 设置接受内容编码格式为 gzip,并在接受到响应内容后进行解压,省去了用户处理数据的麻烦;
3.3 CacheInterceptor

OkHttp 的缓存使用的是 DiskLruCache,在 CacheInterceptor 的拦截方法 intercept() 中如果在 OkHttpClient 中配置了缓存,首先会从磁盘中获取当前请求的缓存 Cache;

  @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 && cacheResponse == null) {
      return new Response.Builder()
          .request(chain.request())
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          ...
          .build();
    }

    // 不使用网络请求,使用缓存,直接返回缓存
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }
    //开始网络请求
    Response networkResponse = null;
    try {
      networkResponse = chain.proceed(networkRequest);
    } finally {
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }

    // 网络请求和缓存都使用,返回的请求码为 304,表示重定向
    if (cacheResponse != null) {
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        //根据 cacheResponse 构建新的响应,将 networkResponse 合并进来
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            ...
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();

        // 更新缓存 Cache
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response; //返回由 cacheResponse 和 networkResponse 合并的响应
      } else {
        closeQuietly(cacheResponse.body());
      }
    }
    //不使用缓存,根据 networkResponse 构建响应,将 cacheResponse 合并进来
    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    if (cache != null) { //磁盘中有该请求的缓存文件
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // 存入缓存 Cache
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
      //请求方法不可使用缓存
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest); //移除缓存
        } catch (IOException ignored) {
        }
      }
    }
    return response;
  }
  1. 根据当前时间、Request、从磁盘中获取的缓存, 创建一个缓存策略 CacheStrategy

缓存策略包含 networkRequest 和 cacheResponse 两个变量,可用来判断本次请求的响应内容是由网络请求返回还是使用缓存内容,还是两者都使用,networkRequest 为空表示不使用网络请求,cacheResponse 为空表示不使用缓存。

  1. 缓存策略中不使用网络请求也不使用缓存,创建一个包含异常信息的 Response并返回,注意返回码为 504
  2. 不使用网络请求,但使用缓存直接返回缓存;
  3. 需要使用网络请求,调用 chain.proceed() 执行后续拦截器进行网络请求,如果缓存策略中有 cacheResponse 且网络请求返回码为 304(表明是重定向请求),使用缓存 cacheResponse 构建 Response,更新缓存 Cache 并返回 Response;
  4. cacheResponse 为空,即不使用缓存,构建网络请求的 Response;
  5. 该请求在磁盘上有缓存即 Cache 不为空,把这个 Response 写入缓存并返回。
3.4 ConnectInterceptor

建立客户端和服务器之间的连接,为客户端和服务器之间的通信做准备。

  public ConnectionPool() {
    this(5, 5, TimeUnit.MINUTES);
  }
  public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {...}

构造方法中指定了最大空闲连接数 maxIdleConnections 默认为 5,以及连接的最大存活时长 keepAliveDurationNs 默认为 5 分钟,连接池中连接的清理工作交给了线程池去处理;

  1. put():存入一个新的连接,存入连接之前会通过线程池清理掉不必要的连接;
  void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }
  1. cleanup():清理不必要的连接,这里的不必要是指超出了最大空闲连接数 maxIdleConnections 或者超出了连接存活时长 keepAliveDurationNs 的连接;
  long cleanup(long now) {
    int inUseConnectionCount = 0;
    int idleConnectionCount = 0;
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

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

        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++; //记录正在使用的连接数
          continue; //结束本次循环操作
        }

        idleConnectionCount++; //记录空闲连接数
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) { //记录空闲时长最长的连接
          longestIdleDurationNs = idleDurationNs; 
          longestIdleConnection = connection; 
        }
      }
        //拿到的这条连接空闲时长大于了连接默认存活的最长时间或者空闲连接数大于了最大空闲连接数
      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {
        connections.remove(longestIdleConnection); //从连接队列中清除这条连接
      } else if (idleConnectionCount > 0) { //空闲连接还没达到最大存活时长,等待一段时间后再清除
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {//所有连接都在使用中,等待5分钟后再处理清除操作
        return keepAliveDurationNs;
      } else {//连接队列中没有连接,不需要清理
        cleanupRunning = false;
        return -1;
      }
    }
    closeQuietly(longestIdleConnection.socket());
    // Cleanup again immediately.
    return 0;
  }

a. 遍历连接队列,记录空闲连接数,找到连接队列中空闲时长最长的连接;
b. 如果空闲连接数大于了 maxIdleConnections 或者连接存活时间大于了 keepAliveDurationNs,从连接队列中移除这个空闲连接;
c. 有空闲连接但没必要清除,等待一段时间(达到最大存活时间)再清除;
d. 所有连接都在使用中,5分钟 后再清理;

  1. get():从连接队列中返回一个 Adress的主机名 一致或者 Route 匹配的连接,没有则返回为null;
3.5 CallServerInterceptor

通过 HttpCodec 实现客户端和服务器之间的通信:

  1. writeRequestHeaders() 向服务器发送 Request 的 header;
  2. 如果有 body 通过 createRequestBody(),向服务器发送 body;
  3. readResponseHeaders(),读取服务器返回的 header 并构造一个新的 Response,构造 Response 时断开客户端和服务器的连接;
  4. 如果服务器返回的 Response 中有 body,通过 openResponseBody() 读取返回的 body,在步骤3 的 Response 基础上加上这里的 body 并构建一个新的 Response 。

4. 总结:

OkHttp 具有以下优势:
  1. 失败自动重连:在 RetryAndFollowUpInterceptor 失败重连重定向拦截器中,连接失败时会自动尝试重新连接,也可以处理访问的重定向;
  2. 可以解压编码类型为 gzip 的响应:在 BridgeInterceptor 桥接拦截器中默认支持解压编码类型为 gzip 的响应;
  3. 支持缓存:在 CacheInterceptor 缓存拦截器中,使用缓存避免频繁的重复请求;
  4. 连接可复用:在 ConnectionPool 中实现连接复用,避免频繁创建和断开连接;
  5. OkHttp 使用 Socket 发送请求:Socket 由 RealConnection 维护;
  6. 同主机名的请求共享一个 Socket:参考第4条,同主机名的请求共享同一条连接,同一条连接及共享同一个 Socket;

参考

上一篇 下一篇

猜你喜欢

热点阅读