在IOT开发连接Soft AP后发起网络请求时,发现一个奇怪的现象,经常有网络请求超时,一开始以为是硬件(提供Web服务)的原因,因为客户端代码看到网络请求已经出去了,但是硬件端反馈就是没收到。那问题出在哪里呢?
一、连接池与脏连接
先介绍一下基础概念。
1、为什么需要连接池
因为连接复用带来的性能收益非常大,具体对比如下:
| 项目 | 无连接池 | 有连接池 |
|---|---|---|
| 每次请求 | 建立 TCP + TLS 握手(几十~上百 ms) | 直接发送请求(几 ms) |
| 服务器负载 | 每次都新建 Socket、TLS 握手 | 复用已有连接,减少 CPU |
| 网络流量 | 多次三次握手、证书传输 | 复用连接减少控制包 |
| 用户体验 | 慢、延迟高 | 快、流畅 |
我们在Android开发中一般使用的是Okhttp,Okhttp的源码中连接池的大小是5,连接的保活时间是5分钟,源码如下:
kotlin
class ConnectionPool internal constructor(
internal val delegate: RealConnectionPool
) {
constructor(
maxIdleConnections: Int,
keepAliveDuration: Long,
timeUnit: TimeUnit
) : this(RealConnectionPool(
taskRunner = TaskRunner.INSTANCE,
maxIdleConnections = maxIdleConnections,
keepAliveDuration = keepAliveDuration,
timeUnit = timeUnit
))
//连接池默认构造
constructor() : this(5, 5, TimeUnit.MINUTES)
...略...
}
2、什么是脏连接(stale connection)?
当客户端复用一个已经空闲的 TCP 连接时,如果 服务端已经关闭了这个连接(可能是超时、服务器主动断开、网络抖动),客户端尝试用这个连接发送请求就会失败:
kotlin
java.io.EOFException: unexpected end of stream
java.net.SocketException: Connection reset by peer
这个连接就是所谓的 "脏连接"。因为连接池本身不知道服务端已经关闭了连接,所以当它尝试重用就会出现上面的问题。
二、遇到的问题与解决办法
当时是IOT开发,硬件侧是一个低功耗摄像头,硬件有开启一个Web服务供客户端请求,然后我发现总是有接口请求超时,我就很纳闷,硬件端抓日志说我的请求没收到???
一开始是怀疑硬件端网络请求队列的问题,因为有些页面有高并发,有些请求会不会在队列中等待时间过长就超时了? 后面放宽了超时时间,发现还是有超时现象,这就很奇怪了!!!
后面注意力才转移到脏连接上来,一开始使用了Okhtto的重试机制:
kotlin
.retryOnConnectionFailure(true) // 保留自动重试
发现并不能解决问题,正如这个函数描述的那样:
kotlin
/**
* ...略...
* Stale pooled connections. The [ConnectionPool] reuses sockets
* to decrease request latency, but these connections will occasionally time out.
* ...略...
*/
fun retryOnConnectionFailure(retryOnConnectionFailure: Boolean) = apply {
this.retryOnConnectionFailure = retryOnConnectionFailure
}
复用连接池偶尔会请求超时...
后面跟硬件工程师讨论,他说硬件端往往资源比较紧张,硬件用的这个三方网络库比较轻量,可能没有连接池这些东西或者Socket keep-alive的时间很短会及时释放。硬件工程师比较忙,也没办法继续协助我排查问题,后面查资料发现
Nginx/Tomcat 默认 Keep-Alive 超时时间可能只有 1~5 秒
这样的话使用Okhttp的连接池就没什么意义了,直接禁用了连接池解决了上面的问题:
kotlin
.connectionPool(ConnectionPool(0, 1, TimeUnit.SECONDS)) //不复用连接
三、总结
很多看着奇奇怪怪的问题,其实很有可能是底层没有完全弄懂,要保持一定的探索欲,能解决现实问题也能让我们更加明白深层次的原理。