okhttp实现连接池原理

okhttp实现连接池原理

为啥需要

对于tcp/ip的网络请求,是短连接请求,每次理论上是需要三次握手和四次挥手的。频繁的进行建立Sokcet连接和断开Socket是非常消耗网络资源和浪费时间的。

然后HTTP的keep-alive也是用来优化的连接的。

  • 普通的HTTP请求是客户端连接到服务端了,请求结束后关闭连接。流程是反复的。
  • 带keep-alive头HTTP请求,服务端接受到这个字段,在一定的时间内,会维持这次连接,这期间的请求不会再次连接,而是直接复用了。

由此可见网络请求的优化是至关重要的。而okhttp则是采用连接池进行复用,减少连接的创建和关闭,增加系统负载能力。

如何实现

先说结论:OkHttp里面使用ConnectionPool实现连接池,而且默认支持5个并发KeepAlive,默认链路生命为5分钟。

先看初始化位置,okhttp采用的构建者模式创建的,其中build方法有如下:

 public Builder() {
      dispatcher = new Dispatcher();
      protocols = DEFAULT_PROTOCOLS;
      connectionSpecs = DEFAULT_CONNECTION_SPECS;
      eventListenerFactory = EventListener.factory(EventListener.NONE);
      proxySelector = ProxySelector.getDefault();
      if (proxySelector == null) {
        proxySelector = new NullProxySelector();
      }
      cookieJar = CookieJar.NO_COOKIES;
      socketFactory = SocketFactory.getDefault();
      hostnameVerifier = OkHostnameVerifier.INSTANCE;
      certificatePinner = CertificatePinner.DEFAULT;
      proxyAuthenticator = Authenticator.NONE;
      authenticator = Authenticator.NONE;
      //创建默认连接池,也可以指定其他参数
      connectionPool = new ConnectionPool();
      dns = Dns.SYSTEM;
      followSslRedirects = true;
      followRedirects = true;
      retryOnConnectionFailure = true;
      callTimeout = 0;
      connectTimeout = 10_000;
      readTimeout = 10_000;
      writeTimeout = 10_000;
      pingInterval = 0;
    }

跟进看ConnectionPool构造方法:

  /**
  * 使用适合于单用户应用程序的调整参数创建一个新的连接池。 *此池中的调整参数可能会在将来的OkHttp版本中更改。当前*该池最多可容纳5个空闲连接,这些空闲连接在闲置5分钟后将被驱逐。
  **/
  public ConnectionPool() {
    this(5, 5, TimeUnit.MINUTES);
  }

  public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
    this.maxIdleConnections = maxIdleConnections;
    this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);

    // Put a floor on the keep alive duration, otherwise cleanup will spin loop.
    if (keepAliveDuration <= 0) {
      throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
    }
  }

构造中默认设置了并发数量为5个,默认存活时间为5分钟。

再看下类中静态的变量,随着类创建一起创建的,如下:

/**
   * 后台线程用于清除过期的连接。每个连接池最多只能运行一个*线程。线程池执行程序允许对池本身进行*垃圾收集。
   */
  private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
      Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
      new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));

这里就是创建了一个线程池,只会执行一种线程,是用来执行连接池清理任务的。任务执行内容等会再看。
类中还有个关键的全局变量,双端队列Deque,双端都能进出,用来存储连接的:

  private final Deque<RealConnection> connections = new ArrayDeque<>();

再来说下是如何实现的:

  • put函数:

  void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
    //没有连接的时候调用
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }

RealConnection是双端队列connections存放的连接,先判断是否在锁状态后,继续判断是否在清理状态,cleanupRunning首先初始化是false值,也是无连接时候。执行cleanupRunnable只会在无连接、空闲或者使用时才会把cleanupRunning置成false,因为放入的连接后,连接池可能超出最大规定容量或者存在存活时间超时情况,所以要执行cleanupRunnable。最后会将这次连接放入双端队列中。

  • get函数
 @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;
  }

遍历队列集合,获取匹配且可用的连接并返回,没有则返回null。

  • cleanup函数
    前面提到的线程池来执行的任务,就是不停的调用cleanupRunnable来清楚线程池。
    先看Runnable执行代码块:
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) {
            }
          }
        }
      }
    }
  };

cleanup方法会返回休眠时间,让出cpu节省资源。
cleanup方法:

  /**
   * 在此池上执行维护,如果超过了“保持活动”限制或“空闲连接”限制,则驱逐最长的空闲连接。  <p>以nanos为单位返回睡眠时间,直到下一次计划调用此方法为止。如果不需要进一步清理,则返回-1
   * -1 不需要进一步处理
   */
  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();

        // 如果连接在使用,则跳过继续寻找,inUseConnectionCount++
        if (pruneAndGetAllocationCount(connection, now) > 0) {
         //使用计数
          inUseConnectionCount++;
          continue;
        }
		//空闲计数
        idleConnectionCount++;

        // 如果准备好退出连接,那么我们就完成了。
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }
		//上面是找到了最长空闲时间连接
		//keepAliveDurationNs  是构造定义的最大维持存活时间
		//maxIdleConnections  是构造定义的最大存活数量
      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {
        // 符合条件,就从队列移除
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        // 如果还有连接未清除,那么现在的线程的等待休眠时间
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        // 所有连接都在使用中。直到我们再次运行,这至少是保持生命的持续时间。
        return keepAliveDurationNs;
      } else {
        // 没有连接在使用
        cleanupRunning = false;
        return -1;
      }
    }
	//真正的关闭socket连接
    closeQuietly(longestIdleConnection.socket());

    // Cleanup again immediately.
    return 0;
  }

那如何判断是闲置连接了?

再来看下get函数中的一句代码:streamAllocation.acquire(connection, true);

//使用此分配来保存connection。每个对此的调用必须与同一连接上对{@link #release}的调用配对。
  public void acquire(RealConnection connection, boolean reportedAcquired) {
    assert (Thread.holdsLock(connectionPool));
    if (this.connection != null) throw new IllegalStateException();

    this.connection = connection;
    this.reportedAcquired = reportedAcquired;
    connection.allocations.add(new StreamAllocationReference(this, callStackTrace));
  }

acquire函数,做的就是add操作。
对比看下release函数,其实是allocations移除StreamAllocationReference操作

 /** Remove this allocation from the connection's list of allocations. */
  private void release(RealConnection connection) {
    for (int i = 0, size = connection.allocations.size(); i < size; i++) {
      Reference<StreamAllocation> reference = connection.allocations.get(i);
      if (reference.get() == this) {
        connection.allocations.remove(i);
        return;
      }
    }
    throw new IllegalStateException();
  }

其中StreamAllocationReference是弱引用,RealConnection中,有一个StreamAllocation引用列表allocations。每创建一个连接,就会把连接对应的StreamAllocationReference添加进该列表中,如果连接关闭以后就将该对象移除。这样可以根据弱引用的堆栈回收信息来计数。
再看cleanup函数中,确定计数的函数pruneAndGetAllocationCount

 /**
   *整理所有泄漏的分配,然后返回* {@code connection}上剩余的实时分配数。如果连接正在跟踪分配,但是应用程序代码已放弃分配,则分配将泄漏。泄漏检测不精确,并且依赖于垃圾收集。
   */
  private int pruneAndGetAllocationCount(RealConnection connection, long now) {
  	//拿到了上文提到的引用列表 allocations
    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.
      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);
	//被回收了或者一些其他情况,导致StreamAllocation为null,然后从集合移除此引用
      references.remove(i);
      connection.noNewStreams = true;

      // 如果这是最后一次分配,则该连接可以立即驱逐。
      if (references.isEmpty()) {
        connection.idleAtNanos = now - keepAliveDurationNs;
        return 0;
      }
    }
	//返回计数的size
    return references.size();
  }

这里配合前面的弱引用,完成计数。
其实可以这样理解,在上层反复调用acquire和release函数,来增加或减少connection.allocations所维持的集合的大小,到最后如果size大于0,则代表RealConnection还在使用连接,如果size等于0,那就说明已经处于空闲状态了。

怎么获取?

get函数怎么调用的,其实很简单,往上找代码就行了,简单说说。

发送call请求
Dispatcher分发
拦截器ConnectInterceptor处理
streamAllocation.newStream
findHealthyConnection
findConnection
Internal.instance.get
pool.get
MySQL中concat函数 使用方法: CONCAT(str1,str2,…) 返回结果为连接参数产生的字符串。如有任何一个参数为NULL ,则返回值为 NULL。 注意: 如果所有参数均为非二进制字符串,则结果为非二进制字符串。 如果自变量中含有任一二进制字符串,则结果为一个二进制字符串。 一个数字参数被转化为与之相等的二进制字符串格式;若要避免这种情况,可使用显式类型 cast, 例如: SELECT CONCAT(CAST(int_col AS CHAR), char_col) MySQL的concat函数可以连接一个或者多个字符串,如 mysql> select concat('10'); +--------------+ | concat('10') | +--------------+ | 10 | +--------------+ 1 row in set (0.00 sec) mysql> select concat('11','22','33'); +------------------------+ | concat('11','22','33') | +------------------------+ | 112233 | +------------------------+ 1 row in set (0.00 sec) MySQL的concat函数在连接字符串的时候,只要其中一个是NULL,那么将返回NULL mysql> select concat('11','22',null); +------------------------+ | concat('11','22',null) | +------------------------+ | NULL | +------------------------+ 1 row in set (0.00 sec) MySQL中concat_ws函数 使用方法: CONCAT_WS(separator,str1,str2,...) CONCAT_WS() 代表 CONCAT With Separator ,是CONCAT()的特殊形式。第一个参数是其它参数的分隔符。分隔符的位置放在要连接的两个字符串之间。分隔符可以是一个字符串,也可以是其它参数。 注意: 如果分隔符为 NULL,则结果为 NULL。函数会忽略任何分隔符参数后的 NULL 值。 如连接后以逗号分隔 mysql> select concat_ws(',','11','22','33'); +-------------------------------+ | concat_ws(',','11','22','33') | +-------------------------------+ | 11,22,33 | +-------------------------------+ 1 row in set (0.00 sec) 和MySQL中concat函数不同的是, concat_ws函数在执行的时候,不会因为NULL值而返回NULL mysql> select concat_ws(',','11','22',NULL); +-------------------------------+ | concat_ws(',','11','22',NULL) | +-------------------------------+ | 11,22 | +-------------------------------+ 1 row in set (0.00 sec) MySQL中group_concat函数 完整的语法如下: group_concat([DISTINCT] 要连接的字段 [Order BY ASC/DESC 排序字段] [Separator '分隔符']) 基本查询 mysql> select * from aa; +------+------+ | id| name | +------+------+ |1 | 10| |1 | 20| |1 | 20| |2 | 20| |3 | 200 | |3 | 500 | +------+------+ 6 rows in set (0.00 sec) 以id分组,把name字段的值打印在一行,逗号分隔(默认) mysql> select id,group_concat(name) from aa group by id; +------+--------------------+ | id| group_concat(name) | +------+--------------------+ |1 | 10,20,20| |2 | 20 | |3 | 200,500| +------+--------------------+ 3 rows in set (0.00 sec) 以id分组,把name字段的值打印在一行,分号分隔 mysql> select id,group_concat(name separator ';') from aa group by id; +------+----------------------------------+ | id| group_concat(name separator ';') | +------+----------------------------------+ |1 | 10;20;20 | |2 | 20| |3 | 200;500 | +------+----------------------------------+ 3 rows in set (0.00 sec) 以id分组,把去冗余的name字段的值打印在一行, 逗号分隔 mysql> select id,group_concat(distinct name) from aa group by id; +------+-----------------------------+ | id| group_concat(distinct name) | +------+-----------------------------+ |1 | 10,20| |2 | 20 | |3 | 200,500 | +------+-----------------------------+ 3 rows in set (0.00 sec) 以id分组,把name字段的值打印在一行,逗号分隔,以name排倒序 mysql> select id,group_concat(name order by name desc) from aa group by id; +------+---------------------------------------+ | id| group_concat(name order by name desc) | +------+---------------------------------------+ |1 | 20,20,10 | |2 | 20| |3 | 500,200| +------+---------------------------------------+ 3 rows in set (0.00 sec) repeat()函数 用来复制字符串,如下'ab'表示要复制的字符串,2表示复制的份数 mysql> select repeat('ab',2); +----------------+ | repeat('ab',2) | +----------------+ | abab | +----------------+ 1 row in set (0.00 sec) 又如 mysql> select repeat('a',2); +---------------+ | repeat('a',2) | +---------------+ | aa | +---------------+ 1 row in set (0.00 sec) mysql向表中某字段后追加一段字符串: update table_name set field=CONCAT(field,'',str) mysql 向表中某字段前加字符串 update table_name set field=CONCAT('str',field) 这个函数对你也许会有很大帮助哦!!
OkHttp 是一个开源的 HTTP 客户端,用于 Android 平台和 Java 应用。它建立在 Java 的 HttpURLConnection 类之上,并提供了更简洁、更强大的 API。 OkHttp 的工作原理主要涉及以下几个关键组件: 1. `OkHttpClient`:这是 OkHttp 的核心类,负责配置和创建请求、设置拦截器、管理连接池等。你可以通过构建 OkHttpClient 实例来自定义请求的行为和参数。 2. `Request`:表示一个 HTTP 请求,包括 URL、请求方法(如 GET、POST)、请求体、请求头等信息。你可以通过 Request.Builder 构建一个 Request 实例。 3. `Response`:表示一个 HTTP 响应,包括响应码、响应体、响应头等信息。OkHttp 会将服务器返回的数据解析成 Response 对象。 4. `Interceptor`:拦截器用于在发送请求和接收响应之前进行一些额外的处理。OkHttp 提供了很多内置的拦截器,如重试拦截器、缓存拦截器等,同时也支持自定义拦截器。 5. `Dispatcher`:调度器负责管理请求的调度和执行。它可以控制同时并发执行的请求数量,还可以设置请求超时时间等。 6. `ConnectionPool`:连接池用于管理 HTTP 连接的复用和回收。OkHttp 会自动复用连接以减少网络延迟,提高性能。 7. `Cache`:缓存可以保存服务器返回的响应,以便在后续的请求中复用。OkHttp 支持对响应进行缓存,并提供了灵活的配置选项。 当你使用 OkHttp 发起一个网络请求时,它会通过 OkHttpClient 来创建一个 Request 对象,并通过 Dispatcher 来执行这个请求。在执行过程中,OkHttp 会根据设置的拦截器进行一系列的处理,如添加请求头、重试、缓存等。最终,OkHttp 将返回一个 Response 对象,你可以从中获取到服务器返回的数据。 总体来说,OkHttp 的工作原理是通过封装底层的 HttpURLConnection,提供了简洁易用的 API,并通过拦截器和连接池等机制优化了网络请求的性能和可定制性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值