OkHttp源码分析(2)

·  阅读 193

OkHttp源码分析(2)


前言

OkHttp最重要的两个技术线程复用,连接复用,线程复用体现在Dispatch类中,在OkHttp源码分析(1)中已经分析,而连接复用则体现在OkHttp拦截器中。

承接上篇文章,OkHttp的核心是拦截器,一系列的拦截链通过责任链的设计模式实现了上下文的传递与整体功能的衔接。在RealCall类中getResponseWithInterceptorChain()中,可以看到这个责任链的整体结构

  internal fun getResponseWithInterceptorChain(): Response {
    // Build a full stack of interceptors.
    val interceptors = mutableListOf<Interceptor>()
    interceptors += client.interceptors
    interceptors += RetryAndFollowUpInterceptor(client)
    interceptors += BridgeInterceptor(client.cookieJar)
    interceptors += CacheInterceptor(client.cache)
    interceptors += ConnectInterceptor
    if (!forWebSocket) {
      interceptors += client.networkInterceptors
    }
    interceptors += CallServerInterceptor(forWebSocket)
  	...
  }
复制代码

其中,ConnectInterceptor拦截器主要实现 Http连接的相关功能,CallServerInterceptor拦截器则实现服务访问的相关功能,今天主要分析这两个拦截器,并且最终得到如下两个问题的结论

  • OkHttp是如实现Http连接的;
  • OkHttp是如何实现连接复用的;

CallServerInterceptor

CallServerInterceptor处于责任链的底端,作为最后一个拦截器,他主要是实现服务器的访问,实质是将http报文写入连接流中,相应的将响应报文从流中读取出来,形成响应。

写请求报文
try {
      // 写入报文的起始行与首部
      exchange.writeRequestHeaders(request)
	  // 通过HTTP方法判断是否有请求体,GET与HEAD请求是不存在请求体的
      if (HttpMethod.permitsRequestBody(request.method) && requestBody != null) {
        // 有请求体需要写入
         
        // 有些服务器,当请求体超过1024字节的情况下,直接访问不会响应,此时,访问分两个步骤
        // 先发送携带 “Expect:100-continue”的请求头,询问是否接受数据
        // 当服务端应答后,再把请求体发送给服务端;  
        if ("100-continue".equals(request.header("Expect"), ignoreCase = true)) {
            // 当存在上述情况时,先把特定的首部内容“Except:100-continue”发送服务端
            exchange.flushRequest()
          // 发送后,读取服务端的响应  
          responseBuilder = exchange.readResponseHeaders(expectContinue = true)
          exchange.responseHeadersStart()
          invokeStartEvent = false
        }

        if (responseBuilder == null) {
          // 第一种情况,服务端正常响应了"Expect:100-continue" ,下面可以发送请求体
          if (requestBody.isDuplex()) {
            // 判断是否支持双共通信,也就是HTTP/2协议,双共通信,读写互不影响 
            exchange.flushRequest()
            // 此时可以马上写入请求体  
            val bufferedRequestBody = exchange.createRequestBody(request, true).buffer() 
            requestBody.writeTo(bufferedRequestBody)
          } else {
            // 不支持双工通信,此时为HTTP/1 协议  
            val bufferedRequestBody = exchange.createRequestBody(request, false).buffer()
            // 写入请求提
            requestBody.writeTo(bufferedRequestBody)
            bufferedRequestBody.close()
          }
        } else {
          // 第二种情况,服务段没有响应“Expect:100-continue”,或者响应错误码;  
          exchange.noRequestBody()
          if (!exchange.connection.isMultiplexed) {
            // 没有响应,并且不支持多路复用,此处应该关闭该连接;
            exchange.noNewExchangesOnConnection()
          }
        }
      } else {
        // 不需要写入请求体  
        exchange.noRequestBody()
      }
	  // 不存在请求体,或者不支持双共通信,都关闭连接(exchange字段为false,表示不再有数据交互)
      if (requestBody == null || !requestBody.isDuplex()) {
        exchange.finishRequest()
      }
    } catch (e: IOException) {
      if (e is ConnectionShutdownException) {
        throw e 
      }
      if (!exchange.hasFailure) {
        throw e 
      }
      sendRequestException = e
    }
复制代码

Http报文如下

POST Https://xxx.xxx HTTP/1.1            -----首行

Accept:application/json                  -----请求头
Accept-Language: zh-CN

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx         -----请求体
复制代码

写入报文的第一步是通过下面方法写入首行与请求头

exchange.writeRequestHeaders(request)
复制代码

exchange对象负责Http连接的数据交互。这个对象里面包含了一个ExchangeCodec的实例,并且主要负责Http请求响应的编解码。exchangewriteRequestHeaders就是调用该对象

  fun writeRequestHeaders(request: Request) {
    try {
      eventListener.requestHeadersStart(call)
      // ExchangeCode 写入首行请求头
      codec.writeRequestHeaders(request)
      eventListener.requestHeadersEnd(call, request)
    } catch (e: IOException) {
      eventListener.requestFailed(call, e)
      trackFailure(e)
      throw e
    }
  }
复制代码

HTTP/1协议中,writeRequestHeadersStart实现如下

  override fun writeRequestHeaders(request: Request) {
    val requestLine = RequestLine.get(request, connection.route().proxy.type())
    writeRequest(request.headers, requestLine)
  }
复制代码

其中,RequestLine.get方法如下

  fun get(request: Request, proxyType: Proxy.Type) = buildString {
    append(request.method)
    append(' ')
    if (includeAuthorityInRequestLine(request, proxyType)) {
      append(request.url)
    } else {
      append(requestPath(request.url))
    }
    append(" HTTP/1.1")
  }
复制代码

最终会返回如下的大概如下的字符串

// 方法   URL             协议
   POST  Http://xxx.xxx  HTTP/1.1  
复制代码

writeRequest方法如下

  fun writeRequest(headers: Headers, requestLine: String) {
    check(state == STATE_IDLE) { "state: $state" }
    sink.writeUtf8(requestLine).writeUtf8("\r\n")
    for (i in 0 until headers.size) {
      sink.writeUtf8(headers.name(i))
          .writeUtf8(": ")
          .writeUtf8(headers.value(i))
          .writeUtf8("\r\n")
    }
    sink.writeUtf8("\r\n")
    state = STATE_OPEN_REQUEST_BODY
  }
复制代码

最终会返回如下的字符串

Accept:application/json                  -----请求头
Accept-Language: zh-CN
....
复制代码

这两者最终组成了请求头。

需要注意的是在writeRequest方法中出现的sink对象,它属于okio中的类,还有跟sink对标的一个对象source,这两个跟java/io中的outputStringinputString具有相同的功能。此处的sink对应的是RealConnection中的写入流,这意味着,他将直接写入Socket通道,这后面会讲。

在写完请求头后,则需要写入请求体。但是在此之前,必须先排除不存在请求体的情况。HEADGET请求是没有请求体的。

请求有种特殊的情况存在,在HTTP/1协议下,有些服务器的在请求负载超过1024字节情况下会拒绝访问,此时,客户端需要将访问分为两步

  1. 发送Expect:100-continue请求给服务端,询问是否接收数据;
  2. 服务端响应接受数据后,再将真正的请求体发送服务端;
if ("100-continue".equals(request.header("Expect"), ignoreCase = true)) {
          // 将包含`Expect:100-continue`的请求头发送给服务器
          exchange.flushRequest()
          // 读取读取服务端的响应
          responseBuilder = exchange.readResponseHeaders(expectContinue = true)
          exchange.responseHeadersStart()
          invokeStartEvent = false
}
复制代码

通过exchange.readResponseHeaders读取服务端的响应,如果服务端响应了Expect:100-continue的请求,则返回null。后续针对该情况,写入请求体,其中还设计到流是否支持双共通信的判断。需要注意的是,如果在服务端没有响应Expect:100-continue的情况下,并且建立的连接不支持多路复用,也就是仅支持HTTP/1.0协议,此时,因该关闭该流。因为没有存在的意义。

读响应报文

读取响应报文跟写请求报文是相反的过程。但是思路是一样的

 try {
      if (responseBuilder == null) {
        // 读取响应首行与响应头  
        responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
        if (invokeStartEvent) {
          exchange.responseHeadersStart()
          invokeStartEvent = false
        }
      }
      // 构建response,把读取的响应头放入response对象中
      var response = responseBuilder
          .request(request)
          .handshake(exchange.connection.handshake())
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build()
      var code = response.code
      if (code == 100) {
		// 响应码为100,说明是`Expect:100-continue`的响应,则再次读取响应头。
        // 因为服务气响应后,客户端回发送真正的请求。  
        responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
        if (invokeStartEvent) {
          exchange.responseHeadersStart()
        }
        // 把响应头放入response对象中  
        response = responseBuilder
            .request(request)
            .handshake(exchange.connection.handshake())
            .sentRequestAtMillis(sentRequestMillis)
            .receivedResponseAtMillis(System.currentTimeMillis())
            .build()
        code = response.code
      }

      exchange.responseHeadersEnd(response)

      response = if (forWebSocket && code == 101) {
        // 如果为websocket协议,HTTP状态码101,此时服务端理解客户端,并且切换到websocket协议;  
        response.newBuilder()
            .body(EMPTY_RESPONSE)
            .build()
      } else {
        // 读取响应体  
        response.newBuilder()
            .body(exchange.openResponseBody(response))
            .build()
      }
      // 如果响应行包含"Conection:close",则说明服务端不再继续长链接,主动断开TCP通道
      // 此时该连接已经丧失数据交互功能。
      if ("close".equals(response.request.header("Connection"), ignoreCase = true) ||
          "close".equals(response.header("Connection"), ignoreCase = true)) {
        exchange.noNewExchangesOnConnection()
      }
      // HTTP204表示服务器成功处理了请求,不返回任何实体内容,仅仅返回跟新的元信息(响应头)
      // HTTP205表示服务器成功处理了请求,不返回任何实体内容,并且希望客户端重置视图
      // 两种状态都是不存在响应体
      if ((code == 204 || code == 205) && response.body?.contentLength() ?: -1L > 0L) {
        throw ProtocolException(
            "HTTP $code had non-zero Content-Length: ${response.body?.contentLength()}")
      }
      // 返回response
      return response
    } catch (e: IOException) {
      if (sendRequestException != null) {
        sendRequestException.addSuppressed(e)
        throw sendRequestException
      }
      throw e
    }
复制代码

先读取响应首行与响应头

responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
复制代码

该方法依然是调用Exchange类中的ExchangeCodec实例方法readResponseHeaders

  @Throws(IOException::class)
  fun readResponseHeaders(expectContinue: Boolean): Response.Builder? {
    try {
      // codec 负责报文的编解码;  
      val result = codec.readResponseHeaders(expectContinue)
      result?.initExchange(this)
      return result
    } catch (e: IOException) {
      eventListener.responseFailed(call, e)
      trackFailure(e)
      throw e
    }
  }
复制代码

在读请求报文时,分析的是HTTP/1的协议,此处分析HTTP/2协议,readResponseHeaders的实现如下

 // Http2ExchangeCodec.kt
 override fun readResponseHeaders(expectContinue: Boolean): Response.Builder? {
    val stream = stream ?: throw IOException("stream wasn't created")
    val headers = stream.takeHeaders()
    val responseBuilder = readHttp2HeadersList(headers, protocol)
    return if (expectContinue && responseBuilder.code == HTTP_CONTINUE) {
      null
    } else {
      responseBuilder
    }
  }
复制代码

此处读取的对象为streamHTTP/2支持多路复用,也就是流复用,所以此处用到stream也就不难理解。

同时,HTTP/2报文请求头都是以键值对的方式组织,所以上述的实现中没有像HTTP/1中的专门读取首行的方法。而是简单的调用stream.takeHeaders(),需要说明的是该方法是阻塞的,并且甚至了超时限制,具体细节可以查看实现。在读取响应头后,下一步组织响应头,调用readHttp2HeadersList

  /** Returns headers for a name value block containing an HTTP/2 response. */
    fun readHttp2HeadersList(headerBlock: Headers, protocol: Protocol): Response.Builder {
      var statusLine: StatusLine? = null
      val headersBuilder = Headers.Builder()
      for (i in 0 until headerBlock.size) {
        // key-value 类似于字典一般的参数组织形式  
        val name = headerBlock.name(i)
        val value = headerBlock.value(i)
        // `:status:`在HTTP/2中表示状态码  
        if (name == RESPONSE_STATUS_UTF8) {
          statusLine = StatusLine.parse("HTTP/1.1 $value")
        } else if (name !in HTTP_2_SKIPPED_RESPONSE_HEADERS) {
          // HTTP/2明确不再支持一些响应头;
          headersBuilder.addLenient(name, value)
        }
      }
      if (statusLine == null) throw ProtocolException("Expected ':status' header not present")

      return Response.Builder()
          .protocol(protocol)
          .code(statusLine.code)
          .message(statusLine.message)
          .headers(headersBuilder.build())
    }
复制代码

Stream读取的报文是以类似与字典键值对的方式存储的。其中需要注意的两个点:

  1. HTTP/2一些特殊的响应头的表示
":status" 表示响应状态
":method": 请求方法
":path": 请求路径
":scheme" 请求的协议
":authority" 请求的认证状态
复制代码
  1. HTTP/2不再支持一些特殊的响应头,具体如下
	"connection"
	"host"
    "keep-alive"
	"proxy-connection"
	"transfer-encoding"
  	"te"
	"encoding"
	"upgrade"
复制代码

以上两点均是基于HTTP/2采用头部压缩的特性。最后,解析出的报文如下

HTTP/1.1 200 OK

Date:Tue, 10 Jul 2020 06:40:22 GMT
Content-Length:1024
.....
复制代码

读取出报文后,需要进一步读取响应体,此时存在一些特殊的情况

if (code == 100) {
		// 响应码为100,说明是`Expect:100-continue`的响应,则再次读取响应头。
        // 因为服务气响应后,客户端回发送真正的请求。  
        responseBuilder = exchange.readResponseHeaders(expectContinue = false)!!
        if (invokeStartEvent) {
          exchange.responseHeadersStart()
        }
        // 把响应头放入response对象中  
        response = responseBuilder
            .request(request)
            .handshake(exchange.connection.handshake())
            .sentRequestAtMillis(sentRequestMillis)
            .receivedResponseAtMillis(System.currentTimeMillis())
            .build()
        code = response.code
      }
复制代码

如果HTTP响应码为100,说明服务端希望客户端继续发送请求,此时的情况对应请求头中包含Expect:100-continue的情况,响应码100就是对该请求头的响应,在该响应触发后,客户端会发出包含数据的真正请求。此时,应该继续读取后续的响应,所以上述代码在分析出此种情况后,会重复exchange.responseHeadersStart的操作。

在读取完响应头后,此时可以继续读取响应体。此时存在一种特殊情况,在使用WebSocket协议时候,服务端在接收到请求时候,会先返回101的状态码,表示服务端已经响应请求,后续将升级协议,使用客户端要求的WebSocket。此时,返回101的响应是不包含响应体的。

      response = if (forWebSocket && code == 101) {
        // 如果为websocket协议,HTTP状态码101,此时服务端理解客户端,并且切换到websocket协议;  
        response.newBuilder()
            .body(EMPTY_RESPONSE)
            .build()
      } else {
        // 读取响应体  
        response.newBuilder()
            .body(exchange.openResponseBody(response))
            .build()
      }
复制代码

读取响应体后,如果服务断关闭连接,即响应头中包含Connection:close,则表示该连接不在具备数据交互能力,此时应该释放该连接。

      // 如果响应行包含"Conection:close",则说明服务端不再继续长链接,主动断开TCP通道
      // 此时该连接已经丧失数据交互功能。
      if ("close".equals(response.request.header("Connection"), ignoreCase = true) ||
          "close".equals(response.header("Connection"), ignoreCase = true)) {
        exchange.noNewExchangesOnConnection()
      }
复制代码

最后,处理一些特殊的HTTP状态码,204205均表示响应不包含响应体的情况。

到目前为止,该拦截器完成与服务器数据交互的完整功能。

ConnectInterceptor

CallServerInterceptor拦截器的分析中,可以发现,所有报文的I/O操作都是通过Exchange类中的ExchangeCodec的实例。而所有的报文最终都是通过HTTP连接与服务端交互的,因此,确认了责任链中的Exchange实例的来源就可以确认Http连接的相关信息。那ConnectInterceptor中使用的Exchange实例到底从何而来?回溯责任链,CallServerInterceptor是责任链的底端,他的上层链ConnectInterceptor逻辑如下

object ConnectInterceptor : Interceptor {
  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val realChain = chain as RealInterceptorChain
    // 获得Exchange对象
    val exchange = realChain.call.initExchange(chain)
    // 把Exchange对象传入责任链  
    val connectedChain = realChain.copy(exchange = exchange)
    // 调用下个拦截器
    return connectedChain.proceed(realChain.request)
  }
}
复制代码

该拦截器的功能非常明确,通过realCall.initExchange方法获得了Exchange的实例,将此对象加入传入责任链上下文,传递给下个拦截器,CallServerInterceptor中调用的Exchange对象即来源于此。

realChain.call.initExchange初始化了一个数据交换对象Exchange

  // RealCall.kt
  internal fun initExchange(chain: RealInterceptorChain): Exchange {
    ...
    val exchangeFinder = this.exchangeFinder!!
    //  ExchangeFinder对象主要负责查找匹配的连接 
    val codec = exchangeFinder.find(client, chain)
    // 新建了一个Exchange实例  
    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
  }
复制代码

该方法最终会返回一个新建的Exchange实例,该实例需要一个ExchangeCodec对象,ExchangeCodec的主要功能在CallServerInterceptor中已经分析过,主要将报文写入连接中。而ExchangeCodec对象是通过ExchangFinder对象的find的方法。其实查找ExchangeCodec实例的过程就是查找Connection的过程。同时需要明确的是ExchangeFinder类的核心功能就是根据请求查找Connection

查看ExchangeFinderfind方法

fun find(
    client: OkHttpClient,
    chain: RealInterceptorChain
  ): ExchangeCodec {
    try {
      // 查找连接  
      val resultConnection = findHealthyConnection(
          connectTimeout = chain.connectTimeoutMillis,
          readTimeout = chain.readTimeoutMillis,
          writeTimeout = chain.writeTimeoutMillis,
          pingIntervalMillis = client.pingIntervalMillis,
          connectionRetryEnabled = client.retryOnConnectionFailure,
          doExtensiveHealthChecks = chain.request.method != "GET"
      )
      // 通过连接,返回一个ExchangeCodec实例  
      return resultConnection.newCodec(client, chain)
    } catch (e: RouteException) {
      trackFailure(e.lastConnectException)
      throw e
    } catch (e: IOException) {
      trackFailure(e)
      throw RouteException(e)
    }
  }
复制代码

find方法会调用findHealthyConnection找到健康的HTTP连接,然后通过

resultConnection.newCodec(client, chain)
复制代码

返回一个ExchangeCodec实例,查看newCodec方法

// RealConnection.kt  
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) {
      Http2ExchangeCodec(client, this, chain, http2Connection)
    } else {
      socket.soTimeout = chain.readTimeoutMillis()
      source.timeout().timeout(chain.readTimeoutMillis.toLong(), MILLISECONDS)
      sink.timeout().timeout(chain.writeTimeoutMillis.toLong(), MILLISECONDS)
      Http1ExchangeCodec(client, this, source, sink)
    }
  }
复制代码

Http1ExchangeCodecHttp2ExchangeCodec均实现了ExchangeCodec接口,他们分别是针对HTTP/1HTTP/2协议实现的对ConnectionI/O,HTTP/2协议实现了流的复用,所以,传递的参数是Http2Connection,而HTTP/1传递的是sourcesink。这两者是okio中的类,分别为ConnectionSocket连接的输入流与输出流。在CallServerInterceptor拦截器中对报文的读写操作,最终都是使用上述三种对象。

回到正文,此处的关键应该是findHealthyConnection方法,找到健康的连接。

private fun findHealthyConnection(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean,
    doExtensiveHealthChecks: Boolean
  ): RealConnection {
    while (true) {
        
      // 找到候选(备选)的连接  
      val candidate = findConnection(
          connectTimeout = connectTimeout,
          readTimeout = readTimeout,
          writeTimeout = writeTimeout,
          pingIntervalMillis = pingIntervalMillis,
          connectionRetryEnabled = connectionRetryEnabled
      )

      // 确认候选的连接是否是健康的
      if (candidate.isHealthy(doExtensiveHealthChecks)) {
        return candidate
      }

      // 如果不健康,则移除出连接池
      candidate.noNewExchanges()
		
      // 如果线程池中找不到符合条件的连接,则不断尝试新的路由  
      if (nextRouteToTry != null) continue

      val routesLeft = routeSelection?.hasNext() ?: true
      if (routesLeft) continue

      val routesSelectionLeft = routeSelector?.hasNext() ?: true
      if (routesSelectionLeft) continue

      throw IOException("exhausted all routes")
    }
  }
复制代码

findHealtyConnection方法通过开启一个无限循环,要么找到符合条件的连接,要么触发异常。在找到连接后,通过isHealthy方法确认连接是有效可用的,如果找不到连接,则通过不断的调整连接的路由,多次去尝试,如果所有的路由都已经尝试过,则触发IOException,避免死循环。上述的逻辑中,关键在于如何找到备选的路由,通过findConnection方法,该方法比较长,内部逻辑比较复杂,却是核心。

private fun findConnection(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean
  ): RealConnection {
    if (call.isCanceled()) throw IOException("Canceled")

    // No1:查看请求本身是否存在连接,如果存在并且可用,则使用请求本身的连接,第一步是承上启下的作用
    val callConnection = call.connection 
    if (callConnection != null) {
      var toClose: Socket? = null
      synchronized(callConnection) {
        if (callConnection.noNewExchanges || !sameHostAndPort(callConnection.route().address.url)) {
          // 在CallServerInterCeptor拦截器中,经常通过设置noNewExchange来关闭连接
          // 如果连接不具备交互功能,或者连接路由不同,则释放该连接;  
          toClose = call.releaseConnectionNoEvents()
        }
      }

      // 如果请求原来的连接符合条件,则复用该连接
      if (call.connection != null) {
        check(toClose == null)
        return callConnection
      }

      // 不具备交互功能的连接,关闭socket;
      toClose?.closeQuietly()
      eventListener.connectionReleased(call, callConnection)
    }

    // No2:如果请求没有连接,则尝试在连接迟中查找可以复用的连接;
    
    // 重置状态
    refusedStreamCount = 0
    connectionShutdownCount = 0
    otherFailureCount = 0

    // 从连接池中查找可用的连接,注意此时传入的路由为null,这很重要
    if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
      // 在查找的过程中,如果找到可用路由,会直接赋值给call.connection,所以下面的语句是安全的  
      val result = call.connection!!
      eventListener.connectionAcquired(call, result)
       // 如果连接池中存在符合条件的可复用路由,直接返回。 
      return result
    }

    // No3:如果连接池中暂时找不到可用路由,则不断调整路由,继续从连接池中查找
    val routes: List<Route>?
    val route: Route
    if (nextRouteToTry != null) {
      // Use a route from a preceding coalesced connection.
      routes = null
      route = nextRouteToTry!!
      nextRouteToTry = null
    } else if (routeSelection != null && routeSelection!!.hasNext()) {
      // Use a route from an existing route selection.
      routes = null
      route = routeSelection!!.next()
    } else {
      // Compute a new route selection. This is a blocking operation!
      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")

	  // 调整路由后,routers为一系列的地址,此时,再在连接池中查看是否有匹配的连接。
      if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {
        val result = call.connection!!
        eventListener.connectionAcquired(call, result)
        return result
      }

      route = localRouteSelection.next()
    }

    // NO4:确认连接池中没有匹配的路由,只有重新创建一条新的连接;
    val newConnection = RealConnection(connectionPool, route)
    // 连接创建成功后,是直接赋值给call的。
    call.connectionToCancel = newConnection
    try {
      newConnection.connect(
          connectTimeout,
          readTimeout,
          writeTimeout,
          pingIntervalMillis,
          connectionRetryEnabled,
          call,
          eventListener
      )
    } finally {
      call.connectionToCancel = null
    }
    // 此处,routeDatabase是一个连接黑名单,如果创建的连接在黑名单之内,则将此连接从黑名单中移除
    call.client.routeDatabase.connected(newConnection.route())

	// NO5:连接的创建也是具备竟性条件的
    // 连接创建成功后,在放入连接池之前,又去遍历连接池中是否存在符合条件的连接,
    // 如果存在,则说明在创建新连接过程中,已经有人抢先一步。此时,将先创建的链接释放掉。
    // 并且返回那个抢先一步创建的连接;
    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)
      call.acquireConnectionNoEvents(newConnection)
    }

    eventListener.connectionAcquired(call, newConnection)
    return newConnection
  }
复制代码
查找连接

查找连接的整体流程图如下

graph TD
start([start])--> call{请求是否存在连接}
call--存在--> callUseful{是否具备交互能力}
callUseful --具备-->return[返回连接]
callUseful --不具备-->connectionRelease[释放连接关闭Socket]
call--不存在-->connectionPool[连接池中查找符合条件的连接]
connectionRelease-->connectionPool
connectionPool --> poolFinder{存在符合条件的连接}
poolFinder --存在-->return
poolFinder--不存在-->changeRoute[调整路由]
changeRoute-->reConnectionPool{连接池中查找符合条件的路由}
reConnectionPool --存在-->return
reConnectionPool --不存在-->buildConnection[创建新的连接]
buildConnection-->3thConnectionPool[连接池中查找符合条件的连接]
3thConnectionPool-->3poolFinder{存在符合条件的连接}
3poolFinder--存在-->destroyNewConnection[销毁新创建的连接]
destroyNewConnection-->return
3poolFinder--不存在-->savePool[存放到线程池]
savePool-->return
return --> over([END])







整体流程图如上所示。下面具体分析

  1. 先从请求中查找连接。
    // No1:查看请求本身是否存在连接,如果存在并且可用,则使用请求本身的连接,第一步是承上启下的作用
    val callConnection = call.connection 
    if (callConnection != null) {
      var toClose: Socket? = null
      synchronized(callConnection) {
        if (callConnection.noNewExchanges || !sameHostAndPort(callConnection.route().address.url)) {
          // 在CallServerInterCeptor拦截器中,经常通过设置noNewExchange来关闭连接
          // 如果连接不具备交互功能,或者连接路由不同,则释放该连接;  
          toClose = call.releaseConnectionNoEvents()
        }
      }

      // 如果请求原来的连接符合条件,则复用该连接
      if (call.connection != null) {
        check(toClose == null)
        return callConnection
      }

      // 不具备交互功能的连接,关闭socket;
      toClose?.closeQuietly()
      eventListener.connectionReleased(call, callConnection)
    }
复制代码

为什么要先确认call本身是否已经存在可用连接的情况,我认为这是一个承上启下的作用。如果请求本身已经存在了连接,则使用请求已经存在的连接,如果连接已经不具备交互能力,则释放该连接,并且关闭Socket通道。为什么Call只能被执行一次的情况下会已经存在连接?需要明确的是Call确实是只能被执行一次,但是执行之前已经存在连接的情况确实存在。那就是是请求头中携带Connect:100-continue的情况,在CallServerInterceptor拦截器的代码中已经分析过此种情况了,该请求会分为两个步骤,先向服务端发送请求头,如果服务端响应了100则继续发送请求体。在HTTP/1协议下,连接不可复用,通过设置Connection.onNewChanges =true来关闭连接。关闭的动作就在此处执行。而HTTP/2协议具备连接复用,则可继续使用原来的连接发送请求体,这种情况就是Call已经具备了连接的情况,这里的连接就是发送请求头可以复用的连接。我感觉这里释放连接的意义更大!

  1. 从连接池中查找

Call中已经存在可用连接的情况很少,第二步则去连接池中查找。

    // 从连接池中查找可用的连接,注意此时传入的路由为null,这很重要
    if (connectionPool.callAcquirePooledConnection(address, call, null, false)) {
      // 在查找的过程中,如果找到可用路由,会直接赋值给call.connection,所以下面的语句是安全的  
      val result = call.connection!!
      eventListener.connectionAcquired(call, result)
       // 如果连接池中存在符合条件的可复用路由,直接返回。 
      return result
    }
复制代码

连接池中查找连接的调用方法为connectionPool.callAcquirePooledConnection,该方法需要传入四个参数:地址,请求,路由列表,是否支持多路复用。注意,此处,传入的路由列表为null,看一下具体的方法

// RealConnectionPool
fun callAcquirePooledConnection(
    address: Address,
    call: RealCall,
    routes: List<Route>?,
    requireMultiplexed: Boolean
  ): Boolean {
    // 遍历连接池
    for (connection in connections) {
      synchronized(connection) {
        // 排除不符合多路复用的条件  
        if (requireMultiplexed && !connection.isMultiplexed) return@synchronized
        // 排除不合格的连接  
        if (!connection.isEligible(address, routes)) return@synchronized
        // 找到合格的连接后,直接将连接赋值给请求call  
        call.acquireConnectionNoEvents(connection)
        return true
      }
    }
    return false
  }
复制代码

在方法里会遍历连接池,for循环中的connections就是复用的连接池。针对多路复用的条件,先排除部分连接,在通过isEligible实例方法,确认连接的有效性。

// RealConnection.kt
internal fun isEligible(address: Address, routes: List<Route>?): Boolean {
    assertThreadHoldsLock()
	// 此处默认一个连接同时只能被一个Call对象引用,如果引用对象超过限制,则不合格
    // 如果该连接不再具备数据交互能力,也不合格
    if (calls.size >= allocationLimit || noNewExchanges) return false

    // 如果非主机字段不完全一致,也是不符合条件的
    // 非主机字段指的是:协议、代理、端口、认证等等
    if (!this.route.address.equalsNonHost(address)) return false

    // 主机地址一样,并且非主机字段也一样,则完全匹配,最理想的情况
    if (address.url.host == this.route().address.url.host) {
      return true
    }
	
	// 如果主机名不一致,则查看是否满足合并请求的条件
    // 1. 必须是HTTP/2 协议
    if (http2Connection == null) return false

    // 2. 所有路由必须使用相同的地址,并且不使用代理,因为代理无法知道真正主机的地址
    if (routes == null || !routeMatchesAny(routes)) return false

    // 3. 主机名验证,符合规定的URI格式
    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
    }

    return true // The caller's address can be carried by this connection.
  }
复制代码

连接池中连接可以具备复用的的情况分为两种

  1. 具备直接使用的连接,该情况为最完美的情况
  • 该连接的存在的请求在允许范围内,并且该连接具备数据交互能力
  • 连接的主机名与请求的主机名相同,非主机字段也相同
  1. 具备合并请求的连接,该情况可以将新请求合并在此连接中
  • 必须是HTTP/2协议
  • 请求中的所有路由必须是相同的地址,并且没有使用代理,因为代理无法知道真实服务器的地址
  • 请求中的主机名验证必须有效,符合相关规范
  • 请求中主机名与证书中的身份必须匹配

其中具体的要求可以参考HTTP相关文档,不做具体讨论。符合上述两种情况则说明连接是合格的。则可以复用该连接,并且调用call.acquireConnectionNoEvents方法直接将该连接附加在Call对象上。

 // RealCall.kt 
 fun acquireConnectionNoEvents(connection: RealConnection) {
    connection.assertThreadHoldsLock()
    check(this.connection == null)
    // 设置Call的connection属性 
    this.connection = connection
    connection.calls.add(CallReference(this, callStackTrace))
  }
复制代码

从连接池中查找连接的方法分析完了,但是该方法会被执行多次。后续还会多次进入到该方法中。如果连接池中存在符合条件的连接,则直接返回连接,否则进入下一步。

  1. 调整路由后,查找具备合并请求条件的连接

其实,上一步在线程池中查找可用连接是查找地址完全匹配的可用连接,即主机名和非主机字段都匹配的情况。如果这种完美情况无法实现,则下一部尝试进行符合请求合并的连接,所以在第二步中传入的路由参数为null,这一步主要是调整路由参数后,继续在线程池中查找。

   //ExchangeFinder.kt
    val routes: List<Route>?
    val route: Route
    if (nextRouteToTry != null) {
      // Use a route from a preceding coalesced connection.
      routes = null
      route = nextRouteToTry!!
      nextRouteToTry = null
    } else if (routeSelection != null && routeSelection!!.hasNext()) {
      // Use a route from an existing route selection.
      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")
	  // 在具备一系列的地址后,再次去连接池中查找符合条件的连接,逻辑都是跟上面一样的
      if (connectionPool.callAcquirePooledConnection(address, call, routes, false)) {  
        val result = call.connection!!
        eventListener.connectionAcquired(call, result)
        return result
      }
      route = localRouteSelection.next()
    }
复制代码

上述的情况针对的是用户设置代理的情况。在一开始阶段,因为不存在路由对象,所以会通过实例化一个RouterSelector对象,获取相关的路由列表。

RouterSelector类中,会初始化一个方法

  init {
    resetNextProxy(address.url, address.proxy)
  }

复制代码

该方法如下

  private fun resetNextProxy(url: HttpUrl, proxy: Proxy?) {
    fun selectProxies(): List<Proxy> {
      // 此处的代理对象为用户在`OkHttpClient`类中设立的代理对象
      if (proxy != null) return listOf(proxy)

      // 主机名缺失的情况
      val uri = url.toUri()
      if (uri.host == null) return immutableListOf(Proxy.NO_PROXY)

      // Try each of the ProxySelector choices until one connection succeeds.
      val proxiesOrNull = address.proxySelector.select(uri)
      if (proxiesOrNull.isNullOrEmpty()) return immutableListOf(Proxy.NO_PROXY)

      return proxiesOrNull.toImmutableList()
    }

    eventListener.proxySelectStart(call, url)
    proxies = selectProxies()
    nextProxyIndex = 0
    eventListener.proxySelectEnd(call, url, proxies)
  }
复制代码

这里如果用户在OkHttpClient中设置了代理,则返回代理对象,否则会逐步尝试address.proxySelector,这里的proxySelector是在OkHttpClient中设置的

//  OkHttpClient.kt
  val proxySelector: ProxySelector =
      when {
        // 如果用户未设置代理,返回NullProxySelector
        builder.proxy != null -> NullProxySelector
        else -> builder.proxySelector ?: ProxySelector.getDefault() ?: NullProxySelector
      }
复制代码

NullProxySelector对象如下

object NullProxySelector : ProxySelector() {
  override fun select(uri: URI?): List<Proxy> {
    requireNotNull(uri) { "uri must not be null" }
    return listOf(Proxy.NO_PROXY)
  }

  override fun connectFailed(uri: URI?, sa: SocketAddress?, ioe: IOException?) {
  }
}
复制代码

如果通过调整路由后,依然无法找到可以合并请求的连接,则创建一条新的连接,代码如下

 // NO4:确认连接池中没有匹配的路由,只有重新创建一条新的连接;
    val newConnection = RealConnection(connectionPool, route)
    // 连接创建成功后,是直接赋值给call的。
    call.connectionToCancel = newConnection
    try {
      // 建立通道  
      newConnection.connect(
          connectTimeout,
          readTimeout,
          writeTimeout,
          pingIntervalMillis,
          connectionRetryEnabled,
          call,
          eventListener
      )
    } finally {
      call.connectionToCancel = null
    }
    // 此处,routeDatabase是一个连接黑名单,如果创建的连接在黑名单之内,则将此连接从黑名单中移除
    call.client.routeDatabase.connected(newConnection.route())
复制代码

首先实例化RealConnection创建一个新的连接,随后通过connect方法通道的建立。

创建连接

直接查看newConnection.connect方法,看具体的连接过程

fun connect(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean,
    call: Call,
    eventListener: EventListener
  ) {
    check(protocol == null) { "already connected" }

    var routeException: RouteException? = null
    val connectionSpecs = route.address.connectionSpecs
    val connectionSpecSelector = ConnectionSpecSelector(connectionSpecs)
	
    if (route.address.sslSocketFactory == null) {
      // 如果使用的是SSL,则不支持使用明文协议  
      if (ConnectionSpec.CLEARTEXT !in connectionSpecs) {
        throw RouteException(UnknownServiceException(
            "CLEARTEXT communication not enabled for client"))
      }
      val host = route.address.url.host
      if (!Platform.get().isCleartextTrafficPermitted(host)) {
         // 如果使用的是SSL,则不支持使用明文协议    
        throw RouteException(UnknownServiceException(
            "CLEARTEXT communication to $host not permitted by network security policy"))
      }
    } else {
      // 如果使用的不是SSL,则不支持使用HTTPS协议  
      if (Protocol.H2_PRIOR_KNOWLEDGE in route.address.protocols) {
        throw RouteException(UnknownServiceException(
            "H2_PRIOR_KNOWLEDGE cannot be used with HTTPS"))
      }
    }
    // 直接进入死循环
    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

        eventListener.connectFailed(call, route.socketAddress, route.proxy, null, e)

        if (routeException == null) {
          routeException = RouteException(e)
        } else {
          routeException.addConnectException(e)
        }

        if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
          throw routeException
        }
      }
    }

    if (route.requiresTunnel() && rawSocket == null) {
      throw RouteException(ProtocolException(
          "Too many tunnel connections attempted: $MAX_TUNNEL_ATTEMPTS"))
    }

    idleAtNs = System.nanoTime()
  }
复制代码

该方法首先对协议的兼容性做了判断,如HTTPS是不支持明文的,而HTTP则是不支持加密等。协议兼容性问题解决后,开始正式的Socket连接,一般情况下直接调用connectSocket

private fun connectSocket(
    connectTimeout: Int,
    readTimeout: Int,
    call: Call,
    eventListener: EventListener
  ) {
    val proxy = route.proxy
    val address = route.address

    val rawSocket = when (proxy.type()) {
      // 存在代理的情况下,通过socket工厂方法创建Socket  
      Proxy.Type.DIRECT, Proxy.Type.HTTP -> address.socketFactory.createSocket()!!
      // 不存在情况,则直接实例化Socket,最原始的Socket  
      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)
      }
    }
    try {
      // 建立连接信道后,就可以直接获取I/O流了。  
      source = rawSocket.source().buffer()
      sink = rawSocket.sink().buffer()
    } catch (npe: NullPointerException) {
      if (npe.message == NPE_THROW_WITH_NULL) {
        throw IOException(npe)
      }
    }
  }
复制代码

实例化Socket对象,再通过平台相关API实现Socket的连接

// 平台实现Socket的连接
Platform.get().connectSocket(rawSocket, route.socketAddress, connectTimeout)
复制代码

Socket通道建立后,获取相应的I/O流,后续通过该I/O流实现报文的读写。

      // 建立连接信道后,就可以直接获取I/O流了。  
      source = rawSocket.source().buffer()
      sink = rawSocket.sink().buffer()
复制代码

上述情况是比较正常的情况,比较不正常的情况是遇到HTTP协议的服务器代理HTTPS协议的服务器,这种情况需要先在客户端与被代理服务器之间建立通信隧道。

      if (route.requiresTunnel()) {
          // 先建立与被代理服务器的隧道,然后在建立通信渠道。  
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener)
          if (rawSocket == null) {
            break
          }
        } 
复制代码

connectTunnel就是建立通信隧道的过程。

private fun connectTunnel(
  connectTimeout: Int,
  readTimeout: Int,
  writeTimeout: Int,
  call: Call,
  eventListener: EventListener
) {
  // 创建创建隧道的请求。
  var tunnelRequest: Request = createTunnelRequest()
  val url = tunnelRequest.url
  for (i in 0 until MAX_TUNNEL_ATTEMPTS) {
    // 连接Socket,此处是跟代理服务器建立连接。
    connectSocket(connectTimeout, readTimeout, call, eventListener)
    // 实现隧道的通信连接
    tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url)
        ?: break 
    rawSocket?.closeQuietly()
    rawSocket = null
    sink = null
    source = null
    eventListener.connectEnd(call, route.socketAddress, route.proxy, null)
  }
}
复制代码

建立隧道时先创建一个请求建立隧道的请求,调用createTunnelRequest 方法

  @Throws(IOException::class)
  private fun createTunnelRequest(): Request {
    // 如果不存在身份验证的情况下,直接携带`CONNECT`请求头  
    val proxyConnectRequest = Request.Builder()
        .url(route.address.url)
        .method("CONNECT", null)
        .header("Host", route.address.url.toHostHeader(includeDefaultPort = true))
        .header("Proxy-Connection", "Keep-Alive") // For HTTP/1.0 proxies like Squid.
        .header("User-Agent", userAgent)
        .build()

    // 如果存在身份验证,则使用抢先身份验证的方式,即先mock一个407的响应,这可以让
    // 身份验证器返回自定义的连接请求,或者身份验证器返回null表示拒绝。  
    val fakeAuthChallengeResponse = Response.Builder()
        .request(proxyConnectRequest)
        .protocol(Protocol.HTTP_1_1)
        .code(HTTP_PROXY_AUTH)
        .message("Preemptive Authenticate")
        .body(EMPTY_RESPONSE)
        .sentRequestAtMillis(-1L)
        .receivedResponseAtMillis(-1L)
        .header("Proxy-Authenticate", "OkHttp-Preemptive")
        .build()
	// 如果需要身份验证,则采用抢先身份验证的方式,返回自定义的连接请求。
    val authenticatedRequest = route.address.proxyAuthenticator
        .authenticate(route, fakeAuthChallengeResponse)

    return authenticatedRequest ?: proxyConnectRequest
  }
复制代码

createTunnelRequest方法中,不存在身份验证则正常返回携带CONNECT的请求,如果存在身份验证,则使用抢先身份验证方式,即自己先Mock一个407的响应,通过407的响应,用户在后续的请求中可以自定义连接请求,将响应传给身份验证器authenticated,返回一个自定义的请求。查看authenticate方法的实现

  @Override public Request authenticate(Route route, Response response) throws IOException {
    if (route == null) throw new NullPointerException("route == null");
    if (response == null) throw new NullPointerException("response == null");

    responses.add(response);
    routes.add(route);

    if (!schemeMatches(response) || credential == null) return null;
	// 如果407响应码,则添加`Proxy-Authorization:证书xxxx`
    String header = response.code() == 407 ? "Proxy-Authorization" : "Authorization";
    return response.request().newBuilder()
        .addHeader(header, credential)
        .build();
  }
复制代码

上面的方法之做了一件事,如果响应码为407,则增加Proxy-Authorization:cerdential的请求头,至于cerdential是从何而来,这跟责任链之前的拦截器有关,此处不分析。

获得request后,接下来将次request发给HTTPS的被代理服务器,但是在这之前,则必须先跟代理服务器先建立Socket连接,connectSocket的连接过程已经分析过,但此处建立的连接是客户端与代理服务器。与代理服务器建立连接后,客户端就可以跟被代理服务器建立隧道了,调用createTunnel方法如下

private fun createTunnel(
    readTimeout: Int,
    writeTimeout: Int,
    tunnelRequest: Request,
    url: HttpUrl
  ): Request? {
    var nextRequest = tunnelRequest
    // 首行
    val requestLine = "CONNECT ${url.toHostHeader(includeDefaultPort = true)} HTTP/1.1"
    while (true) {
      val source = this.source!!
      val sink = this.sink!!
      // 写入Socket流中,因为使用的是`HTTP代理`,所以使用是`HTTP1ExchangeCodec`  
      val tunnelCodec = Http1ExchangeCodec(null, this, source, sink)
      source.timeout().timeout(readTimeout.toLong(), MILLISECONDS)
      sink.timeout().timeout(writeTimeout.toLong(), MILLISECONDS)
      tunnelCodec.writeRequest(nextRequest.headers, requestLine)
      tunnelCodec.finishRequest()
      val response = tunnelCodec.readResponseHeaders(false)!!
          .request(nextRequest)
          .build()
      tunnelCodec.skipConnectBody(response)

      when (response.code) {
        // 200 则建立隧道成功  
        HTTP_OK -> {
          if (!source.buffer.exhausted() || !sink.buffer.exhausted()) {
            throw IOException("TLS tunnel buffered too many bytes!")
          }
          return null
        }
		// 407,则继续身份验证
        HTTP_PROXY_AUTH -> {
          nextRequest = route.address.proxyAuthenticator.authenticate(route, response)
              ?: throw IOException("Failed to authenticate with proxy")

          if ("close".equals(response.header("Connection"), ignoreCase = true)) {
            return nextRequest
          }
        }

        else -> throw IOException("Unexpected response code for CONNECT: ${response.code}")
      }
    }
  }
复制代码

建立隧道的方法,在创建与代理服务器连接的基础上,向代理服务器发送CONNECT请求(不含请求体),代理服务器会转发请求到被代理服务器。如果响应码为200则说明隧道建立成功,如果响应码为407则说明需要身份验证,后续的操作就是通过身份验证器生成携带证书的请求进行下次的请求,继续建立隧道。

至此连接的过程结束。

分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改