在IoT开发中,当设备连接Soft AP后发起网络请求时,我们观察到一个奇怪的现象:网络请求频繁超时。起初怀疑是硬件端(提供Web服务)的问题,因为客户端日志显示请求已发出,但硬件端反馈始终未收到。问题究竟出在哪里?
一、连接池与脏连接
首先,我们了解两个核心概念:连接池与脏连接。
1. 为什么需要连接池
连接复用能带来显著的性能提升,具体对比如下:
| 项目 |
无连接池 |
有连接池 |
| 每次请求耗时 |
需建立TCP + TLS握手(几十~上百毫秒) |
可直接发送请求(几毫秒) |
| 服务器负载 |
每次新建Socket、进行TLS握手 |
复用已有连接,减少CPU开销 |
| 网络流量 |
多次三次握手、传输证书 |
复用连接,减少控制包 |
| 用户体验 |
慢、延迟高 |
快、流畅 |
在Android开发中,我们普遍使用OkHttp,其源码中的连接池默认大小为5,连接保活时间为5分钟:
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连接时,如果服务端已经关闭了这个连接(可能由于超时、主动断开或网络抖动),客户端发送请求就会失败,并可能抛出如下异常:
java.io.EOFException: unexpected end of stream
java.net.SocketException: Connection reset by peer
这个已被服务端关闭,但客户端连接池仍认为有效的连接,就是所谓的“脏连接”。连接池无法感知服务端的状态变化,因此复用脏连接必然导致请求失败。
二、问题定位与解决方案
在本次IoT开发场景中,硬件侧为一款低功耗摄像头,其内置了一个Web服务供客户端调用。我们发现部分接口请求频繁超时,硬件侧抓取日志却声称未收到请求。
起初,怀疑是硬件端网络请求队列在高并发下出现阻塞,导致某些请求等待超时。然而,即使大幅放宽了客户端的超时时间限制,超时现象依然存在,这令人生疑。
随后,注意力转向了“脏连接”问题。我们首先尝试启用OkHttp内置的重试机制:
.retryOnConnectionFailure(true) // 启用自动重试
但问题并未解决。正如该函数注释所述,它主要应对的是复用连接池时偶尔出现的超时,对于服务端连接策略不匹配的情况,重试可能无效。
在与硬件工程师沟通后了解到,硬件设备资源紧张,所使用的第三方网络库非常轻量,可能不具备连接池机制,或者Socket Keep-Alive的保持时间非常短,连接会很快被释放。同时,我们也查阅资料得知,常见的服务端软件如 Nginx/Tomcat,其默认的Keep-Alive超时时间可能仅有1~5秒。
这意味着,客户端(OkHttp)默认5分钟的连接保活策略与服务器端极短的连接超时策略严重不匹配,导致客户端尝试复用的大多数都是已被服务器关闭的“脏连接”。在这种情况下,继续使用连接池不仅无益,反而有害。
最终,我们通过彻底禁用OkHttp的连接池解决了该问题:
.connectionPool(ConnectionPool(0, 1, TimeUnit.SECONDS)) // 将最大空闲连接数设为0,即不复用连接
三、总结
许多看似蹊跷的网络问题,其根源往往在于对底层机制的理解不够透彻。例如,默认的连接池策略并非万能,当客户端与服务器的连接保持策略存在巨大差异时(常见于嵌入式或资源受限的服务端),复用连接反而会成为稳定性的绊脚石。保持探索欲,深入理解原理,是解决实际复杂问题的关键。