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

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 总结:
  • 不同:同步请求是在当前线程执行,而异步请求会在线程池中使用子线程执行;
  • 相同:相同的是同步请求和异步请求都会调用 getResponseWithInterceptorChain() 方法进行拦截器的链式调用实现发起请求、失败重连、处理缓存、建立网络连接、接受响应等任务。

3. 拦截器

3.1 RetryAndFollowUpInterceptor

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


RetryAndFollowUpInterceptor拦截流程.png
  • 注意
    RetryAndFollowUpInterceptor 之前的拦截器 interceptors,在客户端发起请求后只会被调用一次,而 RetryAndFollowUpInterceptor 之后添加的拦截器,比如 BridgeInterceptor、CacheInterceptor、ConnectInterceptor、networkInterceptor、CallServerInterceptor 在重定向或者重连时都会重复调用,这也是OkHttpClient 中 interceptors 和 networkInterceptor 两类拦截器的区别 。
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

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

  • StreamAllocation
    1. 作用:协调 Connections、Streams、Calls 三个类之间的关系;
    2. newStream() 创建一个新的 Stream;
    3. findConnection():找到一个 RealConnection 用于管理新的 Stream:
      a. 如果已经分配了连接,返回已分配的连接;
      b. 没有分配过连接则从连接池 ConnectionPool 中返回一个可用的连接(这里的可用即和当前连接的 Adress 主机名一致的连接 );
      c. 没有可用的连接,为当前请求创建一个 Route;
      d. 此时有了 Route,第二次从连接池 ConnectionPool 中找到一个匹配的连接并返回(在第一次基础上加上对 Route 的判断);
      e. 如果仍然没有可用的连接,直接新建一个连接 RealConnection
      f . 创建的新连接通过 RealConnection#connect() 执行 TCP+TLS 握手
      g. 将这个新的连接存入连接池中,如果这个新的连接是多路复用(HTTP2.0支持多路复用,即多个请求可共用一个连接)的且和当前连接是连接的同一个地址,释放这个多路复用的连接并返回当前连接;
    4. 回到 newStream() 中,根据 findConnection() 返回的 RealConnection 创建一个新的 HttpCodec 即 HTTP编解码器,用于编码HTTP请求和解码HTTP响应;
  • HttpCodec

    HttpCodec 是一个接口,它有两个实现类 Http1Codec 和 Http2Codec:

    1. Http1Codec:基于 HTTP1.1协议,实现 Socket 连接并通信;
    2. Http2Codec:基于 HTTP2.0协议,实现编码请求和解码响应;
  • RealConnection

    真正实现连接建立的类,调用 connect() 建立连接,下图是连接建立的流程分析,从图中可以看出,OkHttp 是使用 Socket 进行网络通信的,首先判断是否有连接到代理服务器,有则创建代理请求然后连接到代理的服务器,没有则直接连接到原始的服务器;接着根据连接地址 URL 是 http 还是 https,如果是 https 需要建立 tls 连接进行身份验证:


    RealConnection连接建立流程.png
  • http 和 https 的区别:
    https 需要配证书,ssl 层用于验证证书,tls 是 ssl 3.0 以后的版本,可以认为是 ssl 3.1。

  • ConnectionPool
    连接池,通过一个队列 Deque<RealConnection> connections 存储所有的已创建的连接,实现连接的复用,如果多个请求是请求的同一个主机地址,就不需要重复创建连接(三次握手),直接使用连接池中已有的指向同一个主机地址的连接;
  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;

参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,100评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,862评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,993评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,309评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,303评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,421评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,830评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,501评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,689评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,506评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,564评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,286评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,826评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,875评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,114评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,705评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,269评论 2 341