作者:苍耳叔叔
一个示例
前后分别去请求同一个域名下的接口,通过 Charles 抓包,可以看到 Timing 下面的时间:
- 第二次请求时,DNS、Connect 和 TLS Handshake 部分都是
-
,说明没有这部分的耗时,对比第一次请求的这三个部分,节省了1 + 35 + 97
=133ms
。 - 当然第一次请求的 Request 和 Response 的 Size 比第二次要大一丢丢,且 Speed 低一些,忽略这些差异,在其他条件都一致的情况下,第二次请求比第一次请求能快 133ms。
第一次
第二次
这就是 http(s) 的连接复用。
连接复用
在此之前先简单复习一下发起网络 Request ->收到 Response 的粗略过程:
- 客户端发起网络请求
- 通过 DNS 服务解析域名,获取 IP 地址(一般是 UDP 协议)
- 建立 TCP 连接(三次握手)
- 建立 TLS 连接(Https)
- 发送网络请求 Request
- 服务器接收 Request 到后,执行逻辑并返回 Response
- 关闭 TCP 连接(四次挥手)
通过连接复用,上面的 2、3、4 步都不需要重新走了。使用 RTT 来定义这个时长,RTT(Round-Trip Time, 往返时间) 是网络请求从起点到目的地然后再回到起点所花费的时长。那么节省的时间是:
- DNS 一般使用 UDP 协议,最近重新复习了下 DNS 的内容,如果 DNS 响应报文的长度大于 512 字节,则会使用 TCP 协议。事实上,很多 DNS 服务器进行配置时,也仅支持 UDP。因此这一步可以看成节省了 1 个 RTT。
- 建立 TCP 连接,三次握手,需要 2 个 RTT。
- 建立 TLS 连接,根据 TLS 版本,有不同的 RTT。
HTTP1.1 版本开始默认就是持久连接,可以复用,通过在报文头部加上 Connection:Close
来关闭连接。另外空闲的持久连接也可以随时被关闭,即使不发送 Connection:Close
,也不意味着服务器连接永远保持打开。
预连接
常用的网络框架如 OkHttp 等,都支持 HTTP1.1 和 HTTP2 的功能,那也支持连接复用。
我们可以利用这个机制来做一个预连接,比如说在 APP 闪屏等待时,预先建立起关键页面域名的连接,这样在用户进入相应页面后可以更快的获取到网络请求结果,提升用户体验。
可以简单的对域名链接提前发起一个 HEAD 请求(没有Body),这样就能提前建立好连接,下次同域名的请求可以直接复用。
private val client by lazy { OkHttpClient() }btn.setOnClickListener {// 正式请求launch(Dispatchers.IO) {request()}
}// 预连接
launch(Dispatchers.IO) {preRequest()
}fun preRequest() {val request = Request.Builder().head().url("xxx").build()client.newCall(request).execute()
}fun request() {val request = Request.Builder().get().url("xxx/yyy").build()client.newCall(request).execute()
}
可以抓包看到首次进入时发送的 head 请求和实际上点击发送的 get 请求:
预连接
正式请求
可以看到正式请求时,确实少了上述三个步骤的耗时。还可以分别看下 Connection 和 TLS 的信息:
预连接
正式请求
能看出来正式请求时,这俩是复用的(关注 TLS 的 Session Resumed
和 Connection 的 Server Connection
部分)。
另外 OkHttp 中有个 ConnectionPool 连接池,在使用 Connect 连接时,会优先复用已有的连接,无可用时才创建新连接。连接池里容纳的连接数是限定的(貌似默认是 5 个),如果业务比较复杂,请求比较多的话,可能会导致连接池占满,这样就会释放之前做好的预连接。因此一个比较简单的方式就是适当调大连接池的容量和超时时间。
总结
通过 http(s) 的连接复用机制,我们可以考虑使用预连接来优化 APP 中某些场景的网络请求速度,这需要我们根据实际业务场景以及服务器压力来判断是否进行预连接。
另外我们可以适当调大连接池的容量和超时时间,由于连接是双向的,即使客户端把 Connection 一直保留,服务端也会根据实际连接数量和时长来自动关闭连接的,所以调大连接池一般不会增大服务器压力。
预连接的效果实际和服务器配置有关,如果服务器把连接超时设置得很小,那每次请求可能都需要重新建立连接,这样客户端的预连接会失效,且服务器也需要不断创建和销毁 TCP 连接而浪费更多资源;如果服务器把连接超时设置得很大,那之前的连接长时间都不会释放,导致服务器服务的并发数受到影响,影响新的请求。因此调优需要多端协调,综合考虑。
Android 学习笔录
Android 性能优化篇:https://qr18.cn/FVlo89
Android 车载篇:https://qr18.cn/F05ZCM
Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
Android Framework底层原理篇:https://qr18.cn/AQpN4J
Android 音视频篇:https://qr18.cn/Ei3VPD
Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
Kotlin 篇:https://qr18.cn/CdjtAF
Gradle 篇:https://qr18.cn/DzrmMB
OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
Flutter 篇:https://qr18.cn/DIvKma
Android 八大知识体:https://qr18.cn/CyxarU
Android 核心笔记:https://qr21.cn/CaZQLo
Android 往年面试题锦:https://qr18.cn/CKV8OZ
2023年最新Android 面试题集:https://qr18.cn/CgxrRy
Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
音视频面试题锦:https://qr18.cn/AcV6Ap