一、为什么SignalR服务端会内存泄漏?

SignalR是一个强大的实时通信框架,但处理大量长连接时,稍不注意就会让服务器内存"撑爆"。想象一下:每个连接就像一条水管,如果不用了就关掉阀门,但忘记关的水管越多,水池迟早会溢出来。

常见的内存泄漏场景包括:

  1. 客户端异常断开时,服务端没有及时清理连接对象
  2. 消息队列积压导致消息对象无法释放
  3. 全局集合中保存了不再使用的连接引用

举个典型例子(技术栈:.NET Core + SignalR):

// 错误示例:全局字典保存所有连接
public static Dictionary<string, HubCallerContext> ActiveConnections = new();

public class ChatHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        // 连接建立时存入字典
        ActiveConnections[Context.ConnectionId] = Context;
        await base.OnConnectedAsync();
    }
    
    // 缺少OnDisconnectedAsync实现!
}

这段代码的问题很明显:连接建立时被存入全局字典,但断开时没有移除。随着时间推移,字典会越来越大,即使客户端早已断开。

二、连接生命周期管理技巧

正确处理连接生命周期是解决内存问题的关键。SignalR提供了三个核心事件:

  1. OnConnectedAsync - 连接建立时触发
  2. OnDisconnectedAsync - 连接断开时触发
  3. 超时机制 - 默认30秒无心跳自动断开

改进后的正确写法:

public class ChatHub : Hub
{
    private static readonly ConcurrentDictionary<string, HubCallerContext> _connections = new();

    public override async Task OnConnectedAsync()
    {
        // 使用线程安全的ConcurrentDictionary
        _connections.TryAdd(Context.ConnectionId, Context);
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception exception)
    {
        // 确保连接断开时移除
        _connections.TryRemove(Context.ConnectionId, out _);
        await base.OnDisconnectedAsync(exception);
    }
}

这里做了三处改进:

  1. 改用线程安全的ConcurrentDictionary
  2. 实现OnDisconnectedAsync清理逻辑
  3. 使用TryAdd/TryRemove避免异常

三、资源释放的进阶技巧

除了基本清理,还有更精细的内存控制方法:

3.1 消息缓冲区控制

services.AddSignalR(options => {
    // 限制单个连接的最大缓冲区大小(单位:KB)
    options.MaximumReceiveMessageSize = 32; 
    
    // 客户端超时设置(单位:秒)
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(60);
    
    // 心跳间隔(单位:秒)
    options.KeepAliveInterval = TimeSpan.FromSeconds(15);
});

3.2 使用WeakReference弱引用

对于需要跟踪但不应该阻止GC回收的对象:

private static List<WeakReference<HubCallerContext>> _weakConnections = new();

void TrackConnection(HubCallerContext context)
{
    _weakConnections.RemoveAll(wr => !wr.TryGetTarget(out _));
    _weakConnections.Add(new WeakReference<HubCallerContext>(context));
}

3.3 定期清理机制

// 定时清理无效连接
_ = Task.Run(async () =>
{
    while (true)
    {
        await Task.Delay(TimeSpan.FromMinutes(5));
        
        var deadConnections = _connections
            .Where(c => !c.Value.ConnectionAborted.IsCancellationRequested)
            .ToList();
            
        foreach (var conn in deadConnections)
        {
            _connections.TryRemove(conn.Key, out _);
        }
    }
});

四、实战中的注意事项

  1. 连接状态检测
// 检测连接是否活跃
public async Task<bool> IsConnectionActive(string connectionId)
{
    if (_connections.TryGetValue(connectionId, out var context))
    {
        return !context.ConnectionAborted.IsCancellationRequested;
    }
    return false;
}
  1. 异常处理
public override async Task OnDisconnectedAsync(Exception exception)
{
    try
    {
        // 清理逻辑...
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "清理连接时出错");
    }
    finally
    {
        await base.OnDisconnectedAsync(exception);
    }
}
  1. 性能监控: 建议在项目中添加内存监控:
// 在Startup.cs中
app.Use(async (context, next) =>
{
    var process = Process.GetCurrentProcess();
    _logger.LogInformation($"当前内存使用:{process.WorkingSet64 / 1024 / 1024}MB");
    await next();
});

五、不同场景下的优化策略

5.1 高并发短连接场景

services.AddSignalR()
    .AddMessagePackProtocol() // 使用更小的消息格式
    .AddHubOptions<ChatHub>(options => {
        options.EnableDetailedErrors = false; // 生产环境关闭详细错误
        options.HandshakeTimeout = TimeSpan.FromSeconds(5); // 缩短握手超时
    });

5.2 长连接+大数据量场景

// 在Hub方法中分块传输大数据
public async Task UploadLargeData(byte[] chunk, int totalChunks)
{
    // 使用临时文件而不是内存保存
    var tempPath = Path.GetTempFileName();
    await File.AppendAllBytesAsync(tempPath, chunk);
    
    if (chunk.Length == totalChunks)
    {
        // 处理完整数据
        ProcessCompleteData(tempPath);
    }
}

六、总结与最佳实践

经过以上分析,我们得出以下优化经验:

  1. 必做事项
  • 始终实现OnDisconnectedAsync
  • 使用线程安全的集合类型
  • 设置合理的超时和缓冲区大小
  1. 推荐事项
  • 添加定期清理机制
  • 实现内存监控
  • 根据场景选择合适的配置
  1. 高级技巧
  • 大数据使用流式处理
  • 考虑使用WeakReference
  • 采用更高效的协议如MessagePack

记住,内存优化不是一劳永逸的,需要根据实际运行情况持续调整。建议在开发阶段就建立内存监控机制,这样才能在问题变得严重之前及时发现。

最后分享一个完整的Hub实现示例:

public class OptimizedChatHub : Hub
{
    private static readonly ConcurrentDictionary<string, (HubCallerContext ctx, DateTime lastActive)> _connections = new();
    private readonly ILogger<OptimizedChatHub> _logger;

    public OptimizedChatHub(ILogger<OptimizedChatHub> logger)
    {
        _logger = logger;
        StartCleanupTask();
    }

    private void StartCleanupTask()
    {
        _ = Task.Run(async () =>
        {
            while (true)
            {
                await Task.Delay(TimeSpan.FromMinutes(1));
                CleanInactiveConnections();
            }
        });
    }

    private void CleanInactiveConnections()
    {
        var cutoff = DateTime.UtcNow.AddMinutes(-2);
        var deadConnections = _connections
            .Where(c => c.Value.lastActive < cutoff || 
                   c.Value.ctx.ConnectionAborted.IsCancellationRequested)
            .ToList();

        foreach (var conn in deadConnections)
        {
            _connections.TryRemove(conn.Key, out _);
            _logger.LogInformation($"清理闲置连接:{conn.Key}");
        }
    }

    public override async Task OnConnectedAsync()
    {
        _connections[Context.ConnectionId] = (Context, DateTime.UtcNow);
        _logger.LogInformation($"新连接:{Context.ConnectionId},当前连接数:{_connections.Count}");
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception exception)
    {
        _connections.TryRemove(Context.ConnectionId, out _);
        _logger.LogInformation(exception == null 
            ? $"连接正常关闭:{Context.ConnectionId}"
            : $"连接异常断开:{Context.ConnectionId},错误:{exception.Message}");
        await base.OnDisconnectedAsync(exception);
    }

    public async Task SendMessage(string user, string message)
    {
        // 更新活动时间
        if (_connections.TryGetValue(Context.ConnectionId, out var entry))
        {
            _connections[Context.ConnectionId] = (entry.ctx, DateTime.UtcNow);
        }

        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

这个实现包含了我们讨论的所有优化点:

  1. 使用线程安全集合
  2. 连接状态跟踪
  3. 定期清理
  4. 活动时间记录
  5. 完善的日志记录
  6. 异常处理

希望这些实践经验能帮助你构建更稳定的SignalR服务!