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

如何通过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,一旦触发取消,任务会被立即中断
  • 进度推送和状态通知仅针对当前上传用户,避免跨用户干扰

额外优化建议

  1. 用户身份关联:若系统已实现认证,可使用Context.User.Identity.Name替代ConnectionId关联取消状态,避免连接重连导致状态丢失
  2. 后台任务队列:对于长时间运行的上传任务,建议使用Hangfire等后台任务队列处理,避免占用Web请求线程
  3. 异常处理:在上传任务中添加异常捕获,确保取消操作和异常都能正确通知前端

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 07:05:55