一、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相关处理流程。
相关扫盲链接