3. 为网络请求配置超时与重试策略

为网络请求配置超时与重试策略 学习卡片

超时与重试:让请求在可控时间内结束

HttpURLConnection 默认没有连接超时和读取超时,网络异常时线程可能长时间阻塞。本节通过 setConnectTimeout() 与 setReadTimeout() 控制两个阶段的等待上限,并用指数退避重试处理可恢复的瞬时失败。

两个超时的作用位置

连接超时约束的是 TCP 三次握手与建立连接的时间;读取超时约束的是连接建立后,等待服务端返回数据的间隔时间。两者独立生效,任何一个超时都会抛出 SocketTimeoutException。

HttpURLConnection 请求时间轴与超时边界 时间 建立连接(TCP握手) setConnectTimeout(ms) 等待/读取响应数据 setReadTimeout(ms) 响应完成 连接超时 → SocketTimeoutException 读取超时 → SocketTimeoutException 重试触发条件:IOException / SocketTimeoutException → 指数退避等待后重试,最多 maxRetries 次

指数退避重试实现

重试间隔按 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() 替换为对应的延迟操作符。

验证方法

  1. 将 CONNECT_TIMEOUT_MS 设为 1000,请求一个不可达地址(如 http://10.255.255.1),观察日志中是否在约 1 秒后抛出 SocketTimeoutException 并进入重试。
  2. 将 MAX_RETRIES 设为 0,确认请求只执行一次,不产生重试等待。
  3. 将 BASE_DELAY_MS 设为 500,观察三次重试的等待间隔依次约为 500 ms、1000 ms、2000 ms。
  4. 请求一个返回 404 的地址,确认不会触发重试,直接抛出 IOException("HTTP 404")。