美文网首页Android开发Android开发经验谈Android开发
OkHttpClient源码分析(五)—— ConnectInt

OkHttpClient源码分析(五)—— ConnectInt

作者: chaychan | 来源:发表于2019-01-04 16:05 被阅读5次

    上一篇我们介绍了缓存拦截器CacheInterceptor,本篇将介绍剩下的两个拦截器: ConnectInterceptorCallServerInterceptor

    ConnectInterceptor

    该拦截器主要是负责建立可用的链接,主要作用是打开了与服务器的链接,正式开启了网络请求。
    查看其intercept()方法:

      @Override public Response intercept(Chain chain) throws IOException {
        RealInterceptorChain realChain = (RealInterceptorChain) chain;
        //从拦截器链中获取StreamAllocation对象
        Request request = realChain.request();
        StreamAllocation streamAllocation = realChain.streamAllocation();
        
        //创建HttpCodec对象
        HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
        
        //获取realConnetion
        RealConnection connection = streamAllocation.connection();
    
        //执行下一个拦截器,返回response
        return realChain.proceed(request, streamAllocation, httpCodec, connection);
      }
    

    可以看到intercept中的处理很简单,主要有以下几步操作:

    1. 从拦截器链中获取StreamAllocation对象,在讲解第一个拦截器RetryAndFollowUpInterceptor的时候,我们已经初步了解了StreamAllocation对象,在RetryAndFollowUpInterceptor中仅仅只是创建了StreamAllocation对象,并没有进行使用,到了ConnectInterceptor中,StreamAllocation才被真正使用到,该拦截器的主要功能都交给了StreamAllocation处理;

    2. 执行StreamAllocation对象的 newStream() 方法创建HttpCodec,用于处理编码Request和解码Response;

    3. 接着通过调用StreamAllocation对象的 connection() 方法获取到RealConnection对象,这个RealConnection对象是用来进行实际的网络IO传输的。

    1. 调用拦截器链的proceed()方法,执行下一个拦截器返回response对象。

    上面我们已经了解了ConnectInterceptor拦截器的intercept()方法的整体流程,主要的逻辑是在StreamAllocation对象中,我们先看下它的 newStream() 方法:

     public HttpCodec newStream(
          OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
        ...
        try {
          //创建RealConnection对象
          RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
              writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
          //创建HttpCodec对象
          HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
          
          synchronized (connectionPool) {
            codec = resultCodec;
            //返回HttpCodec对象
            return resultCodec;
          }
        } catch (IOException e) {
          throw new RouteException(e);
        }
      }
    

    newStream()方法中,主要是创建了RealConnection对象(用于进行实际的网络IO传输)和HttpCodec对象(用于处理编码Request和解码Response),并将HttpCodec对象返回。

    findHealthyConnection()方法用于创建RealConnection对象:

     private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
          int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
          throws IOException {
        while (true) {//while循环
          //获取RealConnection对象
          RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
              connectionRetryEnabled);
        
          //同步代码块判断RealConnection对象的successCount是否为0
          synchronized (connectionPool) {
            if (candidate.successCount == 0) {
              //如果为0则返回
              return candidate;
            }
          }
    
          //对链接池中不健康的链接做销毁处理
          if (!candidate.isHealthy(doExtensiveHealthChecks)) {
            noNewStreams();
            continue;
          }
    
          return candidate;
        }
      }
    

    以上代码主要做的事情有:

    1. 开启一个while循环,通过调用findConnection()方法获取RealConnection对象赋值给candidate;
    2. 如果candidate 的successCount 为0,直接返回candidate,while循环结束;
    3. 调用candidate的isHealthy()方法,进行“健康检查”,如果candidate是一个不“健康”的对象,其中不“健康”指的是Socket没有关闭、或者它的输入输出流没有关闭,则对调用noNewStreams()方法进行销毁处理,接着继续循环。

    我们看下findConnection()方法做了哪些操作:

    private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
          boolean connectionRetryEnabled) throws IOException {
        ...
        RealConnection result = null;
        ...
        synchronized (connectionPool) {
          ...
          releasedConnection = this.connection;
          toClose = releaseIfNoNewStreams();
          if (this.connection != null) {
            //如果不为 null,则复用,赋值给 result
            result = this.connection;
            releasedConnection = null;
          }
          ...
          //如果result为 null,说明上面找不到可以复用的
          if (result == null) {
            //从连接池中获取,调用其get()方法
            Internal.instance.get(connectionPool, address, this, null);
            if (connection != null) {
              //找到对应的 RealConnection对象
              //更改标志位,赋值给 result
              foundPooledConnection = true;
              result = connection;
            } else {
              selectedRoute = route;
            }
          }
        }
        
        ...
        if (result != null) {
          //已经找到 RealConnection对象,直接返回
          return result;
        }
        
        ...
         //连接池中找不到,new一个
         result = new RealConnection(connectionPool, selectedRoute);
        ...
        
        ...
        //发起请求
        result.connect(
            connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled, call, eventListener);
        ...
        //存进连接池中,调用其put()方法
        Internal.instance.put(connectionPool, result);
        ...
        return result;
      }
    

    以上代码主要做的事情有:

    1. StreamAllocation的connection如果可以复用则复用;
    2. 如果connection不能复用,则从连接池中获取RealConnection对象,获取成功则返回;
    3. 如果连接池里没有,则new一个RealConnection对象;
    4. 调用RealConnection的connect()方法发起请求;
    5. 将RealConnection对象存进连接池中,以便下次复用;
    6. 返回RealConnection对象。

    ConnectionPool 连接池介绍

    刚才我们说到从连接池中取出RealConnection对象时调用了Internal的get()方法,存进去的时候调用了其put()方法。其中Internal是一个抽象类,里面定义了一个静态变量instance:

    public abstract class Internal {
        ...
        public static Internal instance;
        ...
    }
    

    instance的实例化是在OkHttpClient的静态代码块中:

    public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory {
      ...
      static {
          Internal.instance = new Internal() {
             ...
              @Override public RealConnection get(ConnectionPool pool, Address address,
              StreamAllocation streamAllocation, Route route) {
                return pool.get(address, streamAllocation, route);
             }
             ...
             @Override public void put(ConnectionPool pool, RealConnection connection) {
               pool.put(connection);
             }
          };
      }
      ...
    }
    

    这里我们可以看到实际上 Internal 的 get()方法和put()方法是调用了 ConnectionPool 的get()方法和put()方法,这里我们简单看下ConnectionPool的这两个方法:

    private final Deque<RealConnection> connections = new ArrayDeque<>();
    
    @Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
        assert (Thread.holdsLock(this));
        for (RealConnection connection : connections) {
          if (connection.isEligible(address, route)) {
            streamAllocation.acquire(connection, true);
            return connection;
          }
        }
        return null;
      }
    

    在get()方法中,通过遍历connections(用于存放RealConnection的ArrayDeque队列),调用RealConnection的isEligible()方法判断其是否可用,如果可用就会调用streamAllocation的acquire()方法,并返回connection。

    我们看下调用StreamAllocation的acquire()方法到底做了什么操作:

    public void acquire(RealConnection connection, boolean reportedAcquired) {
        assert (Thread.holdsLock(connectionPool));
        if (this.connection != null) throw new IllegalStateException();
    
        //赋值给全局变量
        this.connection = connection;
        this.reportedAcquired = reportedAcquired;
        //创建StreamAllocationReference对象并添加到allocations集合中
        connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
      }
    
    1. 先是从连接池中获取的RealConnection对象赋值给StreamAllocation的成员变量connection;

    2. 创建StreamAllocationReference对象(StreamAllocation对象的弱引用),
      并添加到RealConnection的allocations集合中,到时可以通过allocations集合的大小来判断网络连接次数是否超过OkHttp指定的连接次数。

    接着我们查看ConnectionPool 的put()方法:

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

    put()方法在将连接添加到连接池之前,会先执行清理任务,通过判断cleanupRunning是否在执行,如果当前清理任务没有执行,则更改cleanupRunning标识,并执行清理任务cleanupRunnable。

    我们看下清理任务cleanupRunnable中到底做了哪些操作:

    private final Runnable cleanupRunnable = new Runnable() {
        @Override public void run() {
          while (true) {
            //对连接池进行清理,返回进行下次清理的间隔时间。
            long waitNanos = cleanup(System.nanoTime());
            if (waitNanos == -1) return;
            if (waitNanos > 0) {
              long waitMillis = waitNanos / 1000000L;
              waitNanos -= (waitMillis * 1000000L);
              synchronized (ConnectionPool.this) {
                try {
                  //进行等待
                  ConnectionPool.this.wait(waitMillis, (int) waitNanos);
                } catch (InterruptedException ignored) {
                }
              }
            }
          }
        }
      };
    

    可以看到run()方法里面是一个while死循环,其中调用了cleanup()方法进行清理操作,同时会返回进行下次清理的间隔时间,如果返回的时间间隔为-1,则会结束循环,如果不是-1,则会调用wait()方法进行等待,等待完成后又会继续循环执行,具体的清理操作在cleanup()方法中:

    long cleanup(long now) {
        //正在使用的连接数
        int inUseConnectionCount = 0;
        //空闲的连接数
        int idleConnectionCount = 0;
        //空闲时间最长的连接
        RealConnection longestIdleConnection = null;
        //最大的空闲时间,初始化为 Long 的最小值,用于记录所有空闲连接中空闲最久的时间
        long longestIdleDurationNs = Long.MIN_VALUE;
    
        synchronized (this) {
          //for循环遍历connections队列
          for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
            RealConnection connection = i.next();
    
            //如果遍历到的连接正在使用,则跳过,continue继续遍历下一个
            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) {
             //没有空闲连接,返回keepAliveDurationNs
            return keepAliveDurationNs;
          } else {
            //连接池中没有连接存在,返回-1
            cleanupRunning = false;
            return -1;
          }
        }
    
        //关闭空闲时间最长的连接
        closeQuietly(longestIdleConnection.socket());
    
        return 0;
      }
    

    cleanup()方法通过for循环遍历connections队列,记录最大空闲时间和空闲时间最长的连接;如果存在超过空闲保活时间或空闲连接数超过最大空闲连接数限制的连接,则从connections中移除,最后执行关闭该连接的操作。

    主要是通过pruneAndGetAllocationCount()方法判断连接是否处于空闲状态:

    private int pruneAndGetAllocationCount(RealConnection connection, long now) {
        List<Reference<StreamAllocation>> references = connection.allocations;
        for (int i = 0; i < references.size(); ) {
          Reference<StreamAllocation> reference = references.get(i);
    
          if (reference.get() != null) {
            i++;
            continue;
          }
    
          ...
          
          references.remove(i);
          connection.noNewStreams = true;
          
          ...
          
          if (references.isEmpty()) {
            connection.idleAtNanos = now - keepAliveDurationNs;
            return 0;
          }
        }
    
        return references.size();
      }
    

    该方法通过for循环遍历RealConnection的allocations集合,如果当前遍历到的StreamAllocation被使用就遍历下一个,否则就将其移除,如果移除后列表为空,则返回0,所以如果方法的返回值为0则说明当前连接处于空闲状态,如果返回值大于0则说明连接正在使用。

    CallServerInterceptor

    接下来讲解最后一个拦截器CallServerInterceptor了,查看intercept()方法:

    @Override public Response intercept(Chain chain) throws IOException {
        RealInterceptorChain realChain = (RealInterceptorChain) chain;
        //相关对象的获取 
        HttpCodec httpCodec = realChain.httpStream();
        StreamAllocation streamAllocation = realChain.streamAllocation();
        RealConnection connection = (RealConnection) realChain.connection();
        Request request = realChain.request();
    
        ...
        
        //写入请求头
        httpCodec.writeRequestHeaders(request);
    
        Response.Builder responseBuilder = null;
        if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
          //判断是否有请求体
          if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
            //询问服务器是否愿意接收请求体
            httpCodec.flushRequest();//刷新请求
            realChain.eventListener().responseHeadersStart(realChain.call());
            responseBuilder = httpCodec.readResponseHeaders(true);
          }
    
          if (responseBuilder == null) {
            //服务器愿意接收请求体
            //写入请求体
            ...
          } else if (!connection.isMultiplexed()) {
            streamAllocation.noNewStreams();
          }
        }
    
        //结束请求
        httpCodec.finishRequest();
    
        if (responseBuilder == null) {
          realChain.eventListener().responseHeadersStart(realChain.call());
          //根据服务器返回的数据构建 responseBuilder对象
          responseBuilder = httpCodec.readResponseHeaders(false);
        }
    
        //构建 response对象
        Response response = responseBuilder
            .request(request)
            .handshake(streamAllocation.connection().handshake())
            .sentRequestAtMillis(sentRequestMillis)
            .receivedResponseAtMillis(System.currentTimeMillis())
            .build();
    
        ...
        
        //设置 response的 body
        response = response.newBuilder()
              .body(httpCodec.openResponseBody(response))
              .build();
       
       //如果请求头中 Connection对应的值为 close,则关闭连接
        if ("close".equalsIgnoreCase(response.request().header("Connection"))
            || "close".equalsIgnoreCase(response.header("Connection"))) {
          streamAllocation.noNewStreams();
        }
        
        ...
        
        return response;
      }
    

    以上代码具体的流程:

    1. 从拦截器链中获取到保存的相关对象;
    2. 调用HttpCodec的writeRequestHeaders()方法写入请求头;
    3. 判断是否需要写入请求体,先是判断请求方法,如果满足,请求头通过携带特殊字段Expect: 100-continue来询问服务器是否愿意接收请求体;
    4. 结束请求;
    5. 根据服务器返回的数据构建response对象;
    6. 关闭连接;
    7. 返回response;

      好了,到这里OkHttpClient源码分析就结束了,相信看完本套源码解析会加深你对OkHttpClient的认识,同时也学到了其巧妙的代码设计思路,在阅读源码的过程中,我们的编码能力也逐步提升,如果想要写更加优质的代码,阅读源码是一件很有帮助的事。

    相关文章

      网友评论

        本文标题:OkHttpClient源码分析(五)—— ConnectInt

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