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

Azure SignalR限制用户多标签页重复连接异常排查

问题:SignalR多标签页连接管理异常,奇数新标签页无法断开旧连接

我使用SignalR从服务器向特定登录用户发送消息,用户在不同标签页打开同一URL会产生额外Azure SignalR连接,增加成本。于是实现了新标签页打开时关闭旧连接的逻辑,能正常识别用户,但出现奇怪现象:第1、3、5等奇数个新标签页不会断开旧连接,第2、4等偶数个可以。最初用字典存储连接信息,改用MemoryCache后问题依旧。


前端代码

<script>
    const userIdentifier = '@objectIdClaim';//users unique id

    const connection = new signalR.HubConnectionBuilder()
        .withUrl(`/webhub?username=${userIdentifier}`, { accessTokenFactory: () => this.loginToken })
        .configureLogging(signalR.LogLevel.Trace) 
        .build();

    // Handle server messages
    connection.on("SendMessageToUser", async (message) => {
        console.log("Received message from server:", message);
    });

    //handle opening new tab
    connection.on("ForceDisconnect", () => {
        connection.stop(); // Stop the SignalR connection
        showNotification("You have been disconnected, due to the new login");
    });

    // Stop connection when the user leaves the page
    window.addEventListener("beforeunload", async () => {
        await connection.stop();
        console.log("SignalR web connection stopped.");
    });

    // Start the connection
    (async () => {
        try {
            await connection.start();
            console.log("SignalR connection established.");
        } catch (e) {
            console.error("SignalR connection error:", e.toString());
        }
    })();

    function showNotification(message) {
        //pops up modal
    }

</script>

后端WebHub代码

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Diagnostics;
using System.Threading.Tasks;

namespace Something
{
    [Authorize]
    public class Webhub : Hub
    {
        // track user connections
        private readonly IMemoryCache _cache;
        public Webhub(IMemoryCache cache)
        {
            _cache = cache;
        }
    
        public override async Task OnConnectedAsync()
        {
            var userId = Context.UserIdentifier;
            if (userId == null)
            {
                // No UserIdentifier available, disconnect this connection
                Context.Abort();
                return;
            }

            var connectionId = Context.ConnectionId;

            // Check if the user already has an active connection
            if (_cache.TryGetValue(userId, out string existingConnectionId))
            {
                if (existingConnectionId != connectionId) // New connection attempt
                {

                    Debug.WriteLine($"User {userId} connected BEFORE connection ID {existingConnectionId} connecting NOW {connectionId}");
                    // Notify and disconnect the old connection
                    await DisconnectOldConnection(existingConnectionId);
                }
            }

            _cache.Set(userId, connectionId, TimeSpan.FromHours(1));       

            Debug.WriteLine($"User {userId} connected with connection ID {connectionId}.");
            await base.OnConnectedAsync();
        }

        public override async Task OnDisconnectedAsync(Exception? exception)
        {
            var userId = Context.UserIdentifier;
            if (userId != null)
            {
                // Remove the user's connection when they disconnect
                _cache.Remove(userId);
                Debug.WriteLine($"User {userId} disconnected.");
            }

            await base.OnDisconnectedAsync(exception);
        }

        private async Task DisconnectOldConnection(string connectionId)
        {
            // This will disconnect the client with the given connectionId
            // The client handles disconnection on the client-side
            await Clients.Client(connectionId).SendAsync("ForceDisconnect");
        }
    }
}

异常日志

User f1091f94-ae9b-42ad-a96c-3ed2ab309938 connected with connection ID Ii9pOdJti4lRyA8QdG5zWAZq3viAr02.
User f1091f94-ae9b-42ad-a96c-3ed2ab309938 connected BEFORE connection ID Ii9pOdJti4lRyA8QdG5zWAZq3viAr02 connecting NOW fUpzQDu-Itg7yu-z4reQ_QZq3viAr02
User f1091f94-ae9b-42ad-a96c-3ed2ab309938 connected with connection ID fUpzQDu-Itg7yu-z4reQ_QZq3viAr02.
User f1091f94-ae9b-42ad-a96c-3ed2ab309938 disconnected.
User f1091f94-ae9b-42ad-a96c-3ed2ab309938 connected with connection ID R_qLkwKtxuE7nLE0TniVbQZq3viAr02.
User f1091f94-ae9b-42ad-a96c-3ed2ab309938 connected BEFORE connection ID R_qLkwKtxuE7nLE0TniVbQZq3viAr02 connecting NOW t3mNN20gUdhOK-UXIH5TQQZq3viAr02
User f1091f94-ae9b-42ad-a96c-3ed2ab309938 connected with connection ID t3mNN20gUdhOK-UXIH5TQQZq3viAr02.
User f1091f94-ae9b-42ad-a96c-3ed2ab309938 disconnected.
User f1091f94-ae9b-42ad-a96c-3ed2ab309938 connected with connection ID F9ne7QxXFTH1DZYs3lvVOQZq3viAr02.
User f1091f94-ae9b-42ad-a96c-3ed2ab309938 connected BEFORE connection ID F9ne7QxXFTH1DZYs3lvVOQZq3viAr02 connecting NOW 645t2jvOMbLgeJCVp6Gm5QZq3viAr02
User f1091f94-ae9b-42ad-a96c-3ed2ab309938 connected with connection ID 645t2jvOMbLgeJCVp6Gm5QZq3viAr02.
User f1091f94-ae9b-42ad-a96c-3ed2ab309938 disconnected.

Startup.cs配置代码

services.AddSignalR().AddAzureSignalR(Configuration["SignalUrl"]);
services.AddSingleton<IUserIdProvider, MyCustomProvider>();

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

    endpoints.MapHub<Webhub>("/webhub");
})

问题原因分析

  1. 并发竞争冲突:新标签页建立连接时,旧连接收到ForceDisconnect指令后触发OnDisconnectedAsync,直接删除缓存中该用户的记录。此时新连接刚把自身ID写入缓存,就被旧连接的删除操作清空,导致下一次新连接时缓存无记录,无法触发断开逻辑,形成奇数标签页无法断开的循环。
  2. 缓存清理逻辑错误:OnDisconnectedAsync中无条件删除用户缓存的做法不合理,若用户已有新连接活跃,会误删新连接的记录。

修复方案

方案1:调整缓存逻辑,解决并发冲突

修改OnDisconnectedAsync,仅当缓存中的连接ID与当前断开的ID一致时才删除记录,避免误删新连接的缓存:

public override async Task OnDisconnectedAsync(Exception? exception)
{
    var userId = Context.UserIdentifier;
    if (userId != null)
    {
        if (_cache.TryGetValue(userId, out string cachedConnectionId) && cachedConnectionId == Context.ConnectionId)
        {
            _cache.Remove(userId);
            Debug.WriteLine($"User {userId} disconnected, removed cache entry.");
        }
        Debug.WriteLine($"User {userId} disconnected.");
    }

    await base.OnDisconnectedAsync(exception);
}

同时在OnConnectedAsync中,先更新缓存再发送断开指令,缩小并发冲突窗口,必要时加锁:

public override async Task OnConnectedAsync()
{
    var userId = Context.UserIdentifier;
    if (userId == null)
    {
        Context.Abort();
        return;
    }

    var connectionId = Context.ConnectionId;
    string? existingConnectionId = null;

    // 锁操作确保缓存读写原子性,避免并发冲突
    lock (_cache)
    {
        _cache.TryGetValue(userId, out existingConnectionId);
        _cache.Set(userId, connectionId, TimeSpan.FromHours(1));
    }

    if (existingConnectionId != null && existingConnectionId != connectionId)
    {
        Debug.WriteLine($"User {userId} connected BEFORE connection ID {existingConnectionId} connecting NOW {connectionId}");
        await DisconnectOldConnection(existingConnectionId);
    }

    Debug.WriteLine($"User {userId} connected with connection ID {connectionId}.");
    await base.OnConnectedAsync();
}

注:分布式部署场景下,需用Redis等分布式锁替代内存锁。

方案2:改用数据库存储连接信息(多实例场景)

若应用为多实例部署,MemoryCache无法跨实例共享,可使用数据库(如SQL Server、Redis)存储用户最新连接ID:

  1. 创建UserConnections表,包含UserId、ConnectionId、LastActiveTime字段。
  2. OnConnectedAsync时,更新该用户的ConnectionId为当前ID,刷新LastActiveTime。
  3. OnDisconnectedAsync时,仅当数据库中存储的ConnectionId与当前断开ID一致时,才删除或标记无效。
  4. 发送断开指令时,从数据库读取旧ConnectionId并发送。

方案3:利用Azure SignalR内置用户连接管理

Azure SignalR支持用户连接分组,可直接获取同一用户的所有连接并处理:

public override async Task OnConnectedAsync()
{
    var userId = Context.UserIdentifier;
    if (userId == null)
    {
        Context.Abort();
        return;
    }

    // 获取该用户所有活跃连接
    var userConnections = await Clients.User(userId).GetConnectionsAsync();
    foreach (var connId in userConnections)
    {
        if (connId != Context.ConnectionId)
        {
            await Clients.Client(connId).SendAsync("ForceDisconnect");
        }
    }

    await base.OnConnectedAsync();
}

注:需确保IUserIdProvider配置正确,让Azure SignalR能准确识别用户。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 02:35:57