败走麦城——HTTP 请求触发 RST

398 阅读7分钟

HTTP 请求触发 RST 排查

1.问题描述

通过 dio 实现 HTTP 下位机控制功能的场景中,发现对某个控制接口短时间连续发送请求控制指令无法生效,进行抓包排查发现存在 RST 报文;

问题报文

通过命令筛选关键数据包 通过筛选 tcp 连接可查看所有 TCP 报文

(ip.addr == 192.168.0.77 && ip.addr == 192.168.0.75) && tcp

分包筛选双方通信查看对应连接过程

ip.src = 192.168.0.77 and ip.dst == 192.168.0.75
ip.src == 192.168.0.75 and ip.dst == 192.168.0.77

关键包如下

rst触发.png

根据报文数据,关键流程如下(数据时间单位为秒):

  • Packet 2315(6.016444s):客户端(192.168.0.77)发送 PUT 请求到服务器(192.168.0.75)。
  • Packet 2320(6.036431s):服务器发送 HTTP 200 OK 响应,此时连接仍处于活动状态。
  • Packet 2322(6.038660s):客户端发送 GET 请求(/query/state)。尝试复用同一 TCP 连接进行新的请求。
  • Packet 2323(6.039143s):服务器发送 FIN, ACK,开始关闭该连接(即主动半关闭,表示服务器不再发送数据)。
  • Packet 2324-2325(6.039230s、6.039368s):客户端对服务器 FIN 的响应,发送 ACK 和 FIN, ACK,表示自己也关闭发送端。
  • Packet 2326-2328(6.040691s):服务器发送 RST,重置该连接。
sequenceDiagram
    participant C as 客户端 (10.40.84.77)
    participant S as 服务器 (10.40.82.75)
    
    C->>S: PUT /put/ctrl (Packet 2315)
    S-->>C: HTTP 200 OK (Packet 2320)
    C->>S: GET /query/state (Packet 2322)
    S-->>C: FIN, ACK (Packet 2323)
    C->>S: ACK (Packet 2324)
    C->>S: FIN, ACK (Packet 2325)
    S-->>C: RST (Packet 2326-2328)

主要问题:

  • 客户端在服务器已发 FIN 后仍然发送新的请求(GET 请求)。
    TCP 协议规定,一旦一端发送 FIN 表示该端不再发送数据,新的请求不应在该连接上发送。客户端继续使用该连接发送数据,会被服务器视为协议违规,导致服务器立即发送 RST 重置连接。

  • 引发原因:

    • 连接管理不当:客户端在收到 FIN(或检测到连接半关闭)后,仍然尝试复用同一连接。
    • 缺少状态检测:客户端没有检测到连接状态已不再适合发送新请求,而没有新建连接。

端口分析:

服务器在包 2323 中已经关闭了写端(发送 FIN),表示不再接收新数据。如果客户端在此后尝试在同一连接上发送 GET 请求,就会违反 TCP 协议,导致服务器直接回复 RST 来拒绝数据。

  • 协议状态错误:客户端在未检测到服务器已经关闭连接情况下,依然使用重复端口请求连接,从而引发 RST。
  • 资源清理问题:客户端如果没有正确管理连接的生命周期,也可能导致误用已经关闭的连接。
sequenceDiagram
    participant C as 客户端 (192.168.0.77, SrcPort:55750)
    participant S as 服务端 (192.168.0.75, DestPort:8080)

    C->>S: SYN (建立连接)
    S-->>C: SYN, ACK
    C->>S: ACK
    Note over C,S: 连接建立成功

    C->>S: PUT /put/ctrl (使用端口 55750)
    S-->>C: HTTP 200 OK
    Note over S: 服务器处理完请求后,开始关闭连接
    S-->>C: FIN, ACK (关闭发送方向)  包 2323

    Note over C: 客户端应检测到 FIN,停止发送数据并新建连接
    C->>S: GET /query/state (重用端口 55750)  包 2322
   
    Note over S: 服务端检测到异常数据(在半关闭连接上收到数据)
    S-->>C: RST (重置连接,拒绝异常数据) 包 2326-2328

根据抓包数据,关键步骤如下:

  1. 连接建立与数据传输:
    • 步骤 1-3 :TCP 三次握手成功建立连接。
    • 步骤 5:客户端发送 PUT 请求,服务端返回 HTTP 200 OK。
  2. 服务端发起断开:
    • 步骤 6:服务端发送 FIN, ACK(Seq=112, Ack=317),表示服务端主动关闭写方向,开始半关闭连接。
  3. 客户端异常行为:
    • 步骤 7:客户端在收到 FIN 后,依然发送新的 GET 请求(GET /unico/v1/interface/io-state?ioId=25602)。
  4. 服务端的响应:
    • 步骤 8:服务端收到在已半关闭连接上收到数据后,立即发送 RST(重置)报文,拒绝非法数据。

分析

  • 协议违例: TCP 协议规定,当一方发送 FIN 后,该方向对方表示不再发送数据。如果客户端在接收到 FIN 后继续发送数据(如 GET 请求),则会违反协议,导致对方直接回复 RST,重置连接。
  • 客户端处理问题: 客户端未能正确检测到连接已进入半关闭状态(收到 FIN),仍然复用该连接发送新的请求,最终引发服务端 RST。
  • 引发原因:
    • 服务端本身未支持 TCP 长链接 ,在处理单次请求后便主动关闭 scoket 通信(本身有一定延迟),在完全建立四次挥手关闭当前连接时,客户端使用上一次端口尝试下发数据被服务端视为无效请求;
    • 客户端可能没有在网络库中对 FIN 信号做出正确反应。
    • 或者存在连接复用逻辑错误,导致错误地继续使用已经关闭的连接。

2.解决策略

客户端优化策略

1. 正确处理 FIN 信号

  • 检测连接状态
    • 在接收到 FIN 信号后,立即将连接标记为不可用,避免继续使用该连接发送新请求;
    • 使用 HTTP 客户端库时,确保其内部状态管理能检测到连接终止(如检查 TCP 状态或捕获 HTTP 错误);
  • 自动重建连接
    • 当检测到连接已半关闭或失效时,自动新建一个 TCP 连接进行后续请求;

2. 连接管理改进

  • TCP 连接状态检测
    • 在 HTTP 客户端封装中增加对 TCP 连接状态的检测,通过底层网络库或捕获异常(如 RST)触发重连;
    • 如果使用 Dio 或其他网络库,确保它们在出现 RST 后能自动重新建立连接;
  • 连接复用策略
    • 在发送新请求前,检查现有连接的状态,避免重用已关闭的连接;
    • 如果检测到连接已关闭,则自动重建新连接;

3. 增强错误处理

  • RST 错误处理
    • 增加重试逻辑,在收到 RST 错误时主动触发重连或请求重发;
    • 记录详细日志,便于快速定位问题;

服务端优化策略

1. 记录和监控异常连接

  • 日志记录
    • 增加日志记录,监控来自同一源端口的重复请求情况,分析客户端是否存在连接复用问题;
  • 实时监控
    • 使用监控工具(如 Prometheus、Grafana)实时跟踪 RST 事件和连接异常情况;

2. 连接超时策略

  • 严格超时配置
    • 设定更严格的连接超时策略,确保在收到 FIN 后尽快关闭连接,减少异常数据包到达的可能性;

3. 返回明确错误码

  • HTTP 错误码
    • 在检测到非法数据包时,返回适当的 HTTP 错误码(如 400 Bad Request),而不是立即重置连接;
    • 注意:此操作可能违背 TCP 协议规定,需权衡使用;

4. 连接管理优化

  • 客户端状态管理
    • 建议客户端做好连接状态管理,确保服务端收到的数据都是在有效连接内的;
  • 快速回收端口
    • 启用 SO_REUSEADDR,允许服务端直接复用处于 TIME_WAIT 的端口;
优化层级措施技术实现适用场景
客户端连接池+自动重试OkHttp/Dio连接池配置移动端/高并发Web服务
服务端日志监控+超时策略Prometheus/Grafana监控工具服务器集群/边缘计算节点

通过以上策略,可以有效降低 RST 错误的发生率,提升系统的稳定性和用户体验。

5.关键改动

我们假定 服务端不遵循Connection: close 最坏场景改动,如果支持实际只要通过 options.headers!['Connection'] = 'close' 即可很大程度解决此问题(未验证,从源码角度问题可行性较高)

_refreshClient()
    ├─ HttpClient().close(force: true) // 强制释放旧连接
    └─ new HttpClient() // 创建新连接实例
class PortResetClient {
  final Dio _dio = Dio();
  HttpClient? _currentClient;

  PortResetClient() {
    _refreshClient();
  }

  // 重置连接,用于短链接场景
  void _refreshClient() {
    // 强制释放旧连接
    _currentClient?.close(force: true);
    // 创建新连接实例
    _currentClient =
        HttpClient()
          ..connectionTimeout = Duration(seconds: 3)
          ..idleTimeout = Duration(seconds: 1); // 缩短空闲超时

    _dio.httpClientAdapter = IOHttpClientAdapter(
      createHttpClient: () => _currentClient!,
    );
  }

  Future<Response> safeRequest(
    String url, {
    Object? data,
    bool resetClient = false,
    Options options,
  }) async {

    try {
      // 增加判定重置端口
      if (resetClient) {
        options.headers!['Connection'] = 'close';
        _refreshClient();
      }

      return await _dio.post(
        url,
        data: data,
        options: options,
      );
    } on DioError catch (e) {
      log.error('DioError: $e');
      ///其他容错逻辑
      rethrow;
    }
  }
}

flowchart TD
    %% 垂直排列主流程
    subgraph MainFlow[请求处理流程]
        direction TB
        Start([开始请求]) --> CheckReset{需要重置连接?}
        
        CheckReset -- 是 --> ShortFlow
        CheckReset -- 否 --> LongFlow
        
        ShortFlow --> Send
        LongFlow --> Send
        
        Send --> SuccessCheck{请求成功?}
    end

    %% 短连接处理分支
    subgraph ShortFlow[短连接模式]
        direction TB
        SetShort[设置Connection:close] --> RefreshClient[刷新客户端连接] 
        RefreshClient --> BuildUrl[构建唯一URL参数] 
    end

    %% 长连接处理分支
    subgraph LongFlow[长连接模式]
        direction TB
        KeepAlive[设置Connection:keep-alive] --> BuildNormalUrl[使用标准URL] 
  
    end



    %% 连接线优化
    Send -->|HTTP请求| SuccessCheck
    SuccessCheck -- 是 --> EndSuccess([成功结束])

    %% 样式定义
    classDef decision fill:#FFEECC,stroke:#FF9933
    classDef action fill:#E6F3FF,stroke:#3399FF
    classDef endpoint fill:#F5F5F5,stroke:#666
    
    class CheckReset,SuccessCheck,ErrorCheck decision
    class SetShort,RefreshClient,BuildUrl,KeepAlive,BuildNormalUrl,ResetRetry action
    class Start,EndSuccess,EndFailure endpoint

    %% 隐藏中间连接点
    linkStyle 3 stroke-width:0;
    linkStyle 4 stroke-width:0;

3. 结论

主要问题在于服务端本身未支持长链接,而客户端未正确处理服务器发送的 FIN 信号,错误地在半关闭连接通道上继续发送数据,从而引发服务端返回 RST。

优化建议:

  • 客户端:
    • 及时检测 FIN 信号,停止使用已关闭连接。
    • 新请求应新建连接,确保不会发送到半关闭连接。
    • 增加错误处理机制,捕获 RST 后重建连接。
  • 服务端:
    • 按照 TCP 标准发送 RST,但通过日志记录帮助发现客户端异常行为。

这种优化能确保连接管理正确、网络通信稳定,减少因为连接状态异常引发的重置问题,进而提升整体多端智能反馈系统的可靠性。