ASP.NET MVC 中SignalR实现实时进度通信的深度解析

发布于:2025-06-19 ⋅ 阅读:(13) ⋅ 点赞:(0)

一、SignalR 是什么?能解决啥问题?

简单说,SignalR 就是一个帮网页实现 “实时聊天” 的工具。以前是客户端不停问服务器要数据(轮询),现在换成服务器主动给客户端发消息,比如后台处理到第 100 条数据了,马上告诉前端更新进度条。

具体解决这 3 个痛点:

  • 进度看得见:批量抓取企业信息时,实时显示 “已处理 200 条,失败 5 条” 这样的动态信息,不再让用户干等。
  • 不卡不费资源:用长连接代替频繁请求,就像开了个热线电话,有消息直接传,省流量还快。
  • 能随时喊停:用户觉得任务太慢想取消?前端点个按钮,后台立刻停止处理,不浪费资源。

二、前端怎么写?一步步跟着来

1. 先准备好 “聊天工具”

在网页里引入 SignalR 的客户端库,就像安装微信才能发消息一样:

@section Scripts{
    <!-- 基础库 -->
    <script src="~/Scripts/jquery.min.js"></script>
    <!-- SignalR客户端,用来连接服务器 -->
    <script src="~/Scripts/jquery.signalR-2.4.3.min.js"></script>
    <!-- 自动生成的"聊天房间"代码,后端定义的ProgressHub会在这里出现 -->
    <script src="~/signalr/hubs"></script>
}

2. 建立连接:先和服务器搭上话

// 初始化连接对象
var hubConnection = $.connection.hub;
// 对应后端的ProgressHub,就像找到对应的聊天房间
var progressHub = $.connection.ProgressHub;

// 告诉前端:当服务器发进度消息时,要做什么
progressHub.client.ReceiveProgress = function (progress) {
    // 更新网页上的进度显示
    $('#progressMessage').html(progress.Message);
    // 任务完成了就关掉进度弹窗
    if (progress.IsCompleted) layer.close(progressLayer);
};

// 启动连接(这里用"长轮询"方式,兼容性好)
function startSignalR() {
    return new Promise((resolve, reject) => {
        if (hubConnection.state === $.signalR.connectionState.connected) {
            resolve(); // 已经连上了就直接下一步
            return;
        }
        // 开始连接,成功了走resolve,失败了走reject
        hubConnection.start({ transport: 'longPolling' })
            .done(resolve)
            .fail(reject);
    });
}

这里用 Promise 是为了保证 “必须先连上服务器,再开始任务”,就像打电话要等对方接通了再说话。

3. 启动任务 + 控制流程:用户点按钮,后台开始干活

function startBatchTask(taskEvent, taskName) {
    // 先弹出一个进度弹窗,告诉用户任务开始了
    progressLayer = layer.open({
        type: 1,
        title: `正在处理${taskName}`,
        content: '<div class="layui-text-center"><p id="progressMessage">加载中...</p></div>'
    });
    // 连接成功后,告诉后端开始任务
    startSignalR().then(() => {
        $.ajax({
            url: `/xxxx/xxxxx/${taskEvent}`,
            success: function (res) {
                if (res.Success) {
                    currentTaskId = res.TaskId; // 记住任务ID,后续用来停任务
                    console.log(`任务开始,ID是${currentTaskId}`);
                } else {
                    layer.close(progressLayer);
                    layer.msg('任务启动失败', { icon: 2 });
                }
            }
        });
    });
}

// 用户关闭弹窗时,终止任务
function handleCloseOperation() {
    if (currentTaskId) {
        // 告诉服务器:把这个ID的任务停掉!
        progressHub.server.stopTask(currentTaskId);
    }
    // 断开连接+刷新表格
    disconnectAndRefresh();
}

这里就像用户点了 “开始下载”,先弹出下载进度条,下载过程中也能随时点 “取消”。

三、后端怎么写?让服务器会 “主动报信”

1. 定义 "聊天房间"Hub:允许前后端互相调用

[HubName("ProgressHub")] // 前端通过这个名字找到我
public class ProgressHub : Hub {
    // 用字典记录每个任务的取消令牌,方便停任务
    private static readonly Dictionary<string, CancellationTokenSource> TaskCancellationTokenSources = new Dictionary<string, CancellationTokenSource>();

    // 前端调用这个方法来停任务
    public async Task StopTask(string taskId) {
        lock (TaskCancellationTokenSources) { // 保证线程安全,别让多个任务抢资源
            if (TaskCancellationTokenSources.TryGetValue(taskId, out var cts)) {
                cts.Cancel(); // 取消正在进行的任务
                TaskCancellationTokenSources.Remove(taskId); // 从字典里删掉,避免内存泄漏
            }
        }
        // 告诉所有客户端(这里其实只需要告诉发起任务的客户端)任务终止了
        await Clients.All.SendAsync("ReceiveProgress", new { TaskId = taskId, Message = "任务已终止" });
    }
}

Hub 就像一个中转站,前端说 “停任务”,Hub 收到后通知后台取消,同时能给前端发消息。

2. 处理任务 + 发进度:后台干活时不忘报信

public JsonResult GetAllEnterpriseBaseInfo() {
    var taskId = Guid.NewGuid().ToString("N"); // 生成唯一任务ID
    // 用Task.Run开一个后台线程处理,别阻塞主线程
    Task.Run(() => ProcessEnterpriseDataAsync(taskId, _enterpriseService.GetFactoryList(), _baseInfoUrl, "企业基本信息"));
    return Json(new { Success = true, TaskId = taskId }); // 告诉前端任务启动成功,给个ID
}

private async Task ProcessEnterpriseDataAsync(string taskId, List<FactoryModel> factoryList, string apiUrl, string taskType) {
    var totalCount = factoryList.Count;
    var processedCount = 0;
    var failedCount = 0;

    try {
        for (int i = 0; i < totalCount; i += 200) { // 分批处理,一次处理200条
            var batch = factoryList.Skip(i).Take(200).ToList();
            // 处理每一条数据...中间省略具体逻辑...

            processedCount += batch.Count; // 记录处理了多少条
            // 每处理10条或者处理完了,就给前端发一次进度
            if (processedCount % 10 == 0 || processedCount == totalCount) {
                SendProgressUpdateAsync(taskId, totalCount, processedCount, failedCount, taskType, processedCount >= totalCount);
            }
        }
    } catch (Exception ex) {
        // 出错了也告诉前端
        SendProgressUpdateAsync(taskId, totalCount, processedCount, totalCount, taskType, true, "任务出错啦:" + ex.Message);
    }
}

private void SendProgressUpdateAsync(string taskId, int total, int done, int failed, string taskName, bool isDone, string msg = null) {
    var hubContext = GlobalHost.ConnectionManager.GetHubContext<ProgressHub>(); // 获取Hub实例
    var message = msg ?? (isDone ? 
        $"完成啦!总共{total}条,失败{failed}条" : 
        $"处理中...已完成{done}/{total}条");
    // 给所有客户端发消息,这里其实应该只发给发起任务的客户端,不过示例简化了
    hubContext.Clients.All.ReceiveProgress(new ProgressUpdate {
        TaskId = taskId,
        Message = message,
        IsCompleted = isDone
    });
}

核心就是:后台每处理一批数据,就调用SendProgressUpdateAsync告诉前端当前进度,就像快递员每到一个站点就更新物流信息。

四、SignalR 背后的原理:到底怎么做到实时的?

1. 连接方式:就像不同的通信工具

SignalR 会根据浏览器能力自动选择最合适的连接方式:

  • WebSocket:现代浏览器首选,像开了个实时聊天窗口,双向通信最快。
  • 长轮询:浏览器不支持 WebSocket 时用,就像客户端问 “有新消息吗?”,服务器没消息就等着,有消息马上回,比频繁轮询省流量。
  • 还有一些兼容旧浏览器的方式,比如 IFrame,不过现在用得少了。

2. 双向通信:前后端能互相调用

  • 前端可以调用后端 Hub 里的方法,比如progressHub.server.stopTask(),就像给服务器发了一条指令:“把这个任务停了”。
  • 后端可以调用前端的方法,比如hubContext.Clients.All.ReceiveProgress(),相当于服务器给前端发消息:“进度更新啦,快显示给用户看”。

3. 异步处理:后台干活不阻塞

Task.Runasync/await让耗时操作(比如网络请求、数据库插入)在后台线程执行,就像你一边下载文件一边浏览网页,互不影响,服务器能同时处理更多任务。

五、为啥非得用 SignalR?对比一下就知道

方案 优点 缺点 适合场景
轮询 简单,兼容性好 巨费流量,巨慢 数据更新极慢场景
WebSocket 最快,实时性最强 自己处理兼容性超麻烦 高实时性需求
SignalR 自动适配连接方式,代码少 多引入一个库(很小) 90% 的实时场景

简单说:SignalR 帮你把复杂的底层通信搞定了,你只需要关注 “什么时候该发进度” 和 “收到进度怎么显示”,开发效率飙升!

六、效果图长啥样?交互流程走一遍

1. 大概长这样(示意图):

在这里插入图片描述
(实际图会有 “开始任务” 按钮、进度文字、取消按钮,任务完成后自动关闭)

2. 用户操作步骤:

  • 点击 “一键获取企业信息” → 弹出进度弹窗,显示 “正在连接服务器”。
  • 连接成功后,后台开始处理,弹窗显示 “已处理 50 条 / 1000 条”。
  • 处理过程中用户可点击弹窗关闭按钮 → 后台任务终止,弹窗显示 “任务已取消”。
  • 任务正常完成后,弹窗自动关闭,数据表格刷新显示最新结果。

七、新手注意!这些坑别踩

  • 连接失败要重试:网络不好时可能连接不上,前端加个重试逻辑,比如 3 秒后再连一次。
  • 别发太多消息:后台每处理 10 条发一次进度就够了,别每条都发,不然前端会卡。
  • 任务 ID 要保密:别把任务 ID 随便暴露,防止有人恶意终止别人的任务,后端停任务前检查用户权限。
  • 服务端客户端事件名称要一致:服务端发送的方法要和页面注册的名称一致,要不然发送不了
  • 旧浏览器兼容:如果用户用 IE8 之类的古董,可能需要启用 Forever Frame 模式,但现在基本没人用了,建议引导用户升级浏览器。
  • -先连接后通信:千万要记住先要通了话,才能说话。要不然即使后端发送了消息,等前端连接上也接收不到,消息就会报废掉

总结:SignalR 就是实时通信的 “懒人工具”

用 SignalR,就像点外卖用美团,不用自己找骑手,平台都帮你搞定。前端简单几行代码就能接收进度,后端调用 Hub 发消息就行,开发效率翻倍,用户体验还好。下次遇到需要实时更新的场景,直接上 SignalR 准没错!


网站公告

今日签到

点亮在社区的每一天
去签到