OkHttpClient源码分析(五)—— ConnectInterceptor和CallServerInterceptor

上一篇我们介绍了缓存拦截器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的认识,同时也学到了其巧妙的代码设计思路,在阅读源码的过程中,我们的编码能力也逐步提升,如果想要写更加优质的代码,阅读源码是一件很有帮助的事。

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

推荐阅读更多精彩内容