OkHttp连接复用
当使用OkHttp时,它会先建立TCP连接,然后再发送请求,当请求结束后,TCP连接并不是立马关闭的,而是缓存起来,直接空闲了一段时间后才会关闭,所以当你连续多次请求时,如果请求的服务器和协议(HTTP 1.1/2.0)相同,则它会复用之前的建立的连接,以加快请求速度。
需要注意的是,要想复用TCP链接,必须满足一些要求:
- 每次请求时,必须使用相同的
OkHttpClient对象 - 读取完Response中的响应体内容,或者关闭这个响应体,因为读取完响应体或者关闭响应体则代表这次的请求结束了,可以拿来复用了。
- 请求同一个主机(Host + Port + Protocol)
- 在指定的空闲超时时间内
OkHttp 4.x中,连接池的配置默认是缓存5个空闲连接,每个连接的空闲超时时间是5分钟,如果5分钟没有被使用才会关闭该缓存的空闲连接。
在 HTTP/1.1 协议中,连接默认是 持久连接(Keep-Alive)。因此,一个 GET 请求完成后,连接一般会保持活动状态约 5 分钟,以等待可能的复用。
有一些情况不会复用连接:
- 服务器返回 Connection: close 响应头,表示服务器主动要求关闭连接。
- 客户端请求头中设置了 Connection: close,表示客户端不希望保持连接。
- 请求或响应出错(如网络中断、TLS 握手失败等),OkHttp 会关闭连接而不缓存。
- HTTP/2 或 HTTP/3 多路复用,若目标服务器支持 HTTP/2 或 HTTP/3,则一个 TCP(或 QUIC)连接可被多个请求同时复用,更不会轻易断开。只有当空闲超时或连接异常时才会释放。
- 每次请求使用不同的OkHttpClient对象,则连接缓存在不同的OkHttpClient对象中,也没有复用说法了,相当于没缓存。
- 请求的不是同一个主机(Host + Port + Protocol)。
- 连接缓存池已满,新连接则没法缓存了。
缓存连接示例:
private fun connect() { val request = Request.Builder().url("https://www.baidu.com")//.header("Connection", "close").build()// DNS解析不受OkHttp的超时影响,如果网络不通,光DNS解析要等40秒才会返回失败:// UnknownHostException: Unable to resolve host "www.baidu.com": No address associated with hostname// UnknownHostException异常表明都没到建立TCP连接的阶段。client.newCall(request).enqueue(object : Callback {override fun onFailure(call: Call, e: IOException) {Log.i("ABCD", "请求失败, ${e.message}", e)}override fun onResponse(call: Call, response: Response) {val responseCode = response.codeval protocol = response.protocol//val body = response.body.string()response.closeQuietly()//println("请求成功:$protocol, $responseCode")}})
}val listener = object : okhttp3.EventListener() {override fun dnsStart(call: Call, domainName: String) {println("DNS start: $domainName")}override fun dnsEnd(call: Call, domainName: String, inetAddressList: List<InetAddress>) {println("DNS end: $domainName -> $inetAddressList")}override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) {println("Connect start: $inetSocketAddress")}override fun connectEnd(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?) {println("Connect end: $inetSocketAddress, $protocol")}override fun connectionAcquired(call: Call, connection: Connection) {println("Connection acquired: ${connection.hashCode()} ${connection.route().socketAddress}")}override fun connectionReleased(call: Call, connection: Connection) {println("Connection released: ${connection.hashCode()}")}
}val client = OkHttpClient.Builder().eventListener(listener).build()
多次调用connect()函数,打印结果如下:
DNS start: www.baidu.com
DNS end: www.baidu.com -> [www.baidu.com/183.2.172.17, www.baidu.co
Connect start: www.baidu.com/183.2.172.17:443
Connect end: www.baidu.com/183.2.172.17:443, http/1.1
Connection acquired: 99258806 www.baidu.com/183.2.172.17:443
Connection released: 99258806
Connection acquired: 99258806 www.baidu.com/183.2.172.17:443
Connection released: 99258806
Connection acquired: 99258806 www.baidu.com/183.2.172.17:443
Connection released: 99258806
Connection acquired: 99258806 www.baidu.com/183.2.172.17:443
Connection released: 99258806
可以看到,只有第一次请求时执行了DNS解析,且只有第一次请求才执行了connectStart回调,这代表开始建立新的连接。connectionAcquired回调则说明取到了一个连接,以执行请求,这里打印了连接的hashCode,多次请求打印的连接hashCode相同,说明后面的请求复用了相同的连接,所以后面的请求不再需要进行DNS解析,也不需要建立新连接,所以请求速度会比较快。
当我们把响应体读取完,或者关闭这个响应体时,则connectionReleased会被调用,这代码这个连接处于空闲状态了,可以被复用。
有时候,有些场景可能不希望复用连接,则可以在请求时添加请求头:header("Connection", "close")。
当然,如果你每次都创建新的OkHttpClient对象来执行请求,也不会有复用连接的效果,但是不推荐这样做。
另外需要注意的是,DNS解析的超时时间不受OkHttp的请求超时时间的影响,我在一台设备上测试发现网络不通的请求下,请求40秒才返回UnknownHostException,导致等了很久,所以,如果可以,尽量直接使用IP的方式发送请求,以省去解析DNS的时间。即使设置了callTimeout(10, TimeUnit.SECONDS),如果使用域名访问,当网络不通时,也是要等40秒才返回错误,只是返回的错误异常是:InterruptedIOException:timeout,这说明DNS解析是一个同步阻塞操作,所以OkHttp没办法在超时时间到达时即时抛出InterruptedIOException异常,只能等到DNS解析返回时才能抛出InterruptedIOException异常。
当我启动了VPN后,UnknownHostException异常20秒就能返回。
另外还发现一个奇怪现在,我们公司使用了天融信VPN,打启动了VPN后,我们要检测网络是否能通到互联网(我们的需求是不允许通到互联网的),如果访问baidu.com,不论是get请求,还是ping,都会导致DNS解析,所以这个DNS要卡20秒,在这20秒之内,别的能正常访问的请求都变得不正常了,即不能访问了,直到访问百度的请求结束后,别的请求才能正常。所以,为了解决这个问题,只能改成直接访问ip的方式,不要访问域名,但是这也会带来新问题,比如百度的ip可能是会变的,所以也不安全,后来想到用ping,我们可以ping DNS的域名服务器,如果能ping通就说明网络已经通了互联网了,而且DNS服务器IP一般是不会变的,所以这就能解决我们的问题:避免访问域名导致的域名解析,然后域名解析实际上网络又不通就会导致域名解析需要很久才返回异常,在返回异常之前的这段时间网络是没办法使用的,这很奇怪,不知道是系统的Bug,还是VPN的Bug。
另外,关于DNS解析日志的回调,我使用如下版本时:
implementation("com.squareup.okhttp3:okhttp:4.8.1")
implementation("com.squareup.okhttp3:logging-interceptor:4.8.1")
我发现即使我的请求是直接是用ip的,它也会打印DNS start的信息,日志如下:
DNS start: 172.36.20.241
DNS end: 172.36.20.241 -> [/172.36.20.241]
Connect start: /172.36.20.241:35008
Connect end: /172.36.20.241:35008, http/1.1
Connection acquired: 131289579 /172.36.20.241:35008
Connection released: 131289579
请求成功:http/1.1, 200
这就很奇怪,后来AI后得知,这是OkHttp的设置问题,虽然DNS的事件回调被调用了,但是它并不会真正走DNS解析。
后来我发现使用较新版本的OkHttp时,这个Bug就解决了,比如使用:
implementation("com.squareup.okhttp3:okhttp:5.2.1")
implementation("com.squareup.okhttp3:logging-interceptor:5.2.1")
相同的代码,运行日志如下:
Connect start: /172.36.20.241:35008
Connect end: /172.36.20.241:35008, http/1.1
Connection acquired: 246524744 /172.36.20.241:35008
Connection released: 246524744
请求成功:http/1.1, 200
这里就没有打印DNS回调的相关信息了,所以,我们还是要随时保持依赖库使用最新版本,必竟新版本一般是修复Bug或提供性能或增加新功能的。
