如何通过SignalR从服务端触发函数取消上传进度?求替代静态字段方案
基于SignalR的上传进度与取消功能优化方案
原实现使用静态字段IsCancelled管理取消状态,存在多用户状态共享的致命问题,一个用户的取消操作会影响所有正在上传的用户。以下是替代优化方案:
一、重构SignalR Hub:基于连接的独立取消状态
使用并发安全字典存储每个连接的取消令牌源,配合CancellationToken实现规范的任务中断:
using System.Collections.Concurrent; using Microsoft.AspNet.SignalR; namespace Antz.App.SignalR.Hubs { public class ProgressHub : Hub { // 并发安全字典,为每个连接维护独立的取消令牌源 private static readonly ConcurrentDictionary<string, CancellationTokenSource> _connectionCancelTokens = new ConcurrentDictionary<string, CancellationTokenSource>(); /// <summary> /// 向当前连接的客户端推送进度 /// </summary> public void SendProgress(string progressMessage, int progressCount, int totalItems) { var hubContext = GlobalHost.ConnectionManager.GetHubContext<ProgressHub>(); var percentage = (progressCount * 100) / totalItems; // 仅推送给当前发起请求的连接,避免干扰其他用户 hubContext.Clients.Client(Context.ConnectionId).AddProgress(progressMessage, $"{percentage}%"); } /// <summary> /// 处理客户端的取消上传请求 /// </summary> public void RequestUploadCancel() { if (_connectionCancelTokens.TryGetValue(Context.ConnectionId, out var tokenSource)) { tokenSource.Cancel(); _connectionCancelTokens.TryRemove(Context.ConnectionId, out _); } } /// <summary> /// 为当前连接生成取消令牌,供上传任务使用 /// </summary> public CancellationToken GetUploadCancellationToken() { var tokenSource = new CancellationTokenSource(); _connectionCancelTokens.TryAdd(Context.ConnectionId, tokenSource); return tokenSource.Token; } /// <summary> /// 连接断开时清理对应取消令牌,避免内存泄漏 /// </summary> public override Task OnDisconnected(bool stopCalled) { _connectionCancelTokens.TryRemove(Context.ConnectionId, out _); return base.OnDisconnected(stopCalled); } } }
关键改进点:
- 用
ConcurrentDictionary替代静态字段,确保每个用户/连接有独立的取消状态 - 进度推送仅针对当前连接,解决原代码"广播所有用户"的问题
- 使用.NET标准的
CancellationToken管理任务取消,比手动检测布尔值更可靠 - 连接断开自动清理资源,避免内存泄漏
二、前端添加取消操作逻辑
在原有SignalR连接代码基础上,增加取消按钮的事件处理:
// 初始化SignalR连接(原有代码保留) signalrConnection = $.hubConnection("/SignalR", { useDefaultPath: false }); signalrProxy = signalrConnection.createHubProxy('ProgressHub'); signalrProxy.on('AddProgress', function (message, percentage) { UpdateProgressBar(percentage); }); signalrConnection.start({ transport: 'longPolling' }).then(function () { console.log("SignalR连接已启动"); }); signalrConnection.disconnected(function () { console.log('SignalR连接断开'); setTimeout(function () { signalrConnection.start({ transport: 'longPolling' }).then(function () { console.log("SignalR连接已重新启动"); }); }, 3000); }); signalrConnection.error(function (error) { console.error("SignalR错误:", error); }); // 进度条更新逻辑(原有代码保留) function UpdateProgressBar(percentage) { const progressBar = document.querySelector(".progress-bar"); progressBar.style.opacity = "1"; progressBar.style.width = percentage; progressBar.innerHTML = percentage; if (percentage === "100%") { setTimeout(() => { progressBar.style.width = ""; progressBar.innerHTML = ""; }, 1500); } } // 新增:取消上传按钮点击事件 document.getElementById("cancel-upload-btn").addEventListener("click", function() { signalrProxy.invoke('RequestUploadCancel') .then(function() { console.log("取消上传请求已发送"); // 重置进度条状态 const progressBar = document.querySelector(".progress-bar"); progressBar.style.width = "0%"; progressBar.innerHTML = "已取消"; }) .catch(function(error) { console.error("发送取消请求失败:", error); }); });
三、重构上传后端代码:使用取消令牌中断任务
注意:禁止手动实例化ProgressHub,必须通过SignalR HubContext获取上下文,同时绑定取消令牌到上传任务:
// 获取SignalR Hub上下文 var hubContext = GlobalHost.ConnectionManager.GetHubContext<ProgressHub>(); // 从请求头获取前端传递的SignalR连接ID(前端上传时需将connectionId放到请求头或参数中) var connectionId = Request.Headers["SignalR-ConnectionId"].FirstOrDefault(); if (string.IsNullOrEmpty(connectionId)) { return BadRequest("缺少SignalR连接ID"); } // 获取当前连接对应的取消令牌 var cancellationToken = ((ProgressHub)hubContext).GetUploadCancellationToken(); var refNoList = new List<string>(); await Task.Run(async () => { foreach (ArrayList row in data3) { // 检查取消令牌,若已取消则终止流程 if (cancellationToken.IsCancellationRequested) { hubContext.Clients.Client(connectionId).AddProgress("上传已取消", "0%"); break; } recordProcessed++; rowIndex++; // 跳过表头行 if (rowIndex == 1) { continue; } // 向当前连接推送进度 hubContext.Clients.Client(connectionId).AddProgress("处理中...", rowIndex, totalRows); bool all_pass = true; string docType = row[0].ToString().Trim(); string refNo = row[1].ToString().Trim(); string vendor = row[2].ToString().Trim(); string remark = row[3].ToString().Trim(); // 后续业务逻辑... } // 若未被取消,通知前端上传完成 if (!cancellationToken.IsCancellationRequested) { hubContext.Clients.Client(connectionId).AddProgress("上传完成", "100%"); } }, cancellationToken); // 将取消令牌绑定到Task.Run,确保任务能被及时中断
关键改进点:
- 通过HubContext操作SignalR,而非手动实例化Hub(SignalR Hub实例由框架管理,手动实例化会导致状态丢失)
- 上传任务绑定
CancellationToken,一旦触发取消,任务会被立即中断 - 进度推送和状态通知仅针对当前上传用户,避免跨用户干扰
额外优化建议
- 用户身份关联:若系统已实现认证,可使用
Context.User.Identity.Name替代ConnectionId关联取消状态,避免连接重连导致状态丢失 - 后台任务队列:对于长时间运行的上传任务,建议使用Hangfire等后台任务队列处理,避免占用Web请求线程
- 异常处理:在上传任务中添加异常捕获,确保取消操作和异常都能正确通知前端
内容的提问来源于stack exchange,提问作者Man
相关产品推荐
相关产品推荐

