美文网首页
开源框架 | OkHttp 请求流程源码解析

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

作者: 南子李 | 来源:发表于2020-09-01 15:52 被阅读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 总结:
    • 不同:同步请求是在当前线程执行,而异步请求会在线程池中使用子线程执行;
    • 相同:相同的是同步请求和异步请求都会调用 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;

    参考

    相关文章

      网友评论

          本文标题:开源框架 | OkHttp 请求流程源码解析

          本文链接:https://www.haomeiwen.com/subject/wrassktx.html