四、OkHttp_连接池

news2024/11/16 2:27:38

预备知识

通常我们进行HTTP连接网络的时候我们会进行TCP的三次握手,然后传输数据,然后再释放连接。 大量的连接每次连接关闭都要三次握手四次分手的很显然会造成性能低下, 因此http有一种叫做keep-alive connections的机制(HTTP1.1以后默认开启),它可以在传输数据后仍然保持连接, 当客户端需要再次获取数据时,直接使用刚刚空闲下来的连接而不需要再次握手。

image

OkHttp的复用连接池就是为了复用这些没有断开连接的TCP连接的。

连接池概述

okhttp连接的建立主要是围绕ConnectInterceptor来的,

1、流程为首先通过RealCall的initExchange(chain)创建一个Exchange对象,其中会打开与目标服务器的链接, 并调用 Chain.proceed()方法进入下一个拦截器。

2、initExchange()方法中会先通过 ExchangeFinder 尝试去 RealConnectionPool 中寻找已存在的连接,未找到则会重新创建一个RealConnection 并开始连接,

3、然后将其存入RealConnectionPool,现在已经准备好了RealConnection 对象,

4、然后通过请求协议创建不同的ExchangeCodec 并返回,返回的ExchangeCodec正是创建Exchange对象的一个参数。

ConnectInterceptor 主要目的

ConnectInterceptor 的主要职责是建立与目标服务器的连接,并为后续的拦截器(CallServerInterceptor)提供一个有效的网络通道。这样可以实现发送请求并接收服务器响应的功能。

ConnectInterceptor的代码很简单,主要的功能就是初始化RealCall的Exchange。这个Exchange的功能就是基于RealConnection+ExchangeCodec进行数据交换。

核心类介绍


RealConnection

RealConnection 实现了 Connection接口,其中使用 Socket建立HTTP/HTTPS连接,并且获取 I/O 流,内部持有输入和输出流。

如果拥有了一个RealConnection就代表了我们已经跟服务器有了一条通信链路,而且通过RealConnection代表是连接socket链路,RealConnection对象意味着我们已经跟服务端有了一条通信链路了,同一个 Connection 可能会承载多个 HTTP 的请求与响应。

类构造与关键属性:

class RealConnection(
  val connectionPool: RealConnectionPool,
  private val route: Route
) : Http2Connection.Listener(), Connection {
  //和服务器直接通信的socket实例和用于数据读写的输入输出流
  private var rawSocket: Socket? = null
  private var source: BufferedSource? = null
  private var sink: BufferedSink? = null
  ...
  //引用计数法记录本连接被多少个请求持有,用于回收管理中判断该连接是否空闲
  val calls = mutableListOf<Reference<RealCall>>()
  ...
}

RealConnectionPool

这是用来存储 RealConnection 的池子,内部使用一个双端队列来进行存储。

在 OkHttp 中,一个连接(RealConnection)用完后不会立马被关闭并释放掉,而且是会存储到连接池(RealConnectionPool)中。 除了缓存连接外,缓存池还负责定期清理过期的连接,在 RealConnection 中会维护一个用来描述该连接空闲时间的字段,每添加一个新的连接到连接池中时都会进行一次检测,遍历所有的连接,找出当前未被使用且空闲时间最长的那个连接,如果该连接空闲时长超出阈值,或者连接池已满,将会关闭该连接。

类构造与关键属性:

class RealConnectionPool(
	taskRunner: TaskRunner,
	//每个空闲Socket的最大连接数,默认为5
	private val maxIdleConnections: Int,
	keepAliveDuration: Long,
	timeUnit: TimeUnit
) {
	//连接保活时间,默认5分钟
	private val keepAliveDurationNs: Long = timeUnit.toNanos(keepAliveDuration)

	private val cleanupQueue: TaskQueue = taskRunner.newQueue()
	private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
		override fun runOnce() = cleanup(System.nanoTime())
	}

	//RealConnection是Socket对象的包装类,connections也就是对这些连接的缓存
	private val connections = ArrayDeque<RealConnection>()
	
	...
}

ExchangeCodec

ExchangeCodec 的功能就是对http报文的编解码,负责对Request 编码及解码 Response,也就是写入请求及读取响应,我们的请求及响应数据都通过它来读写。

其实现类有两个:Http1ExchangeCodec 及 Http2ExchangeCodec,分别对应两种协议版本。

Exchange

功能类似 ExchangeCodec,但它是对应的是单个请求,其在 ExchangeCodec 基础上担负了一些连接管理及事件分发的作用。

具体而言,Exchange 与 Request 以及ExchangeCodec 一一对应,新建一个请求时就会创建一个 Exchange,该 Exchange 负责将这个请求发送出去并读取到响应数据,而发送与接收数据使用的是 ExchangeCodec。

关键流程解析


调用链

--ConnectInterceptor.intercept
	--RealCall.initExchange
		--ExchangeFinder.find
			--ExchangeFinder.findHealthyConnection
				--ExchangeFinder.findConnection
				       --RealConnectionPool.callAcquirePooledConnection
						...
				--RealConnection.newCodec

通过ExchangeFinder找到或者新建连接

class ExchangeFinder(
 ...
) {
	...
	fun find(client: OkHttpClient, chain: RealInterceptorChain ): ExchangeCodec {
		...
		val resultConnection = findHealthyConnection(...)
		return resultConnection.newCodec(client, chain)
		...
	}

	private fun findHealthyConnection(...): RealConnection {
		...
		val candidate = findConnection(...)
		...
	}

	private fun findConnection(...): RealConnection {
		...
	// 首先查找当前call对应的connection是否可以使用	
	val callConnection = call.connection	
	if (callConnection != null) {
	    if (callConnection.noNewExchanges || !sameHostAndPort(callConnection.route().address.url)) {
	        
	    }
	}
	// 从连接池里面查找可用连接
		if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
			val result = call.connection!!
			eventListener.connectionAcquired(call, result)
			return result
		}
		...
	// 找不到的话创建新的连接
		val newConnection = RealConnection(connectionPool, route)
		...
	// 连接socket
		newConnection.connect(......
	// 将新连接丢到连接池
		connectionPool.put(newConnection)
	// 绑定RealCall和连接
		call.acquireConnectionNoEvents(newConnection)
		...
		return newConnection
	}
	...
}
		   
class RealCall(...) : Call {
	fun acquireConnectionNoEvents(connection: RealConnection) {
		...
		this.connection = connection
		connection.calls.add(CallReference(this, callStackTrace))
	}
}


我们可以看到这里主要干了:

1.首先从判断当前RealCall是否有对应的Connection,并校验是否可以使用

2.call.connection不存在或者不符合条件,则接着从连接池里面查找可用连接

2.找不到的话创建新的连接RealConnection,连接socket

3.将新连接丢到连接池

4.绑定RealCall和连接RealConnection,也就是将RealConnection内部的引用计数+1

可以用图来描述就是:

image

从连接池里面查找可用连接

我们详细看一下是如何从连接池获取符合条件的connection并弄清楚连接复用的条件。

connectionPool.callAcquirePooledConnection()

从连接池中获取连接并将call添加到connection 的calls集合中

 -> RealConnectionPool
 
  fun callAcquirePooledConnection(
    address: Address,
    call: RealCall,
    routes: List<Route>?,
    requireMultiplexed: Boolean
  ): Boolean {
    for (connection in connections) {
      synchronized(connection) {
      //当需要进行多路复用且当前的连接不是 HTTP/2 连接时,则放弃当前连接
        if (requireMultiplexed && !connection.isMultiplexed) return@synchronized
        // 查找到符合条件的address
        // 分析1.1.1.1.1.1
        if (!connection.isEligible(address, routes)) return@synchronized
        // 找到复用的 connection 并添加call
        call.acquireConnectionNoEvents(connection)
            // realcall.connection = connection
            // 当前连接承载的所有call
            // connection.calls.add(CallReference(realCall, callStackTrace))
        return true
      }
    }
    return false
  }

分析1.1.1.1.1.1、判断连接是否有效

// 对比连接池中获取的每一个address,找到符合条件的address.


internal fun isEligible(address: Address, routes: List<Route>?): Boolean {
    assertThreadHoldsLock()

    ...
    // 连接次数是否已满,在HTTP 1.X的情况下allocationLimit总是为1,即线头阻塞,
    // 前一个请求完全结束后后一个请求才能复用,否则只能新开Connection
    if (calls.size >= allocationLimit || noNewExchanges) return false
    // 找到符合条件的address ,
    // 对比:非Host的地址部分是否相等,内部会比较dns、protocols、proxy、sslSocketFactory、port等
    if (!this.route.address.equalsNonHost(address)) return false

    // 如果主机名相同则返回 true
    if (address.url.host == this.route().address.url.host) {
      return true // This connection is a perfect match.
    }

   // http2 相关 ----------------
   
    // 到达这一步,我们没有主机名匹配。但如果满足连接合并的要求,仍然可以承载请求。
    // 1. 1. 此连接必须是 HTTP/2。
    if (http2Connection == null) return false

    // 2. 路由必须共享一个 IP 地址。
    if (routes == null || !routeMatchesAny(routes)) return false

    // 3. 此连接的服务器证书必须包含新主机。
    if (address.hostnameVerifier !== OkHostnameVerifier) return false
    if (!supportsUrl(address.url)) return false

    // 4. 证书锁定必须与主机匹配。
    try {
      address.certificatePinner!!.check(address.url.host, handshake()!!.peerCertificates)
    } catch (_: SSLPeerUnverifiedException) {
      return false
    }
    // http2 调用方的地址可以由此连接承载。
    return true 
  }
我们可以得到连接池复用的条件为:

1.当前连接可用请求次数已满不可复用,在HTTP 1.X的情况下allocationLimit总是为1,即线头阻塞,前一个请求完全结束后后一个请求才能复用此连接,否则只能新开Connection

2.非Host的地址部分不相等不可复用,内部会比较dns、protocols、proxy、sslSocketFactory、port等

3.前面满足的前提下,host如果相同可复用

4.如果host不相等,但当前http协议是http2那就需要继续判断其他条件决定是否可复用

这里要特别注意

allocationLimit 在HTTP 1.X的情况下allocationLimit总是为1,保证了HTTP 1.X的情况下每次只能跑一个请求, 也就是说必须一个将上次Request的Response完全读取之后才能发送下一次Request。

但http2在多路复用+二进制帧的加持下是允许一个连接同时被多个请求使用的,允许连续发送多个Request 。

Connection引用计数

创建或者复用Connection的时候都会调用到RealCall.acquireConnectionNoEvents,将RealCall的弱引用丢到connection.calls里面,于是就完成了请求对Connection引用计数+1;

class RealCall(...) : Call {
	fun acquireConnectionNoEvents(connection: RealConnection) {
		...
		this.connection = connection
		connection.calls.add(CallReference(this, callStackTrace))
	}
}

有add就有remove,请求完成后,会调用Exchange.complete方法,最终调到RealCall.releaseConnectionNoEvents将引用从connection.calls里面删掉,于是就完成了请求对Connection引用计数-1;

 internal fun releaseConnectionNoEvents(): Socket? {
	...
	val calls = connection.calls
	val index = calls.indexOfFirst { it.get() == this@RealCall }//找到当前请求RealCall对应的Connecton.calls列表中的RealCall
	check(index != -1)

	calls.removeAt(index)
	this.connection = null
	...

	return null
  }

Sockt连接的建立

连接的真正建立其实也是通过Socket来完成的,我们可以简单看一下:

--> RealConnection
 fun connect(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean,
    call: Call,
    eventListener: EventListener
  ) {
    ...
    while (true) {
      try {
        // 检测当前的路由是否通过HTTP代理来实现HTTPS的隧道传输
        if (route.requiresTunnel()) {
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener)
          if (rawSocket == null) {
            // 无法建立隧道连接,但正确关闭了资源。
            break
          }
        } else {
          // 连接socket ,
          connectSocket(connectTimeout, readTimeout, call, eventListener)
        }
        ...
        break
      } catch (e: IOException) {
        // 异常情况重置状态
        ... 
      }
    }
  }
--> RealConnection

 private fun connectSocket(
    connectTimeout: Int,
    readTimeout: Int,
    call: Call,
    eventListener: EventListener
  ) {
    val proxy = route.proxy
    val address = route.address
    // 创建Socket 
    val rawSocket = when (proxy.type()) {
      Proxy.Type.DIRECT, Proxy.Type.HTTP -> address.socketFactory.createSocket()!!
      else -> Socket(proxy)
    }
    this.rawSocket = rawSocket

    rawSocket.soTimeout = readTimeout
    try {
    // 连接socket
      Platform.get().connectSocket(rawSocket, route.socketAddress, connectTimeout)
    } catch (e: ConnectException) {
      ...
    }

    // 获取Socket的读写流,分别封装 读source 写sink
    try {
      source = rawSocket.source().buffer()
      sink = rawSocket.sink().buffer()
    } catch (npe: NullPointerException) {
      ...
    }
  }

Sockt连接的断开

由于我们需要复用连接,因此Socket连接并不是在请求完成后就断开的,如果空闲连接数在允许范围内(默认5个),他会保持空闲存活keep-alive的时间后(默认5分钟)由okhttp的自动清理机制进行清理并关闭。具体分析见下面的空闲连接清理部分。

如何清理空闲连接?

从上面的复用机制我们看到,socket连接在上一次请求完成之后是不会断开的,等待下次请求复用。 如果一直不去断开的话,就会有一个资源占用的问题。

那么OkHttp是在什么时候断开连接的呢?

其实RealConnectionPool内部会有个cleanupTask专门用于连接的清理,它会在RealConnectionPool的put(加入新连接)、connectionBecameIdle(有连接空闲)里面被调用。


  private val cleanupQueue: TaskQueue = taskRunner.newQueue()
  private val cleanupTask = object : Task("$okHttpName ConnectionPool") {
    override fun runOnce() = cleanup(System.nanoTime())
  }
  //加入新连接
  fun put(connection: RealConnection) {
    ...
    cleanupQueue.schedule(cleanupTask)
  }
  //有连接空闲,对应引用计数的-1,也就是RealCall.releaseConnectionNoEvents
  fun connectionBecameIdle(connection: RealConnection): Boolean {
    connection.assertThreadHoldsLock()

    return if (connection.noNewExchanges || maxIdleConnections == 0) {
      connection.noNewExchanges = true
      connections.remove(connection)
      if (connections.isEmpty()) cleanupQueue.cancelAll()
      true
    } else {
      cleanupQueue.schedule(cleanupTask)
      false
    }
  } 

cleanupQueue会根据 Task.runOnce的返回值等待一段时间再次调用runOnce, 这样设计是为了在本次执行清理后,拿到最近一个需要清理的连接到期剩余的时间,方便第一时间将过期的连接清理掉。 这里的runOnce实际就是cleanup方法,这里面会查找空闲过久的连接,然后关闭它的socket:

fun cleanup(now: Long): Long {
    var inUseConnectionCount = 0
    var idleConnectionCount = 0
    var longestIdleConnection: RealConnection? = null
    var longestIdleDurationNs = Long.MIN_VALUE

    // 找到下一次空闲连接超时的时间
    for (connection in connections) {
      synchronized(connection) {
        // 如果这个connection还在使用(Response还没有读完),就计数然后继续搜索
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++
        } else {
          idleConnectionCount++

          // 这个连接已经空闲,计算它空闲了多久,并且保存空闲了最久的连接
          val idleDurationNs = now - connection.idleAtNs
          if (idleDurationNs > longestIdleDurationNs) {
            longestIdleDurationNs = idleDurationNs
            longestIdleConnection = connection
          } else {
            Unit
          }
        }
      }
    }

    when {
      longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections -> {
        // 如果空闲最久的连接比keepAliveDurationNs这个值要大就回收
        val connection = longestIdleConnection!!
        ...
        // 关闭socket
        connection.socket().closeQuietly()
        if (connections.isEmpty()) cleanupQueue.cancelAll()

        // 我们只回收了空闲超时最久的连接,可能还会有其他连接也超时了,返回0让它立马进行下一次清理
        return 0L
      }

      idleConnectionCount > 0 -> {
        // 如果有空闲连接,就计算最近的一次空闲超时的时间,去等待
        return keepAliveDurationNs - longestIdleDurationNs
      }

      inUseConnectionCount > 0 -> {
        // 如果所有连接都在使用,就等待这个超时时间去重新检查清理
        return keepAliveDurationNs
      }

      else -> {
        // 如果没有连接,就不需要再检查了
        return -1
      }
    }
}

这里面主要是干了这几个事情:

1.遍历缓存池中所有的Connection,根据pruneAndGetAllocationCount计算每个Connection被多少个请求引用决定该Connection是否要进入回收判断逻辑, 如果需要被回收,得到空闲时间最长的Connection和时间

2.对比刚才收集到的最长时间的Connection和keepAlive的时间

3.对比当前空闲的数量和连接池允许的最大的空闲数量

4.对满足23条件的Connection进行Socket断开操作,并且返回0马上进行下一次cleanup回收,因为我们只回收了空闲时间最久的连接

5.如果有空闲连接,但是还没到最大空闲时间,那就返回时间差值,等待这个时间后再次执行cleanup回收

6.如果没有空闲连接,就等待keepAlive时间后再次进行检查

7.如果没有连接,就返回-1不检查了

pruneAndGetAllocationCount返回的是正在占用的请求数,用于检测连接是否空闲,prune有修剪的意思, 除了计算被引用的次数外,内部遍历RealConnection的allocations弱引用列表,修剪并移除掉RealConnection.calls引用计数列表中已经内存回收的RealCall对应的弱引用本身 Refrence,这里巧妙利用了弱引用的原理,类似WeakedHashMap:

 RealConnectionPool
 
 private fun pruneAndGetAllocationCount(connection: RealConnection, now: Long): Int {
    connection.assertThreadHoldsLock()

    val references = connection.calls
    var i = 0
    while (i < references.size) {
      val reference = references[i]

      if (reference.get() != null) {//如果得到的为null,说明弱引用指向的对象本身已经发生了内存泄漏
        i++
        continue
      }

      // We've discovered a leaked call. This is an application bug.
      val callReference = reference as CallReference
      val message = "A connection to ${connection.route().address.url} was leaked. " +
          "Did you forget to close a response body?"
      Platform.get().logCloseableLeak(message, callReference.callStackTrace)

      references.removeAt(i)
      connection.noNewExchanges = true

      // If this was the last allocation, the connection is eligible for immediate eviction.
      if (references.isEmpty()) {
        connection.idleAtNs = now - keepAliveDurationNs
        return 0
      }
    }

    return references.size
  }


什么情况下会发生reference.get()==null的情况呢?

既然前面提到RealCall.releaseConnectionNoEvents中会主动对引用计数进行remove,那什么时候才会发生泄漏的情况呢?比如得到一个Response之后一直不去读取的话实际上它会一直占中这个RealConnection,具体可能是下面的样子:

client.newCall(getRequest()).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 {
    // 啥都不干
  }
});

onResponse传入的response没有人去读取数据,就会一直占用连接,但是由于它在后面又没有人引用就会被GC回收导致这条连接再也不能断开。 pruneAndGetAllocationCount里面就通过弱引用get返回null的方式去检查到这样的异常,进行清理动作。

我们也可以联想到OkHttp常常需要注意的两个问题的原因:

1.Response.string只能调用一次
由于Response.string读取完成之后这次请求其实就已经结束了,而且OkHttp并没有对这个结果做缓存, 所以下次再读取就会出现java.lang.IllegalStateException: closed异常

2.Response必须被及时读取
如果我们得到一个Response之后一直不去读取的话实际上它会一直占中这这个Connect,下次HTTP 1.X的请求就不能复用这套链接,要新建一条Connection

小结: 以上就是对ConnectInterceptor的主要流程分析,主要源于OkHttp源码分析之连接池复用 这篇文章。内容层次清晰,容易懂。

下面是我自己对源码的分析理解:


ConnectInterceptor完整源码分析:

一、ConnectInterceptor拦截器源码分析

ConnectInterceptor 的源码比较简单

    object ConnectInterceptor : Interceptor {
      @Throws(IOException::class)
      override fun intercept(chain: Interceptor.Chain): Response {
        val realChain = chain as RealInterceptorChain
        // 分析1:为即将进行的请求和响应找到一个新的或者从连接池中获取的连接。
        val exchange = realChain.call.initExchange(chain)
        val connectedChain = realChain.copy(exchange = exchange)
        return connectedChain.proceed(realChain.request)
      }
    }
分析1、RealCall.initExchange()
 ---> RealCall
 
 internal fun initExchange(chain: RealInterceptorChain): Exchange {
    // RetryAndFollowUpInterceptor 的 call.enterNetworkInterceptorExchange 方法中初始化 
    // RealCall 中的 exchangeFinder 
    // exchangeFinder 是一个用于查找连接和编解码器的对象,它包含了连接池和路由选择等相关逻辑
    val exchangeFinder = this.exchangeFinder!!
    // 找到链接并通过连接创建 传输解码器
    // 方法作用 1、建立有效链接  2、通过建立的链接创建传输编解码器
    // 通过find获取一个连接的编解码器(codec)。
    // 分析1.1
    val codec = exchangeFinder.find(client, chain)
    // 创建Exchange
    // Exchange: 传输单个HTTP请求和响应对。这将连接管理和事件分层到ExchangeCodec上,后者处理实际的I/O。
    // 将 codec 的操作 包装在 Exchange 实体中。
    // 根据获取的连接和编解码器,创建一个 Exchange 对象,该对象用于管理请求和响应的交换过程。
    // 分析1.2
    val result = Exchange(this, eventListener, exchangeFinder, codec)
    this.interceptorScopedExchange = result
    this.exchange = result
    synchronized(this) {
      this.requestBodyOpen = true
      this.responseBodyOpen = true
    }

    if (canceled) throw IOException("Canceled")
    return result
  }

find() 方法完成了ConnectInterceptor 的两个功能:

  • 找到(创建)有效的连接,并建立连接
  • 通过建立的连接创建 Exchange 管理请求和响应的交换过程。

分析1.1、ExchangeFinder.find

--> ExchangeFinder

fun find(
    client: OkHttpClient,
    chain: RealInterceptorChain
  ): ExchangeCodec {
    try {
        // 1、找到连接
        // 分析1.1.1
      val resultConnection = findHealthyConnection(
          connectTimeout = chain.connectTimeoutMillis,
          readTimeout = chain.readTimeoutMillis,
          writeTimeout = chain.writeTimeoutMillis,
          pingIntervalMillis = client.pingIntervalMillis,
          connectionRetryEnabled = client.retryOnConnectionFailure,
          doExtensiveHealthChecks = chain.request.method != "GET"
      )
      // 2、通过链接创建 Exchange Codec 交换传输编码器
      // 分析1.1.2
      return resultConnection.newCodec(client, chain)
    }

分析1.1.1、寻找连接,建立连接

--> ExchangeFinder

 @Throws(IOException::class)
  private fun findHealthyConnection(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean,
    doExtensiveHealthChecks: Boolean
  ): RealConnection {
    while (true) {
      // 找到候选的连接
      // 分析1.1.1.1 
      val candidate = findConnection(
          connectTimeout = connectTimeout,
          readTimeout = readTimeout,
          writeTimeout = writeTimeout,
          pingIntervalMillis = pingIntervalMillis,
          connectionRetryEnabled = connectionRetryEnabled
      )

      // 确认连接是正常的
      if (candidate.isHealthy(doExtensiveHealthChecks)) {
        return candidate
      }

      // If it isn't, take it out of the pool.
      candidate.noNewExchanges()

      ...
    }
  }
分析1.1.1.1 、寻找和建立连接逻辑

如何找到有效的连接呢?官方给出了寻找连接的逻辑:

作用是为请求获取一个连接。
1、它首先尝试复用已有的连接,
2、然后尝试从连接池获取连接,
3、最后建立新的连接。
在获取连接的过程中,会进行路由选择、连接合并等操作,以提高连接的效率和复用性。


  @Throws(IOException::class)
  private fun findConnection(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean
  ): RealConnection {
    // 检查请求是否已取消
    if (call.isCanceled()) throw IOException("Canceled")

    // 1、尝试复用RealCall已有的连接
    // 此处的连接可能会在 releaseConnectionNoEvents() 中被修改!
    val callConnection = call.connection 
    if (callConnection != null) {
      var toClose: Socket? = null
      synchronized(callConnection) {
        // 如果连接没有新的交换或者与当前请求的主机和端口不匹配,则释放连接
        if (callConnection.noNewExchanges || !sameHostAndPort(callConnection.route().address.url)) {
          toClose = call.releaseConnectionNoEvents()
        }
      }

     // 如果连接没有被释放,则复用连接
      if (call.connection != null) {
        check(toClose == null)
        return callConnection
      }

      // 连接已被释放
      toClose?.closeQuietly()
      eventListener.connectionReleased(call, callConnection)
    }

    // 需要一个新的连接,重置状态数据信息
    refusedStreamCount = 0
    connectionShutdownCount = 0
    otherFailureCount = 0

    // 2、尝试从连接池(ConnectionPool)中获取
    // 分析1.1.1.1.1
    // private val connections = ConcurrentLinkedQueue<RealConnection>()
    if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
      val result = call.connection!!
      eventListener.connectionAcquired(call, result)
      return result
    }

    // 3、连接池中没有可用连接,确定下一个要尝试的路由
    val routes: List<Route>?
    val route: Route
    if (nextRouteToTry != null) {
      // 使用之前合并连接的路由
      routes = null
      route = nextRouteToTry!!
      nextRouteToTry = null
    } else if (routeSelection != null && routeSelection!!.hasNext()) {
      // 使用现有的路由选择中的路由
      routes = null
      route = routeSelection!!.next()
    } else {
      // 计算新的路由选择,这是一个阻塞操作!
      var localRouteSelector = routeSelector
      if (localRouteSelector == null) {
        localRouteSelector = RouteSelector(address, call.client.routeDatabase, call, eventListener)
        this.routeSelector = localRouteSelector
      }
      val localRouteSelection = localRouteSelector.next()
      routeSelection = localRouteSelection
      routes = localRouteSelection.routes
        // 检查请求是否已取消
      if (call.isCanceled()) throw IOException("Canceled")

      // 由于连接合并,现在有了一组 IP地址,再次尝试从连接池获取连接
      if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {
        val result = call.connection!!
        eventListener.connectionAcquired(call, result)
        return result
      }

      route = localRouteSelection.next()
    }

    // 告知请求正在连接的连接,以便异步取消操作生效
    val newConnection = RealConnection(connectionPool, route)
    call.connectionToCancel = newConnection
    try {
      // 分析1.1.1.1.2
      newConnection.connect(
          connectTimeout,
          readTimeout,
          writeTimeout,
          pingIntervalMillis,
          connectionRetryEnabled,
          call,
          eventListener
      )
    } finally {
      call.connectionToCancel = null
    }
    call.client.routeDatabase.connected(newConnection.route())

    // 如果与该主机的其他请求同时进行连接,则合并连接
    if (connectionPool.callAcquirePooledConnection(address, call, routes, true)) {
      val result = call.connection!!
      nextRouteToTry = route
      newConnection.socket().closeQuietly()
      eventListener.connectionAcquired(call, result)
      return result
    }
    // 添加连接到连接池中
    synchronized(newConnection) {
      // 添加连接到连接池中
      connectionPool.put(newConnection)
      // RealCall 和 newConnection关联
      call.acquireConnectionNoEvents(newConnection)
    }

    eventListener.connectionAcquired(call, newConnection)
    return newConnection
  }

这段代码是一个用于查找连接的函数,其主要逻辑如下:

  1. 首先检查请求是否已取消,如果已取消,则抛出IOException。
  2. 尝试重用来自请求的连接,如果存在可重用的连接,则直接返回。
  3. 如果连接池中存在可用连接,从连接池中获取连接并返回。
  4. 如果连接池中不存在可用连接,则确定下一个要尝试的路由。
  5. 如果存在已合并连接的路由,则使用该路由。
  6. 如果存在已存在的路由选择,并且还有未尝试的路由,则使用该路由。
  7. 否则,计算新的路由选择,并获取下一个要尝试的路由。
  8. 进行连接操作,创建新的连接,并进行连接设置。
  9. 如果与该主机的其他请求同时进行连接,则合并连接。
  10. 将新连接放入连接池中,并为请求获取该连接。
  11. 返回新连接。
分析1.1.1.1.1、从连接池中获取连接 connectionPool.callAcquirePooledConnection()

从连接池中获取连接并将call添加到connection 的calls集合中

 -> RealConnectionPool
 
  fun callAcquirePooledConnection(
    address: Address,
    call: RealCall,
    routes: List<Route>?,
    requireMultiplexed: Boolean
  ): Boolean {
    for (connection in connections) {
      synchronized(connection) {
      // http2,如果需要要求使用多路复用的连接(requireMultiplexed 为 true),但该连接不支持多路复用.继续遍历下一个
        if (requireMultiplexed && !connection.isMultiplexed) return@synchronized
        // 查找到符合条件的address
        // 分析1.1.1.1.1.1
        if (!connection.isEligible(address, routes)) return@synchronized
        // 找到复用的 connection 并添加call
        call.acquireConnectionNoEvents(connection)
            // realcall.connection = connection
            // 当前连接承载的所有call
            // connection.calls.add(CallReference(realCall, callStackTrace))
        return true
      }
    }
    return false
  }

分析1.1.1.1.1.1、判断连接是否有效

// 对比连接池中获取的每一个address,找到符合条件的address.


internal fun isEligible(address: Address, routes: List<Route>?): Boolean {
    assertThreadHoldsLock()

    ...
    // 找到符合条件的address ,
    // 对比:this.dns == that.dns &&
        this.proxyAuthenticator == that.proxyAuthenticator &&
        this.protocols == that.protocols
        ...
    if (!this.route.address.equalsNonHost(address)) return false

    // 如果主机名相同则返回 true
    if (address.url.host == this.route().address.url.host) {
      return true // This connection is a perfect match.
    }

   // http2 相关 ----------------
   
    // 到达这一步,我们没有主机名匹配。但如果满足连接合并的要求,仍然可以承载请求。
    // 1. 1. 此连接必须是 HTTP/2。
    if (http2Connection == null) return false

    // 2. 路由必须共享一个 IP 地址。
    if (routes == null || !routeMatchesAny(routes)) return false

    // 3. 此连接的服务器证书必须包含新主机。
    if (address.hostnameVerifier !== OkHostnameVerifier) return false
    if (!supportsUrl(address.url)) return false

    // 4. 证书锁定必须与主机匹配。
    try {
      address.certificatePinner!!.check(address.url.host, handshake()!!.peerCertificates)
    } catch (_: SSLPeerUnverifiedException) {
      return false
    }
    // http2 调用方的地址可以由此连接承载。
    return true 
  }
连接绑定, call 和 connection 绑定赋值,

call.acquireConnectionNoEvents(connection)

--> RealCall
fun acquireConnectionNoEvents(connection: RealConnection) {
    ...
    // RealCall 和 connection关联
    this.connection = connection
    // calls 是 当前connection 承载的所有call 的集合
    connection.calls.add(CallReference(this, callStackTrace))
  }

分析1.1.1.1.2、建立连接 findConnection 的connect

--> RealConnection
 fun connect(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean,
    call: Call,
    eventListener: EventListener
  ) {

    ...

    while (true) {
      try {
        // 检测当前的路由是否通过HTTP代理来实现HTTPS的隧道传输
        if (route.requiresTunnel()) {
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener)
          if (rawSocket == null) {
            // 无法建立隧道连接,但正确关闭了资源。
            break
          }
        } else {
          // 连接socket ,
          connectSocket(connectTimeout, readTimeout, call, eventListener)
        }
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener)
        eventListener.connectEnd(call, route.socketAddress, route.proxy, protocol)
        break
      } catch (e: IOException) {
        // 异常情况重置状态
        socket?.closeQuietly()
        rawSocket?.closeQuietly()
        socket = null
        rawSocket = null
        source = null
        sink = null
        handshake = null
        protocol = null
        http2Connection = null
        allocationLimit = 1
        ... 
      }
    }
  }
连接Socket
--> RealConnection

 private fun connectSocket(
    connectTimeout: Int,
    readTimeout: Int,
    call: Call,
    eventListener: EventListener
  ) {
    val proxy = route.proxy
    val address = route.address
    // 创建Socket 
    val rawSocket = when (proxy.type()) {
      Proxy.Type.DIRECT, Proxy.Type.HTTP -> address.socketFactory.createSocket()!!
      else -> Socket(proxy)
    }
    this.rawSocket = rawSocket

    eventListener.connectStart(call, route.socketAddress, proxy)
    rawSocket.soTimeout = readTimeout
    try {
    // 连接socket
      Platform.get().connectSocket(rawSocket, route.socketAddress, connectTimeout)
    } catch (e: ConnectException) {
      throw ConnectException("Failed to connect to ${route.socketAddress}").apply {
        initCause(e)
      }
    }

    // 获取Socket的读写流,分别封装 读source 写sink
    try {
      source = rawSocket.source().buffer()
      sink = rawSocket.sink().buffer()
    } catch (npe: NullPointerException) {
      if (npe.message == NPE_THROW_WITH_NULL) {
        throw IOException(npe)
      }
    }
  }

分析1.1.2、创建传输编解码管理器

--> RealConnection

internal fun newCodec(client: OkHttpClient, chain: RealInterceptorChain): ExchangeCodec {
    val socket = this.socket!!
    val source = this.source!!
    val sink = this.sink!!
    val http2Connection = this.http2Connection

    return if (http2Connection != null) {
        // 如果是Http2
      Http2ExchangeCodec(client, this, chain, http2Connection)
    } else {
        // Http1 
      socket.soTimeout = chain.readTimeoutMillis()
      source.timeout().timeout(chain.readTimeoutMillis.toLong(), MILLISECONDS)
      sink.timeout().timeout(chain.writeTimeoutMillis.toLong(), MILLISECONDS)
      Http1ExchangeCodec(client, this, source, sink)
    }
  }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/697532.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【每日一题】Leetcode - 10. 正则表达式匹配

题目 Leetcode - 10. 正则表达式匹配 解题思路 预处理正则表达式&#xff0c;合并同项&#xff0c;比如: "a * b * c * . * " -> " . * "更加预处理后的正则表达式&#xff0c;构建NFA通过NFA状态转移条件一个一个匹配字符串字符不匹配的状态要回退匹…

【统一安全管控平台】4A解析

文章目录 一、统一帐号&#xff08;Account&#xff09;二、统一认证(Authentication)三、统一授权(Authorization)四、统一审计(Audit)参考&感谢 4A是指帐号&#xff08;Account&#xff09;、认证&#xff08;Authentication&#xff09;、授权&#xff08;Authorization…

mysql8.0新特性详解

一、my.ini或my.cnf的全局参数 一个连接最少占用内存是256K&#xff0c;最大是64M&#xff0c;如果一个连接的请求数据超过64MB&#xff08;比如排序&#xff09;&#xff0c;就会申请临时空间&#xff0c;放到硬盘上。 #最大连接数 max_connections3000 #最大用户连接数 max_…

智能饮品机器人的市场现状与前景未来,点赋科技与您共同期待

引言&#xff1a; 近年来&#xff0c;智能饮品机器人行业迅猛发展&#xff0c;成为引人瞩目的新兴市场。然而&#xff0c;这个行业的市场现状到底如何&#xff1f;它的前景又将如何发展&#xff1f;让点赋科技带大家一起来看看。 市场现状&#xff1a; 智能饮品机器人市场在过去…

解锁高效部署!快速搭建Kubernetes集群,提升团队生产力!

1 MacOS 1.1 下载 docker-desktop 从 docker 下载 docker-desktop (opens new window)&#xff0c;并完成安装 1.2 启用 k8s 集群 启动 docker-desktop&#xff0c;打开preference 面板 切换到 Kubernetes 标签页&#xff0c;并勾选启动 Enable Kubernetes&#xff0c;点击…

nginx 入门 (b站IT楠老师学习笔记)

文章目录 Nginx 入门 &#xff08;b站IT楠老师学习笔记&#xff09;一 基础了解1、下载nginx安装包2. nginx 可以提供的服务3. nginx 的优点4. 应用场景 二 实战2.1 基础检查以及配置文件结构了解2.2 main全局配置2.3 events模块2.4 http服务器配置2.5虚拟主机配置详解2.5.1 ht…

el-pagination分页查询封装

需求&#xff1a;因为需要用到表单查询的地方太多了&#xff0c;所以为了避免每个页面都写分页组件&#xff0c;直接封装好调用就完事了&#xff0c;简简单单 1.创建Pigination.vue公用组件 <template><div :class"{hidden:hidden}" class"paginatio…

AD中修改一个元器件的所有焊盘大小

1.首先&#xff0c;选中其中一个焊盘。 2.查找相似对象 3.修改component为same 4.修改X size和Y size为same 5.确定 6.修改Pad Stack中的x/y 注&#xff1a;component可以不用改为same&#xff0c;用any就是查找和这个焊盘大小类似的对象。

力扣1768.交替合并字符串(java模拟法)

题目描述&#xff1a; 给你两个字符串 word1 和 word2 。请你从 word1 开始&#xff0c;通过交替添加字母来合并字符串。如果一个字符串比另一个字符串长&#xff0c;就将多出来的字母追加到合并后字符串的末尾。 返回 合并后的字符串 。 问题分析&#xff1a; 我们可以直…

servlet 监听器

做法: 在web.xml中: 或

矩形中的正方形:探究力扣最大正方形问题的奥秘

本篇博客会讲解力扣“1725. 可以形成最大正方形的矩形数目”的解题思路&#xff0c;这是题目链接。 本题的思路是&#xff1a;对于每一个矩形&#xff0c;切分出来的最大正方形的边长是矩形的长和宽的较小值。我们需要维护最大的正方形边长&#xff0c;并统计数目。 我们可以写…

Verilog基础之十、计数器实现

目录 一、前言 二、工程设计 2.1 设计代码 2.2 综合结果 ​2.3 仿真结果 一、前言 计数器是较为基础的逻辑&#xff0c;很多其他逻辑可依靠计数器实现&#xff0c;如控制器&#xff0c;分频。原理为通过统计时钟脉冲的个数来输出计数值。 二、工程设计 2.1 设计代码 工…

实验 09 线性回归与波士顿房价预测

文章目录 实验 09 线性回归与波士顿房价预测一、实验目的二、实验设备三、实验内容3.1 了解数据3.2 分析数据3.3 建立模型&#xff08;一&#xff09;使用一个变量进行预测&#xff08;二&#xff09;使用多元线性回归分析进行预测 实验 09 线性回归与波士顿房价预测 一、实验…

Dubbo【Dubbo实战(整合Mybaits-plus配置、接口、集成Thymeleaf) 】(五)-全面详解(学习总结---从入门到深化)

目录 ​编辑 Dubbo实战_整合Mybaits-plus配置 Dubbo实战_创建添加用户接口 Dubbo实战_查询用户业务接口 Dubbo实战_更新用户业务接口 Dubbo实战_删除用户业务接口 Dubbo实战_集成Thymeleaf Dubbo实战_用户添加业务消费者实现 Dubbo实战_用户查询业务消费者实现 Dubbo实战_…

R带参数运行

rm(listls()) suppressPackageStartupMessages({library(getopt) })# getopt(),是getopt包的函数&#xff0c;需要先按照getopt包 # # getopt(spec NULL, opt commandArgs(TRUE),command get_Rscript_filename(), usage FALSE,debug FALSE) # # spec&#xff1a;一个4或…

linux suse12 安装mysql

1.下载mysql https://dev.mysql.com/downloads/mysql/ 2.选中mysql版本 对选中的版本进行下载 也可以在linux服务器上直接下载&#xff1a;wget -c https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.21-linux-glibc2.12-x86_64.tar.xz 3.把下载好的文件放到/usr/local…

mac上的vscode设置用滚轮来调节编辑页面大小

mac上的vscode设置用滚轮来调节编辑页面大小 问题背景 今天在mac上安装了vscode&#xff0c;刚想编写代码&#xff0c;但是突然发现无法用ctrl鼠标的滚轮来进行控制 代码编辑区域字体的大小。 解决方法 经过查找资料&#xff0c;发现在设置的配置文件中&#xff0c;有一个配…

前后端分离模式开发的BS电子病历编辑器源码(Java)

电子病历系统&#xff0c;是医学专用系统。医院通过电子病历以电子化方式记录患者就诊的信息&#xff0c;包括&#xff1a;首页、病程记录、检查检验结果、医嘱、手术记录、护理记录等等&#xff0c;其中既有结构化信息&#xff0c;也有非结构化的自由文本&#xff0c;还有图形…

HOT19-螺旋矩阵

leetcode原题链接&#xff1a;螺旋矩阵 题目描述 给你一个 m 行 n 列的矩阵 matrix &#xff0c;请按照 顺时针螺旋顺序 &#xff0c;返回矩阵中的所有元素。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,9]] 输出&#xff1a;[1,2,3,6,9,8,7,4,5]示例…

通过USB和wifi连接真机编写第一个脚本

目录 一、连接手机 1、通过usb数据线连接手机 2、无线连接手机 二、编写第一个脚本 一、连接手机 1、通过usb数据线连接手机 数据线连接手机并允许调试 cmd命令行执行&#xff1a; adb devices 如果没有显示device信息&#xff0c;请检查&#xff1a; 手机是否开启usb调…