Okhttp源码读后反馈

848 阅读5分钟

一、okhttp简写尝试及结果(version:4.6.0)

okhttp可能是android最复杂的一个库,源码量大,分支多,在简写的时候,先通读了下源码,尝试把拦截器部分拆解、连接池复用的部分、http明文传输、缓存、http和socks代理相关的逻辑统统删除,精简下来,代码量确实少了很多,看起来很香。

最后的成果

支持https HTTP1.1的get请求 (已测试)

支持https HTTP2.0的post请求 (已测试)

二、okhttp的认知

1. 支持的协议HTTP1.1、HTTP2

HTTP1.1 本身就具备在创建好的tcp连接上,发起多个请求,请求可以并发,但是受限于Http1.1规定,服务端的响应发送根据请求被接收的顺序排队,如果最先的请求处理时间长,会阻塞已生成的响应的发送。 (测试okhttp的http1.1请求时,发现连接池未复用,大家可以用"www.baidu.com/" get请求测试)

HTTP2 在同一个tcp连接上,可以有多个stream,每一个stream承载请求和响应,各个stream相互独立,互不阻塞。 (测试okhttp的http2请求,连接池中的连接复用了,大家可以用"www.httpbin.org/post" post请求测试)

2. 拦截器

okhttp的拦截器很有创意,这样的逻辑处理很巧妙。

class BridgeInterceptor(private val cookieJar: CookieJar) : Interceptor {
  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
   ....请求头中header处理:比如添加对gzip的支持...
   
    //链处理,进入到下一个拦截器
    val networkResponse:Response = chain.proceed(requestBuilder.build())
    
   ....响应处理,如果gzip过,就解压response...
    return responseBuilder.build()
  }

chain.proceed(requestBuilder.build())将拦截器的请求和响应的处理分割开了,然后这个方法调用就会进入到下一个拦截器处理。这里的拦截器是有严格顺序的,最后一个拦截器CallServerInterceptor进行真正的io交互,比如写请求头、请求体,读取响应头和响应体。当最后一个拦截器走完,层层向上返回,其他拦截器开始处理response。用户自定义的拦截器(非网络拦截器)是最先处理请求的,然后是最后处理响应的。基于这样的顺序我们可以在拦截器里做token失效静默换token的场景需求。(ps:跟ARouter拦截器处理不一样,ARouter的拦截器是遍历依次调用,一个拦截器方法体走完才会走下一个拦截器)

  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)

3. 证书校验

这里以访问百度"www.baidu.com/"为例,当在简写时,发现okhttp对于这种CA机构颁发证书的host处理超级简单。okhttpClient创建时初始化,基于系统内置根证书。如socket创建以及握手能ok,就表示证书这块校验没问题。以下代码按照okhttp源码改写。

 // okhttpClient的init初始化
    var socketFactory: SocketFactory = SocketFactory.getDefault()
    val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
    factory.init(null as KeyStore?)
    val trustManagers = factory.trustManagers!!
    //返回的是根证书
    val x509TrustManager = trustManagers[0]
    val sslContext = SSLContext.getInstance("TLS")
    sslContext.init(null, arrayOf<TrustManager>(x509TrustManager), null)
    sslSocketFactoryOrNull = sslContext.socketFactory

基于socketFactory 和 sslSocketFactory去创建对应的socket。

    val rawSocket = okHttpClient.socketFactory.createSocket()!!
    var socketPort = request.url.port
    rawSocket.connect(InetSocketAddress(addresses[0], socketPort), 10_000)
    //所谓ssl 就是基于rawSocket创建sslSocket
    sslSocket = okHttpClient.sslSocketFactory!!.createSocket(rawSocket, request.url.host, request.url.port, true) as SSLSocket
    //与服务器协商时,需要给sslSocket设置共同的配置  客户端支持 TLSv1.3 TLSv1.2
    var tlsConnectionSpec: ConnectionSpec = ConnectionSpec.MODERN_TLS
    for (connectionspec in okHttpClient.connectionSpec) {
        if (connectionspec.isCompatible(sslSocket)) {
            tlsConnectionSpec = connectionspec;
            break
        }
    }
    //将sslSocket支持的protocols与cihersuites 和 这个TlsConnectionSpec取交集,再赋值给sslSocket
    val cipherSuitesAsString = tlsConnectionSpec.cipherSuites!!.map { it.javaName }!!.toTypedArray()
    var cipherSuitesIntersection = if (tlsConnectionSpec.cipherSuites != null) {
        sslSocket.enabledCipherSuites.intersect(cipherSuitesAsString,CipherSuite.ORDER_BY_NAME)
    } else {
        sslSocket.enabledCipherSuites
    }
    val tlsVersionsAsString = tlsConnectionSpec.tlsVersions!!.map { it.javaName }.toTypedArray()
    val tlsVersionIntersection = if (tlsVersionsAsString != null) {
        sslSocket.enabledProtocols.intersect(tlsVersionsAsString, naturalOrder())
    } else {
        sslSocket.enabledProtocols
    }
    //TLS_FALLBACK_SCSV 信令套件可以用来阻止客户端和服务器之间的意外降级,预防中间人攻击。
    val supportedCipherSuites = sslSocket.supportedCipherSuites
    val indexOfFallbackScsv = supportedCipherSuites.indexOf(
        "TLS_FALLBACK_SCSV", CipherSuite.ORDER_BY_NAME
    )
    if (indexOfFallbackScsv != -1) {
        cipherSuitesIntersection = cipherSuitesIntersection.concat(supportedCipherSuites[indexOfFallbackScsv])
    }
    sslSocket.enabledProtocols = tlsVersionIntersection
    sslSocket.enabledCipherSuites = cipherSuitesIntersection
    //开始握手
    sslSocket.startHandshake()

这一块的逻辑其实就是处理我们面试常遇到的“https的原理”的一部分。

4. RouteSelector用于当连接出错时,切换到其他的route去尝试创建新的连接。

为啥RouteSelector可以做这样的尝试,因为我们请求的url,会被dns解析为多个ip地址。

 var addresses:List<InetAddress> = InetAddress.getAllByName(request.url.host).toList()

以百度https://www.baidu.com/为例,会被解析成如下的地址

  //InetAddress    www.baidu.com/182.61.200.6  
  //InetAddress    www.baidu.com/182.61.200.7
  okhttp Route.address = www.baidu.com:443
  okhttp Route.socketAddress =  www.baidu.com/182.61.200.6:443

okhttp使用第一个地址去创建连接,创建连接失败或者创建的连接出现错误,就尝试用另外剩下的ip地址去创建连接。

5. okhttp怎么判断连接是走HTTP1.1还是HTTP2(要区分android系统版本)

//握手前配置 configureTlsExtensions
val sslSocketClass =
    Class.forName("com.android.org.conscrypt.OpenSSLSocketImpl") as Class<in SSLSocket>
val setAlpnProtocols =
    sslSocketClass.getMethod("setAlpnProtocols", ByteArray::class.java)
var protocols = immutableListOf(Protocol.HTTP_1_1, Protocol.HTTP_2)
setAlpnProtocols.invoke(sslSocket, concatLengthPrefixed(protocols))

//开始握手
sslSocket.startHandshake()

//可以在这里判断是走的http1.1还是http2.0(h2)
val getAlpnSelectedProtocol = sslSocketClass.getMethod("getAlpnSelectedProtocol")
val alpnResult = getAlpnSelectedProtocol.invoke(sslSocket) as ByteArray?
val protocolStr = if (alpnResult != null) String(alpnResult, StandardCharsets.UTF_8) else null
//默认是http1.1
val protocol = if (protocolStr != null) Protocol.get(protocolStr) else Protocol.HTTP_1_1

最后protocol返回h2表示HTTP2, 返回http/1.1表示HTTP1.1

三、okhttp的讨论点

1. okhttp异步请求,在Dispatcher创建了一个线程池,这个线程池是一个可缓存的线程池,为什么队列使用SynchronousQueue()?

class Dispatcher {
    val executorService: ExecutorService by lazy {
        ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS, SynchronousQueue(),
            ThreadFactory {
                Thread(it, "okhttp").apply {
                    isDaemon=false
                }
            })
    }
}

2. okhttp在处理socket超时比如连接超时或者读取超时时,为什么使用守护线程?守护线程有什么特殊的作用?

  private class Watchdog internal constructor() : Thread("Okio Watchdog") {
    init {
      isDaemon = true
    }
    override fun run() {
      while (true) {
        try {
          var timedOut: AsyncTimeout? = null
          synchronized(AsyncTimeout::class.java) {
            timedOut = awaitTimeout()
            // The queue is completely empty. Let this thread exit and let another watchdog thread
            // get created on the next call to scheduleTimeout().
            if (timedOut === head) {
              head = null
              return
            }
          }
          // Close the timed out node, if one was found.
          timedOut?.timedOut()
        } catch (ignored: InterruptedException) {
        }
      }
    }
  }

3. HTTP2下,去读取Stream,直接创建子线程处理,是否是最佳方案?

@Throws(IOException::class) @JvmOverloads
fun start(sendConnectionPreface: Boolean = true) {
    if (sendConnectionPreface) {
        //客户端预先知道服务器提供HTTP/2支持,可以免去101协议切换的流程开销 客户端必须首先发送一个连接序言
        writer.connectionPreface()
        //SETTINGS帧
        writer.settings(okHttpSettings)
        val windowSize = okHttpSettings.initialWindowSize
        if (windowSize != DEFAULT_INITIAL_WINDOW_SIZE) {
        //更新滑动窗口
            writer.windowUpdate(0, (windowSize - DEFAULT_INITIAL_WINDOW_SIZE).toLong())
        }
    }
    //每一个连接的读取  开一个线程
    Thread(readerRunnable, connectionName).start() // Not a daemon thread.
}

四、总结

okhttp真的值得一看,解决了以前存在的很多疑问。由于目前对HTTP2的协议不太理解,在HTTP2 流处理这块,直接囫囵吞枣的看了一遍,有点遗憾,等看完相关书籍再来补充。 诚意奉上精简的okhttp,这个库只是让你了解http1.1以及http2.0相关处理流程。

SimpleOkhttp

相关扫盲链接

HTTP2 的连接

tcp的滑动窗口

HTTP 2.0与OkHttp