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

ASP.NET Core 6 MVC:如何在视图中实时展示定时推送的事件数据?

解决方案与最佳实践

在传统HTTP请求响应模型中,Web视图无法主动接收服务器推送的实时数据,因此需要借助实时通信技术实现需求。以下是针对场景的具体实现方案及最佳实践:

一、核心问题修正:避免在Controller中注册事件

你当前在HomeController中监听Connector事件的方式存在风险:

  • HomeController是Scoped服务(每次请求都会实例化),多次请求会重复注册事件,导致同一数据变更触发多次处理。
  • Controller被回收后,若Connector为Singleton,会持有Controller的引用,引发内存泄漏。

正确做法:使用Singleton/HostedService监听Connector事件,确保事件只注册一次且能安全清理。


二、推荐方案:SignalR(ASP.NET官方实时通信框架)

SignalR是处理实时双向通信的最佳选择,支持自动重连、分组推送、跨平台等特性,完美适配你的场景。

1. 基础配置

安装SignalR包

dotnet add package Microsoft.AspNetCore.SignalR

注册服务与中间件

在Program.cs中添加:

// 注册Connector为Singleton(确保全局唯一实例)
builder.Services.AddSingleton<Connector>();

// 注册SignalR服务
builder.Services.AddSignalR();

// 注册HostedService用于监听Connector事件
builder.Services.AddHostedService<DataChangeListener>();

// 配置中间件,映射SignalR Hub
app.MapHub<DataHub>("/dataHub");

2. 实现事件监听与Hub通信

创建HostedService处理事件

public class DataChangeListener : IHostedService
{
    private readonly Connector _connector;
    private readonly IHubContext<DataHub> _hubContext;

    public DataChangeListener(Connector connector, IHubContext<DataHub> hubContext)
    {
        _connector = connector;
        _hubContext = hubContext;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        // 注册Connector的DataChanged事件
        _connector.DataChanged += OnDataChanged;
        return Task.CompletedTask;
    }

    private async void OnDataChanged(object data, string nodeId)
    {
        // 向所有连接的客户端推送数据
        await _hubContext.Clients.All.SendAsync("ReceiveData", data?.ToString(), nodeId);
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        // 取消事件订阅,避免内存泄漏
        _connector.DataChanged -= OnDataChanged;
        return Task.CompletedTask;
    }
}

创建SignalR Hub

public class DataHub : Hub
{
    // 可扩展:添加客户端订阅特定NodeId的方法,实现按需推送
    public async Task SubscribeToNode(string nodeId)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, nodeId);
    }
}

3. 前端视图实现数据展示

在Home/Index.cshtml中添加:

<!-- 数据展示容器 -->
<div id="data-container"></div>

<!-- 引入SignalR客户端库(本地文件) -->
<script src="~/lib/signalr/dist/browser/signalr.js"></script>
<script>
    // 建立SignalR连接
    const connection = new signalR.HubConnectionBuilder()
        .withUrl("/dataHub")
        .build();

    // 监听服务器推送的ReceiveData事件
    connection.on("ReceiveData", function (data, nodeId) {
        // 更新UI:存在对应NodeId的元素则更新,否则创建新元素
        const targetElement = document.getElementById(`data-${nodeId}`);
        if (targetElement) {
            targetElement.textContent = data;
        } else {
            const newElement = document.createElement("div");
            newElement.id = `data-${nodeId}`;
            newElement.innerHTML = `<strong>${nodeId}:</strong> ${data}`;
            document.getElementById("data-container").appendChild(newElement);
        }
    });

    // 启动连接,处理错误
    connection.start().catch(err => console.error(err.toString()));
</script>

三、备选方案:Server-Sent Events(SSE)

若只需单向服务器推送(无需客户端向服务器发送指令),SSE是更轻量的选择,基于HTTP协议,无需额外框架。

1. 后端实现SSE端点

[ApiController]
[Route("api/data-stream")]
public class DataStreamController : ControllerBase
{
    private readonly Connector _connector;
    private readonly ConcurrentDictionary<string, Action<object, string>> _subscribers = new();

    public DataStreamController(Connector connector)
    {
        _connector = connector;
        _connector.DataChanged += OnDataChanged;
    }

    private void OnDataChanged(object data, string nodeId)
    {
        foreach (var handler in _subscribers.Values)
        {
            handler(data, nodeId);
        }
    }

    [HttpGet]
    public async Task Get(CancellationToken cancellationToken)
    {
        // 设置SSE响应头
        Response.Headers.Add("Content-Type", "text/event-stream");
        Response.Headers.Add("Cache-Control", "no-cache");
        Response.Headers.Add("Connection", "keep-alive");

        var subscriberId = Guid.NewGuid().ToString();
        // 定义数据推送逻辑
        Action<object, string> handler = async (data, nodeId) =>
        {
            var message = $"data: {JSON.stringify({ Data: data?.ToString(), NodeId: nodeId })}\n\n";
            await Response.WriteAsync(message, cancellationToken);
            await Response.Body.FlushAsync(cancellationToken);
        };

        _subscribers.TryAdd(subscriberId, handler);

        try
        {
            // 保持连接活跃
            while (!cancellationToken.IsCancellationRequested)
            {
                await Task.Delay(1000, cancellationToken);
            }
        }
        finally
        {
            // 移除订阅,清理资源
            _subscribers.TryRemove(subscriberId, out _);
        }
    }
}

2. 前端监听SSE流

<div id="data-container"></div>
<script>
    const eventSource = new EventSource("/api/data-stream");
    
    eventSource.onmessage = function(event) {
        const { Data, NodeId } = JSON.parse(event.data);
        // 更新UI逻辑同SignalR示例
        const targetElement = document.getElementById(`data-${NodeId}`);
        if (targetElement) {
            targetElement.textContent = Data;
        } else {
            const newElement = document.createElement("div");
            newElement.id = `data-${NodeId}`;
            newElement.innerHTML = `<strong>${NodeId}:</strong> ${Data}`;
            document.getElementById("data-container").appendChild(newElement);
        }
    };

    eventSource.onerror = function(error) {
        console.error("SSE连接异常:", error);
        eventSource.close();
    };
</script>

四、最佳实践总结

  1. 避免Scoped服务订阅Singleton事件:使用HostedService或Singleton服务处理事件监听,防止重复注册与内存泄漏。
  2. 优先选择SignalR:对于需要双向通信或复杂场景,SignalR提供了更完善的连接管理、分组推送等能力。
  3. 数据序列化规范:若传递复杂对象,定义DTO类确保可序列化,避免JSON序列化异常。
  4. 资源清理:在服务停止时取消事件订阅,释放资源。
  5. 按需推送优化:若无需全局推送,使用SignalR的分组功能,只向订阅特定NodeId的客户端发送数据。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 10:55:20