3. 为网络请求配置超时与重试策略
超时与重试:让请求在可控时间内结束
HttpURLConnection 默认没有连接超时和读取超时,网络异常时线程可能长时间阻塞。本节通过 setConnectTimeout() 与 setReadTimeout() 控制两个阶段的等待上限,并用指数退避重试处理可恢复的瞬时失败。
两个超时的作用位置
连接超时约束的是 TCP 三次握手与建立连接的时间;读取超时约束的是连接建立后,等待服务端返回数据的间隔时间。两者独立生效,任何一个超时都会抛出 SocketTimeoutException。
指数退避重试实现
重试间隔按 baseDelay × 2^attempt 增长,避免连续快速重试加重服务端压力。以下代码可直接放入 Android 项目的工具类中运行。
public class HttpRetryClient {
private static final int CONNECT_TIMEOUT_MS = 5000;
private static final int READ_TIMEOUT_MS = 8000;
private static final int MAX_RETRIES = 3;
private static final long BASE_DELAY_MS = 500L;
public static String getWithRetry(String urlStr) throws IOException {
IOException lastException = null;
for (int attempt = 0; attempt <= MAX_RETRIES; attempt++) {
HttpURLConnection conn = null;
try {
URL url = new URL(urlStr);
conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(CONNECT_TIMEOUT_MS);
conn.setReadTimeout(READ_TIMEOUT_MS);
int code = conn.getResponseCode();
if (code == HttpURLConnection.HTTP_OK) {
return readStream(conn.getInputStream());
}
// 5xx 服务端错误可重试,4xx 客户端错误不重试
if (code >= 500 && attempt < MAX_RETRIES) {
lastException = new IOException("HTTP " + code);
} else {
throw new IOException("HTTP " + code);
}
} catch (SocketTimeoutException e) {
lastException = e;
} catch (IOException e) {
lastException = e;
} finally {
if (conn != null) conn.disconnect();
}
if (attempt < MAX_RETRIES) {
long delay = BASE_DELAY_MS * (1L << attempt); // 500, 1000, 2000
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new IOException("重试被中断", ie);
}
}
}
throw lastException != null ? lastException : new IOException("请求失败");
}
private static String readStream(InputStream is) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
reader.close();
return sb.toString();
}
}
不同超时参数下的行为差异
以下为教学示例参数,用于说明超时设置对请求结果的影响。实际耗时取决于网络环境,此处数值为预期行为描述。
| 连接超时 | 读取超时 | 典型场景 | 预期行为 |
|---|---|---|---|
| 2000 ms | 2000 ms | 弱网/高延迟 | 连接阶段即超时,抛出 SocketTimeoutException,触发重试 |
| 5000 ms | 8000 ms | 正常 4G/WiFi | 连接与读取均在超时内完成,返回响应体 |
| 5000 ms | 1000 ms | 服务端响应慢 | 连接成功但读取超时,抛出 SocketTimeoutException,触发重试 |
| 10000 ms | 10000 ms | 服务端不可达 | 连接超时后重试,3 次均失败后抛出最后一次异常 |
调参建议:连接超时通常设为 3–5 秒,读取超时设为 5–10 秒。重试次数建议不超过 3 次,退避基数 300–500 ms。对 4xx 客户端错误不应重试,仅对超时、连接失败和 5xx 服务端错误进行重试。
注意:重试逻辑必须在子线程中执行,不能在主线程调用
Thread.sleep(),否则会阻塞 UI 并触发 ANR。若在协程或 RxJava 中使用,应将 Thread.sleep() 替换为对应的延迟操作符。
验证方法
- 将
CONNECT_TIMEOUT_MS设为 1000,请求一个不可达地址(如http://10.255.255.1),观察日志中是否在约 1 秒后抛出SocketTimeoutException并进入重试。 - 将
MAX_RETRIES设为 0,确认请求只执行一次,不产生重试等待。 - 将
BASE_DELAY_MS设为 500,观察三次重试的等待间隔依次约为 500 ms、1000 ms、2000 ms。 - 请求一个返回 404 的地址,确认不会触发重试,直接抛出
IOException("HTTP 404")。