You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过SignalR结合.NET Core API与React客户端避免504网关超时错误

大数据CSV导入时504网关超时问题及SignalR优化尝试

问题背景

我有一个用于上传CSV文件导入数据的接口,小数据集请求可正常完成,但处理大数据集时,客户端会触发504网关超时错误。此时服务器仍在后台继续完成数据入库,但客户端会显示空白页面。

为了维持客户端与服务器的连接,我尝试用SignalR实现心跳机制,目前已成功建立WebSocket连接,客户端每5秒发送ping消息,服务器也能正常响应,但504超时问题依然存在。

当前实现代码

.NET Core 后端实现

Program.cs 配置

...

builder.Services.AddCors(options =>
{
    options.AddPolicy("WebSocketsClientPermission", policy => {
        policy
            .WithOrigins("https://localhost:5001")
            .AllowAnyHeader()
            .AllowAnyMethod()
            .AllowCredentials();
    });
});

builder.Services.AddSignalR(options =>
{
    options.EnableDetailedErrors = true;
});

builder.Services.AddSingleton<SharedConnections>();

app.UseCors("WebSocketsClientPermission");

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller}/{action=Index}/{id?}");

    endpoints.MapHub<WebSocketHub>("/PingServer");
});

...

SharedConnections 类(跟踪连接)

using System.Collections.Concurrent;

public class SharedConnections
{
    private readonly ConcurrentDictionary<string, WebSocketConnection> _connections = new();

    public ConcurrentDictionary<string, WebSocketConnection> Connections => _connections;
}

WebSocketConnection 类

public class WebSocketConnection
{
    public string ConnectionId { get; set; } = string.Empty;
}

WebSocketHub 类

using Microsoft.AspNetCore.SignalR;

public class WebSocketHub : Hub
{
    private readonly SharedConnections _sharedConnections;

    public WebSocketHub(SharedConnections sharedConnections) => _sharedConnections = sharedConnections;

    public async Task StartConnection(WebSocketConnection conn)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, conn.ConnectionId);
        _sharedConnections.Connections[Context.ConnectionId] = conn;
        await Clients.Groups(conn.ConnectionId).SendAsync("StartConnection", $"Connection {conn.ConnectionId} has started...");
    }

    public async Task PingServerWithMessage()
    {
        if(_sharedConnections.Connections.TryGetValue(Context.ConnectionId, out WebSocketConnection conn))
        {
            await Clients.Group(conn.ConnectionId).SendAsync("PingServerWithMessage", $"Pinging connection {conn.ConnectionId} from server...");
        }
    }
}

React 前端实现

App.js(SignalR连接初始化)

...
import { HubConnectionBuilder, LogLevel } from "@microsoft/signalr"

const [webSocketConnection, setWebSocketConnection] = useState()

...

const startConnection = async (connectionId) => {
    try {
        const connection = new HubConnectionBuilder()
                    .withUrl(`${process.env.REACT_APP_URL}/PingServer`)
                    .configureLogging(LogLevel.Information)
                    .build()

        connection.on("StartConnection", (message) => {
            console.log("Message:", message)
        })

        connection.on("PingServerWithMessage", (message) => {
            console.log("Message:", message)
        })

        await connection.start()
        connectionId = connectionId.toString()
        await connection.invoke("StartConnection", {connectionId})

        setWebSocketConnection(connection)
    } catch(error) {
        console.log("Error:", error)
    }
}

ImportData 组件(CSV上传处理)

const ImportData = ({..., webSocketConnection, startConnection }) => {

...

  const [isLoading, setIsLoading] = useState(false)
  const [startPing, setStartPing] = useState(false)
  const MINUTE_MS = 5000

  useEffect(() => {
    let connectionId = Math.floor(Math.random() * 100000)
    startConnection(connectionId)
  }, [])

  const sendMessage = async() => {
    try {
      await webSocketConnection.invoke("PingServerWithMessage")
    } catch(error) {
      console.error("Error:", error)
    }
  }

  useEffect(() => {
    if (webSocketConnection) {
      const interval = setInterval(async () => {
        await sendMessage()
      }, MINUTE_MS)
  
      return () => clearInterval(interval)
    }
    
  }, [startPing])

  const handleFileUpload = async (file) => {
    setIsLoading(true)
    ...
        try {
          setStartPing(true)
          const upload = await apiCalls.postFile(...) // 同步上传API调用
          setIsLoading(false)
          setTimeout(() => {
            navigate(`/main_page`)
          }, 3000)
        } catch (error) {
          console.error("Error:", error)
          ...
        }
      }
    } else {
      ...
    }
  }

  return (
    <>
      {/* 点击提交按钮触发handleFileUpload */}
    </>
  )
}

核心疑惑

原以为只要WebSocket连接保持开放并互发ping,就能避免HTTP上传请求的超时,但目前仅在前端打印服务器响应日志,不确定是否需要通过状态变量捕获消息来维持连接,不知道下一步该如何调整。

明确需求

执行API上传时,客户端与服务器按5秒间隔互发ping消息维持连接,直到API返回成功/错误响应后,关闭WebSocket连接并跳转至主页,避免504超时导致的空白页面。

解决方案建议

1. 区分WebSocket心跳与HTTP请求超时的本质

504超时是同步HTTP上传请求本身的超时,和WebSocket连接无关。当前的SignalR心跳仅维持了WebSocket连接,但并没有延长HTTP请求的超时时间,网关/浏览器依然会因为HTTP请求长时间无响应而触发504。

2. 调整全链路的超时配置

  • 前端:在axios/fetch中设置更长的超时时间,比如:
    // axios示例
    const upload = await apiCalls.postFile(file, { timeout: 300000 }) // 5分钟超时
    
  • 后端:在.NET Core中调整服务器和接口的超时设置:
    // 全局Kestrel超时配置
    builder.Services.Configure<KestrelServerOptions>(options =>
    {
        options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(5);
    });
    
    // 单个接口超时配置
    [HttpPost]
    [RequestTimeout("00:05:00")]
    public async Task<IActionResult> ImportCsv(IFormFile file)
    {
        // 导入逻辑
    }
    
  • 网关(如Nginx):如果使用反向代理,调整以下参数:
    proxy_read_timeout 300s;
    proxy_connect_timeout 300s;
    

3. 优化SignalR心跳机制

当前代码中,心跳的启动依赖startPing状态,可能导致连接建立后心跳未及时启动。建议调整useEffect的依赖为webSocketConnection,确保连接建立后立即启动心跳:

useEffect(() => {
  if (webSocketConnection) {
    const interval = setInterval(async () => {
      await sendMessage()
    }, MINUTE_MS)

    return () => clearInterval(interval)
  }
}, [webSocketConnection]) // 改为依赖webSocketConnection

不需要通过状态捕获服务器消息来维持连接,只要WebSocket的ping/pong正常,连接就会保持,但这无法解决HTTP请求的超时问题。

4. 改用异步导入模式(推荐方案)

大数据导入不适合同步HTTP请求,更可靠的方案是:

  1. 客户端上传CSV后,服务器立即返回一个导入任务ID,结束HTTP请求
  2. 客户端通过SignalR订阅该任务的进度/完成状态
  3. 服务器后台异步处理导入,完成后通过SignalR通知客户端
  4. 客户端根据通知跳转页面,彻底避免HTTP超时

后端调整示例

public class WebSocketHub : Hub
{
    // 订阅导入任务
    public async Task SubscribeToImportTask(string taskId)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, $"ImportTask_{taskId}");
    }

    // 后台导入完成后调用此方法通知客户端
    public async Task NotifyImportComplete(string taskId, bool success)
    {
        await Clients.Group($"ImportTask_{taskId}").SendAsync("ImportComplete", success);
    }
}

前端调整示例

const handleFileUpload = async (file) => {
  setIsLoading(true)
  try {
    // 上传文件获取任务ID,HTTP请求立即返回
    const { data: taskId } = await apiCalls.uploadCsv(file)
    // 订阅任务状态
    await webSocketConnection.invoke("SubscribeToImportTask", taskId)
    // 监听完成通知
    webSocketConnection.on("ImportComplete", (success) => {
      setIsLoading(false)
      if (success) {
        navigate("/main_page")
      } else {
        // 处理导入失败逻辑
      }
    })
  } catch (error) {
    console.error(error)
    setIsLoading(false)
  }
}

5. 前端临时容错处理

在HTTP请求超时后,不要直接显示空白页面,保留加载状态,通过SignalR主动查询或监听服务器的导入状态,直到获取最终结果。


内容的提问来源于stack exchange,提问作者SigmaScout_12

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 05:39:53