MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

基于 Flutter 的网络请求重试机制的实现

2022-04-198.0k 阅读

网络请求在 Flutter 中的重要性

在现代移动应用开发中,网络请求是不可或缺的一部分。Flutter 作为一款流行的跨平台开发框架,为开发者提供了丰富的工具和库来处理网络请求。无论是获取数据以填充列表,还是向服务器发送用户操作的反馈,网络请求都在其中扮演着关键角色。然而,网络环境往往复杂多变,可能会出现网络不稳定、服务器过载等问题,导致网络请求失败。为了提升应用的稳定性和用户体验,实现网络请求重试机制就显得尤为重要。

常见的网络请求库在 Flutter 中的应用

Dio 库

Dio 是 Flutter 中一个广泛使用的网络请求库。它提供了简洁易用的 API,支持诸如 GET、POST、PUT、DELETE 等常见的 HTTP 请求方法。同时,Dio 还具备拦截器、请求取消、响应缓存等强大功能,这使得它成为很多 Flutter 开发者处理网络请求的首选库。以下是一个简单使用 Dio 进行 GET 请求的示例代码:

import 'package:dio/dio.dart';

void main() async {
  try {
    Dio dio = Dio();
    Response response = await dio.get('https://example.com/api/data');
    print(response.data);
  } catch (e) {
    print('请求失败: $e');
  }
}

http 库

Flutter 官方提供的 http 库也是处理网络请求的常用选择。它是一个底层的 HTTP 客户端库,提供了基本的 HTTP 请求功能。虽然相较于 Dio 功能相对简单,但对于一些对性能要求极高且只需要基本网络请求功能的场景,http 库也是不错的选择。以下是使用 http 库进行 GET 请求的示例:

import 'package:http/http.dart' as http;

void main() async {
  try {
    Uri url = Uri.parse('https://example.com/api/data');
    http.Response response = await http.get(url);
    if (response.statusCode == 200) {
      print(response.body);
    } else {
      print('请求失败,状态码: ${response.statusCode}');
    }
  } catch (e) {
    print('请求失败: $e');
  }
}

网络请求失败的原因分析

网络相关原因

  1. 网络连接不稳定:移动设备可能在不同网络环境之间频繁切换,如从 Wi-Fi 切换到移动数据网络,或者在信号弱的区域,网络连接可能会出现短暂中断。这种不稳定可能导致正在进行的网络请求失败。
  2. 网络延迟过高:当网络拥塞时,数据传输速度会显著减慢,网络请求可能因为等待响应超时,从而导致失败。不同的网络请求对延迟的容忍度不同,例如实时数据请求可能对延迟更加敏感。
  3. 网络连接丢失:设备可能会因为进入无网络覆盖区域,如地下停车场、电梯等,导致网络连接完全丢失,使得所有网络请求无法正常进行。

服务器相关原因

  1. 服务器过载:当大量用户同时访问服务器时,服务器可能会因为资源耗尽而无法及时处理新的请求,导致请求失败。这种情况在高流量的应用中较为常见,特别是在一些促销活动、热门事件期间。
  2. 服务器维护或故障:服务器可能会定期进行维护,或者由于硬件故障、软件错误等原因出现故障。在这些情况下,服务器可能无法正常响应网络请求,返回错误状态码。

客户端相关原因

  1. 请求参数错误:客户端在构建网络请求时,如果参数格式不正确或者缺少必要的参数,服务器可能无法正确处理请求,从而返回错误响应。例如,在进行身份验证的请求中,如果缺少认证令牌,服务器会拒绝请求。
  2. 网络配置错误:客户端的网络配置,如代理设置、防火墙规则等,如果设置不当,可能会阻止网络请求的正常发送或接收。例如,错误的代理配置可能导致请求无法到达服务器。

重试机制的基本原理

重试次数的设定

在实现重试机制时,首先需要确定的是重试的次数。这是一个平衡用户体验和资源消耗的过程。如果重试次数设置过少,可能在网络短暂波动的情况下,无法让请求成功,影响用户体验;而如果重试次数设置过多,可能会在请求无法成功的情况下,浪费过多的资源,如电量、流量等,同时也会延长用户等待的时间。一般来说,可以根据具体的业务场景和网络环境来设定重试次数,常见的取值范围为 3 - 5 次。

重试间隔时间的控制

重试间隔时间是指每次重试之间的时间间隔。简单的做法是设置固定的重试间隔时间,例如每次重试间隔 1 秒。然而,这种方式在某些情况下可能不太合适。如果网络问题较为严重,固定的短间隔重试可能会导致过多的无效请求,进一步加重网络负担。因此,一种更灵活的方式是采用指数退避算法。该算法会在每次重试时,将重试间隔时间按照一定的指数规律增加,例如第一次重试间隔 1 秒,第二次重试间隔 2 秒,第三次重试间隔 4 秒,以此类推。这样可以在保证重试机会的同时,避免过于频繁地发送请求。

重试条件的判断

并非所有的网络请求失败都需要重试。例如,如果请求失败是因为客户端请求参数错误,重试并不能解决问题。因此,需要根据请求失败的原因来判断是否进行重试。一般来说,对于网络连接相关的错误(如网络超时、连接丢失等)以及服务器端的临时性错误(如服务器过载返回 503 状态码),可以考虑进行重试。而对于客户端错误(如 400 系列状态码表示的请求错误)和服务器端的永久性错误(如 404 表示资源不存在),则不应该进行重试。

基于 Dio 实现网络请求重试机制

利用 Dio 的拦截器实现重试

Dio 的拦截器机制为我们实现重试功能提供了便利。我们可以创建一个自定义的拦截器,在拦截器中处理请求失败的情况并进行重试。以下是具体的实现代码:

import 'package:dio/dio.dart';

class RetryInterceptor extends Interceptor {
  final int maxRetries;
  final Duration retryDelay;

  RetryInterceptor({
    this.maxRetries = 3,
    this.retryDelay = const Duration(seconds: 1),
  });

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) async {
    if (err.type == DioErrorType.connectTimeout ||
        err.type == DioErrorType.sendTimeout ||
        err.type == DioErrorType.receiveTimeout ||
        (err.response?.statusCode == 503)) {
      int retryCount = 0;
      while (retryCount < maxRetries) {
        await Future.delayed(retryDelay);
        try {
          final response = await err.requestOptions.retry();
          handler.resolve(response);
          return;
        } catch (e) {
          retryCount++;
        }
      }
    }
    handler.next(err);
  }
}

在上述代码中,RetryInterceptor 继承自 Interceptor 类。在 onError 方法中,首先判断错误类型,如果是连接超时、发送超时、接收超时或者服务器返回 503 状态码(表示服务器暂时不可用),则进入重试逻辑。在重试逻辑中,通过一个 while 循环进行重试,每次重试前等待 retryDelay 时间。如果重试成功,则通过 handler.resolve 方法返回响应;如果重试次数达到 maxRetries 仍未成功,则通过 handler.next 方法将错误传递下去。

使用示例

在实际使用中,我们只需要将这个自定义的拦截器添加到 Dio 的拦截器列表中即可。以下是使用示例:

void main() async {
  Dio dio = Dio();
  dio.interceptors.add(RetryInterceptor(maxRetries: 3, retryDelay: const Duration(seconds: 1)));
  try {
    Response response = await dio.get('https://example.com/api/data');
    print(response.data);
  } catch (e) {
    print('最终请求失败: $e');
  }
}

通过上述代码,当网络请求因为符合重试条件的错误而失败时,Dio 会自动按照我们设定的重试机制进行重试。

基于 http 库实现网络请求重试机制

手动实现重试逻辑

由于 http 库相对底层,没有像 Dio 那样直接提供拦截器机制来实现重试。因此,我们需要手动在代码中实现重试逻辑。以下是一个基于 http 库实现重试机制的示例代码:

import 'package:http/http.dart' as http;
import 'dart:async';

Future<http.Response> retryRequest(
  Uri url, {
  int maxRetries = 3,
  Duration retryDelay = const Duration(seconds: 1),
}) async {
  int retryCount = 0;
  while (retryCount < maxRetries) {
    try {
      return await http.get(url);
    } catch (e) {
      retryCount++;
      if (retryCount < maxRetries) {
        await Future.delayed(retryDelay);
      }
    }
  }
  throw Exception('达到最大重试次数,请求失败');
}

在上述代码中,retryRequest 函数接收一个 Uri 对象表示请求的 URL,以及可选的 maxRetriesretryDelay 参数。函数内部通过一个 while 循环进行重试。每次请求失败后,增加重试计数,如果重试次数未达到 maxRetries,则等待 retryDelay 时间后再次尝试。如果达到最大重试次数仍未成功,则抛出异常。

使用示例

以下是使用 retryRequest 函数的示例:

void main() async {
  Uri url = Uri.parse('https://example.com/api/data');
  try {
    http.Response response = await retryRequest(url, maxRetries: 3, retryDelay: const Duration(seconds: 1));
    if (response.statusCode == 200) {
      print(response.body);
    } else {
      print('请求成功,但状态码非 200: ${response.statusCode}');
    }
  } catch (e) {
    print('最终请求失败: $e');
  }
}

通过这种方式,我们在使用 http 库时也实现了网络请求重试机制。

高级重试策略

智能重试策略

除了基本的重试次数和重试间隔时间控制,还可以实现更智能的重试策略。例如,根据不同的错误类型动态调整重试次数和重试间隔时间。对于网络超时错误,可以适当增加重试次数,因为这种错误可能是由于网络瞬间波动引起的;而对于服务器返回 503 状态码的情况,可以采用较长的重试间隔时间,因为服务器可能需要一段时间来恢复正常。

class SmartRetryInterceptor extends Interceptor {
  final Map<int, int> errorCodeToRetries;
  final Map<int, Duration> errorCodeToDelay;

  SmartRetryInterceptor({
    this.errorCodeToRetries = const {
      503: 5,
      // 其他错误码及对应的重试次数
    },
    this.errorCodeToDelay = const {
      503: Duration(seconds: 3),
      // 其他错误码及对应的重试间隔时间
    },
  });

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) async {
    int maxRetries = 3;
    Duration retryDelay = const Duration(seconds: 1);
    if (err.type == DioErrorType.connectTimeout ||
        err.type == DioErrorType.sendTimeout ||
        err.type == DioErrorType.receiveTimeout) {
      maxRetries = 5;
    } else if (err.response?.statusCode != null && errorCodeToRetries.containsKey(err.response.statusCode)) {
      maxRetries = errorCodeToRetries[err.response.statusCode]!;
      retryDelay = errorCodeToDelay[err.response.statusCode]!;
    }
    int retryCount = 0;
    while (retryCount < maxRetries) {
      await Future.delayed(retryDelay);
      try {
        final response = await err.requestOptions.retry();
        handler.resolve(response);
        return;
      } catch (e) {
        retryCount++;
      }
    }
    handler.next(err);
  }
}

熔断机制与重试的结合

熔断机制是一种在系统出现故障时防止连锁反应的策略。在网络请求中,可以将熔断机制与重试机制结合使用。当网络请求连续失败达到一定次数时,触发熔断,暂时停止重试,避免大量无效请求对系统造成更大压力。在熔断一段时间后,可以尝试半开状态,即允许少量请求通过,如果这些请求成功,则恢复正常请求;如果仍然失败,则继续保持熔断状态。

class CircuitBreaker {
  int failureCount = 0;
  bool isOpen = false;
  DateTime? openTime;
  final int failureThreshold;
  final Duration openDuration;

  CircuitBreaker({
    this.failureThreshold = 5,
    this.openDuration = const Duration(seconds: 10),
  });

  bool canExecute() {
    if (isOpen) {
      if (DateTime.now().difference(openTime!) > openDuration) {
        isOpen = false;
        failureCount = 0;
        return true;
      } else {
        return false;
      }
    }
    return true;
  }

  void recordFailure() {
    failureCount++;
    if (failureCount >= failureThreshold) {
      isOpen = true;
      openTime = DateTime.now();
    }
  }
}

class CircuitBreakerRetryInterceptor extends Interceptor {
  final CircuitBreaker circuitBreaker;

  CircuitBreakerRetryInterceptor(this.circuitBreaker);

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) async {
    if (!circuitBreaker.canExecute()) {
      handler.next(err);
      return;
    }
    // 重试逻辑
    int retryCount = 0;
    while (retryCount < 3) {
      await Future.delayed(const Duration(seconds: 1));
      try {
        final response = await err.requestOptions.retry();
        handler.resolve(response);
        return;
      } catch (e) {
        retryCount++;
        circuitBreaker.recordFailure();
      }
    }
    circuitBreaker.recordFailure();
    handler.next(err);
  }
}

重试机制对性能和用户体验的影响

性能方面

  1. 资源消耗:重试机制会增加网络请求的次数,从而消耗更多的网络流量。特别是在采用指数退避算法时,随着重试次数的增加,请求间隔时间变长,虽然在一定程度上减少了无效请求的频率,但总体上延长了请求处理的时间,可能会导致应用在这段时间内占用更多的系统资源,如电量消耗增加。
  2. 响应时间:如果重试机制设置不当,例如重试次数过多或者重试间隔时间过长,会显著增加用户等待响应的时间。这对于一些对实时性要求较高的应用,如即时通讯应用、在线游戏等,可能会严重影响用户体验。

用户体验方面

  1. 可靠性提升:合理的重试机制可以在网络波动或服务器短暂故障的情况下,保证网络请求最终能够成功,从而提升应用的可靠性。用户在使用应用时,遇到网络请求失败的情况会减少,这有助于增强用户对应用的信任。
  2. 等待反馈:在重试过程中,需要给用户提供适当的反馈。如果没有任何反馈,用户可能会认为应用出现了卡顿或死机。可以通过显示加载指示器等方式,告知用户请求正在处理中,并且在重试失败时,给出友好的错误提示,引导用户进行相应的操作,如检查网络连接等。

通过以上详细的介绍,我们对基于 Flutter 的网络请求重试机制有了全面的了解,从基本原理到具体实现,再到高级策略以及对性能和用户体验的影响,希望开发者能够根据具体的业务需求,实现合适的网络请求重试机制,提升应用的质量。