网络请求框架学习 okhttp ----连接池

上一篇拦截器分析中,在ConnectInterceptor的intercept方法中,有这样一句代码来获得stream。

HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);

点streamAllocation进去之后看到newStream方法中调用了findHealthyConnection方法,实现如下:

private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
      int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
      throws IOException {
    while (true) {
      // 这一句代码是实际获得连接的
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
          connectionRetryEnabled);

      // If this is a brand new connection, we can skip the extensive health checks.
      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          return candidate;
        }
      }

      // Do a (potentially slow) check to confirm that the pooled connection is still good. If it
      // isn't, take it out of the pool and start again.
      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        noNewStreams();
        continue;
      }

      return candidate;
    }
  }

如果是新的连接就跳过健康检查,如果不是就查一下是否已经断开啦,输入输出是否关闭啦。再进去看findConnection方法

private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      boolean connectionRetryEnabled) throws IOException {
    Route selectedRoute;
    // 在pool中取连接的话,就需要拿这个pool做同步锁,如果是第一次发起请求应该是拿不到的,会走到下面
    synchronized (connectionPool) {
      if (released) throw new IllegalStateException("released");
      if (codec != null) throw new IllegalStateException("codec != null");
      if (canceled) throw new IOException("Canceled");

      // Attempt to use an already-allocated connection.
      RealConnection allocatedConnection = this.connection;
      // 如果allocatedConnection 不为空并且连接池还没满,就直接使用这个连接
      if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
        return allocatedConnection;
      }

      // Attempt to get a connection from the pool.
      // 调用了okhttpclient的get方法,从connectionPool中根据address拿到连接。
      Internal.instance.get(connectionPool, address, this, null);
      if (connection != null) {
        return connection;
      }
      // 第一次进来就是空
      selectedRoute = route;
    }

    // If we need a route, make one. This is a blocking operation.
    if (selectedRoute == null) {
      // 这里面先是查找内存缓存,根据proxies的类型在routeSelector的集合inetSocketAddresses中查找,没有的话就重设一个
      // 调用address.dns().lookup(socketHost)方法,通过DNS服务器查询返回一组ip地址(一个域名可能对应多个ip地址,可用于自动重连)
      // 最后将得到的address 加入集合inetSocketAddresses中缓存起来。
      selectedRoute = routeSelector.next();
    }

    RealConnection result;
    synchronized (connectionPool) {
      if (canceled) throw new IOException("Canceled");

      // Now that we have an IP address, make another attempt at getting a connection from the pool.
      // This could match due to connection coalescing.
      // 再进行一次尝试,从连接池中拿连接
      Internal.instance.get(connectionPool, address, this, selectedRoute);
      if (connection != null) {
        route = selectedRoute;
        return connection;
      }

      // Create a connection and assign it to this allocation immediately. This makes it possible
      // for an asynchronous cancel() to interrupt the handshake we're about to do.
      route = selectedRoute;
      refusedStreamCount = 0;
      // 新搞一个连接
      result = new RealConnection(connectionPool, selectedRoute);
      // 将connection的引用交给streamAllocation,将streamAllocation的弱引用加入到connection的allocations集合中
      acquire(result);
    }

    // Do TCP + TLS handshakes. This is a blocking operation.
    // 这里调用RealConnection的connect方法,传入ip 端口号进行connect,RealConnection内部的source和sink就是在这个方法中赋值的。
    result.connect(connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled);
    // connectionPool中维护了一个键值对,里面存了所有连接失败的route,每个连接失败的route都加入进去。
    // 而这句话是把连接成功的从route黑名单中去除掉。  
    routeDatabase().connected(result.route());

    Socket socket = null;
    synchronized (connectionPool) {
      // Pool the connection.
      // 用okhttpclient的代码把当前的连接放入连接池中,这种麻烦的写法估计是跟设计模式有关系
      // 注意放进去的同时会触发清理
      Internal.instance.put(connectionPool, result);

      // If another multiplexed connection to the same address was created concurrently, then
      // release this connection and acquire that one.
      // 处理多线程产生的问题,如果产生了多个connection就release掉当前的,用另一个线程创建的connection
      // 并且关闭掉多余的socket
      if (result.isMultiplexed()) {
        socket = Internal.instance.deduplicate(connectionPool, address, this);
        result = connection;
      }
    }
    closeQuietly(socket);

    return result;
  }

代码很长,我看到的是:
RouteSelector做准备;
ConnectionPool管理连接;
RealConnection做具体执行;
以StreamAllocation为中心协调各个类;
将RealConnection生成的Http1Codec和Http2Codec这种面向协议(设置请求头读取回复)进行sink和read的类 传递到拦截器中。

代码执行步骤大致分三部分:

  1. 获得route、ip、port那些鬼,由RouteSelector这个类完成,拿到address之后再尝试连接池中拿connection。
  2. 实在从连接池中拿不到了,就新建connection,用raw socket进行三握手那些鬼,由RealConnection这个类完成,拿到source和sink;
    让connection和streamAllocation相互引用(一个强引用一个弱引用),连接池里面有一个ArrayDeque来记录所有的socket连接。将新的connection放入连接池,触发清理;
    将route从黑名单移除。
  3. 检查是否有多线程导致的问题,如果有,就释放当前连接,用别的线程创建的连接。

上面说将连接加入连接池时会触发清理操作,下面贴上代码详细说明是如何清理的。

在connectionPool中,有个cleanup方法来执行清理操作

 long cleanup(long now) {
    int inUseConnectionCount = 0;
    int idleConnectionCount = 0;
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

    // Find either a connection to evict, or the time that the next eviction is due.
    synchronized (this) {
      for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();

        // If the connection is in use, keep searching.
        // 这里具体执行streamAllocation的清理,具体代码在下面
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }

        idleConnectionCount++;

        // If the connection is ready to be evicted, we're done.
        // 如果说:闲置的时间超过了设定值,或者最大限制连接数超过设定值,就把connection从连接池中移除,并关掉connection。
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }

      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {
        // We've found a connection to evict. Remove it from the list, then close it below (outside
        // of the synchronized block).
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        // A connection will be ready to evict soon.
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // All connections are in use. It'll be at least the keep alive duration 'til we run again.
        return keepAliveDurationNs;
      } else {
        // No connections, idle or in use.
        cleanupRunning = false;
        return -1;
      }
    }

    closeQuietly(longestIdleConnection.socket());

    // Cleanup again immediately.
    // 根据闲置时间和闲置连接数,还有可能立刻执行下一次清理
    return 0;
  }
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;
      }

      // We've discovered a leaked allocation. This is an application bug.
      //遍历每一个connection的streamAllocation弱引用集合,发现弱引用已被回收,就将其在弱引用集合中移除
      StreamAllocation.StreamAllocationReference streamAllocRef =
          (StreamAllocation.StreamAllocationReference) reference;
      String message = "A connection to " + connection.route().address().url()
          + " was leaked. Did you forget to close a response body?";
      // 打印警告,告知程序员,你的使用有问题
      Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace);

      references.remove(i);
      connection.noNewStreams = true;

      // If this was the last allocation, the connection is eligible for immediate eviction.
      if (references.isEmpty()) {
        // 如果所有的弱引用都被移除掉了,说明这个connection是闲置的,记录闲置的时间。将闲置最久的connection记录下来。
        connection.idleAtNanos = now - keepAliveDurationNs;
        return 0;
      }
    }

    return references.size();
  }

总结一下,清理策略就是:

  1. connection自身记录streamAllocation的连接数,达到0的时候就标记自己为闲置连接,记录闲置时间等待清理
  2. 满足闲置时间太长或者闲置连接太多时,ConnectionPool就执行清理操作关掉连接(默认空闲的socket最大连接数为5个,socket的keepAlive时间为5秒)。
  3. 正常情况下ConnectionPool会每隔一段时间就尝试清理一次。看连接使用情况,忙的话就一直尝试清理,闲的时候加入任务也会触发清理。

okhttp对socket的直接管理还是通过ConnectionPool来实现的。

回顾一下前面的拦截器的知识,结合一下:

  1. 在RealInterceptorChain中有一个streamAllocation成员变量
  2. 在RetryAndFollowUpInterceptor中初始化streamAllocation传到RealInterceptorChain中,此时还是没有任何连接和这个streamAllocation绑定的
  3. 到了ConnectInterceptor中,调用streamAllocation的newStream方法,内部调用findConnection方法,获得连接
  4. 连接的获得是先尝试从连接池中取,取不到就初始化一个连接,将streamAllocation弱引用给connection(此时connection可能已经有很多streamAllocation在用了),同时在连接池中尝试清理。
  5. 拿到连接之后,返回给ConnectInterceptor一个HttpCodec,这是一个接口的实现类,根据http协议是 1.x 还是2 内部有不同的实现
  6. 回到CallServerInterceptor中,拿HttpCodec来执行写入请求头、读取返回信息、构造responseBody等。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容