【问题标题】:SignalR OnDisconnected - a reliable way to handle "User is Online" for chatroom?SignalR OnDisconnected - 为聊天室处理“用户在线”的可靠方法?
【发布时间】:2014-01-11 19:10:27
【问题描述】:

我正在实现一个聊天室。到目前为止,一切都很好——用户可以通过 JS 客户端从他们的浏览器发送消息,我可以使用 C# 客户端做同样的事情——这些消息被广播给其他用户。现在,我正在尝试实现“在线用户”。

我的方法如下:

  • OnConnected - 将数据库中的用户更新为 IsOnline = true
  • OnDisconnected - 如果用户没有任何其他连接,则将数据库中的用户更新为 IsOnline = false
  • 我将状态存储在数据库中,因为无论如何我都必须在数据库中查询用户缩略图 - 这似乎是在集线器中使用字典的简单替代方法。

我遇到的问题是 OnDisconnected 并不总是为每个客户端 ID 调用 - 陈旧的连接阻止“如果用户没有任何其他连接”位解析为 true,所以用户始终“在线”。

我能想到的一个 hacky 解决方案是总是OnDisconnect 时将用户设置为在数据库中离线 - 但这意味着如果用户打开两个选项卡并关闭一个选项卡,他们将“离线”。然后,我可以为每条发送的消息将用户重新设置为在线,但这似乎完全浪费了处理周期,并且当用户真正在线时,仍然有大量时间被视为离线。

我相信,如果有办法保证每个客户端都调用 OnDisconnected,这个问题就会消失。 似乎如果我让客户端打开很长时间(> 10 分钟)然后断开连接,OnDisconnected 永远不会被调用。我会尽力确定重现步骤并保持更新。

那么 - 这是处理在线状态的有效方法吗?如果是这样,还可以做些什么来确保OnDisconnected 最终为每个连接触发?

这个问题让我担心,因为如果我没记错的话,现有的连接会随着时间的推移继续增长,最终由于未处理的状态连接而溢出。

代码:

我正在使用In-memory 方法进行分组。

发送消息(C#):

private readonly static ConnectionMapping<string> _chatConnections =
            new ConnectionMapping<string>();
public void SendChatMessage(string key, ChatMessageViewModel message) {
            message.HtmlContent = _compiler.Transform(message.HtmlContent);
            foreach (var connectionId in _chatConnections.GetConnections(key)) {
                Clients.Client(connectionId).addChatMessage(JsonConvert.SerializeObject(message).SanitizeData());
            }
        }

状态管理:

    public override Task OnConnected() {
        HandleConnection();
        return base.OnConnected();
    }

    public override Task OnDisconnected() {
        HandleConnection(true);
        return base.OnDisconnected();
    }

    public override Task OnReconnected() {
        HandleConnection();
        return base.OnReconnected();
    }

    private void HandleConnection(bool shouldDisconnect = false) {
        if (Context.User == null) return;
        var username = Context.User.Identity.Name;
        var _userService = new UserService();
        var key = username;

        if (shouldDisconnect) {
                _chatConnections.Remove(key, Context.ConnectionId);
                var existingConnections = _chatConnections.GetConnections(key);
                // this is the problem - existingConnections occasionally gets to a point where there's always a connection - as if the OnDisconnected() never got called for that client
                if (!existingConnections.Any()) { // THIS is the issue - existingConnections sometimes contains connections despite there being no open tabs/clients
                    // save status serverside
                    var onlineUserDto = _userService.SetChatStatus(username, false);
                    SendOnlineUserUpdate(_baseUrl, onlineUserDto, false);
                }
        } else {
                if (!_chatConnections.GetConnections(key).Contains(Context.ConnectionId)) {
                    _chatConnections.Add(key, Context.ConnectionId);
                }
                var onlineUserDto = _userService.SetChatStatus(Context.User.Identity.Name, true);
                SendOnlineUserUpdate(_baseUrl, onlineUserDto, true);
                // broadcast to clients
        }
    }

连接映射:

public class ConnectionMapping<T> {
        private readonly Dictionary<T, HashSet<string>> _connections =
            new Dictionary<T, HashSet<string>>();

        public int Count {
            get {
                return _connections.Count;
            }
        }

        public void Add(T key, string connectionId) {
            lock (_connections) {
                HashSet<string> connections;
                if (!_connections.TryGetValue(key, out connections)) {
                    connections = new HashSet<string>();
                    _connections.Add(key, connections);
                }

                lock (connections) {
                    connections.Add(connectionId);
                }
            }
        }

        public IEnumerable<string> GetConnections(T key) {
            HashSet<string> connections;
            if (_connections.TryGetValue(key, out connections)) {
                return connections.ToList();
            }
            return Enumerable.Empty<string>();
        }

        public void Remove(T key, string connectionId) {
            lock (_connections) {
                HashSet<string> connections;
                if (!_connections.TryGetValue(key, out connections)) {
                    return;
                }

                lock (connections) {
                    connections.Remove(connectionId);

                    if (connections.Count == 0) {
                        _connections.Remove(key);
                    }
                }
            }
        }
    }

更新

根据 dfowler 的建议,另一种方法是实现数据库内映射而不是内存内映射,这样可以使用更多元数据来识别僵尸连接。不过,我希望找到解决内存问题的方法,而不是从已经实现的recommended approach 中重新架构。

【问题讨论】:

  • 您是否尝试过 DisconnectTimeout、KeepAlive 和 Heatbeat 间隔的不同配置? github.com/SignalR/SignalR/wiki/Configuring-SignalR
  • @thepirat000 - 是的,我有......这也发生在几小时/几天的规模上 - 连接在内存中无限期地保持活跃。
  • 不要使用数据库进行在线/离线检测。这应该是服务器的一部分,只需执行connections.Countconnections.Length 即可获得计数。因为如果你的服务器崩溃了,当你刚启动它时,你会有很多在线用户..
  • 尝试为传输 github.com/SignalR/SignalR/blob/master/samples/… 打开跟踪。它将显示特定连接 ID 的详细信息以及在您的案例中引发或未引发的生命周期事件。
  • 当客户离开时,您不能指望从客户那里得到好消息;网络可能会消失(实际上一直都在消失)。使用您的心跳代码来处理断开连接,除非您幸运 足以获得明确的会话结束通知。不要指望幸运。

标签: c# signalr signalr-hub signalr.client


【解决方案1】:

【讨论】:

  • 我祈祷着希望你能看到这个。我现在正在寻找 - 究竟是哪些部分?我正在使用内存中的方法来映射用户 连接。这似乎使用了不同的方法。
  • 如果一个完整的实现细节会有所帮助 - 这就是一切:codereview.stackexchange.com/questions/39073/…
  • 内存中的方法有一些明显的弱点: - 如果服务器重新启动,您将丢失所有状态 - 如果您有多个服务器,它将不准确(因为每个服务器对谁是谁有不同的看法)在线”)如果不担心上述 2 个问题,那么只要您了解 OnReconnected 和 OnDisconnected 何时发生就应该没问题。 OnReconnected 可以触发例如在应用程序池/应用程序域回收后,您将丢失所有状态,一些客户端将重新连接,其他客户端将断开连接,而其他客户端将连接。
  • 我使用 azure 缓存...我认为您仍然会遇到问题。我通过从客户端 ping 并设置不活动阈值来解决这个问题。可靠的方式虽然不是实时的。
  • 想象一下服务器崩溃了。然后会发生什么?
猜你喜欢
  • 2013-11-16
  • 2012-06-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-28
  • 1970-01-01
相关资源
最近更新 更多