web-dev-qa-db-fra.com

SignalR OnDisconnected - un moyen fiable de gérer "l'utilisateur est en ligne" pour le salon de discussion?

J'implémente une salle de chat. Jusqu'à présent, tout va bien - les utilisateurs peuvent envoyer des messages depuis leur navigateur via un client JS, et je peux utiliser un client C # pour faire la même chose - ces messages sont diffusés aux autres utilisateurs. Maintenant, j'essaye d'implémenter des "utilisateurs en ligne".

Mon approche est la suivante:

  • OnConnected - met à jour l'utilisateur dans la base de données pour qu'il soit IsOnline = true
  • OnDisconnected - si l'utilisateur n'a pas d'autres connexions, mettez à jour l'utilisateur dans la base de données pour qu'il soit IsOnline = false
  • Je stocke l'état dans la base de données car je dois de toute façon interroger la base de données pour les vignettes utilisateur - cela semblait être une alternative simple à l'utilisation des dictionnaires dans le concentrateur.

Le problème que je rencontre est que OnDisconnected n'est pas toujours appelé pour chaque ID client - les connexions obsolètes empêchent le bit "si l'utilisateur n'a pas d'autres connexions" de se résoudre à true, donc l'utilisateur est toujours "en ligne".

Une solution hacky à laquelle je peux penser est de toujours mettre l'utilisateur hors ligne dans la base de données sur OnDisconnect - mais cela signifie que si l'utilisateur ouvre deux onglets et en ferme un, ils sera "hors ligne". Je pourrais alors réinitialiser l'utilisateur en ligne pour chaque message envoyé, mais cela semble être un gaspillage total de cycles de traitement et laisse encore un laps de temps où l'utilisateur est considéré comme hors ligne, alors qu'ils sont vraiment en ligne.

Je crois que s'il y avait un moyen de garantir que OnDisconnected soit appelé pour chaque client, ce problème disparaîtrait. Il semble comme si je laisse les clients ouverts pendant longtemps (> 10 minutes) et que je me déconnecte, OnDisconnected n'est jamais appelé. Je ferai de mon mieux pour localiser les étapes de repro et garder cette mise à jour.

Donc - Est-ce une approche valable pour gérer le statut en ligne? Si tel est le cas, que peut-on faire d'autre pour s'assurer que OnDisconnected se déclenche pour chaque connexion, éventuellement?

Ce problème m'inquiète car les connexions existantes continueront de croître au fil du temps, si je ne me trompe pas, débordant éventuellement en raison de connexions d'état non gérées.

Code:

J'utilise l'approche In-memory pour les regroupements.

Envoi de messages (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());
            }
        }

Gestion des états:

    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
        }
    }

ConnectionMapping:

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);
                    }
                }
            }
        }
    }

Mise à jour

Selon la suggestion de dfowler, une approche alternative serait d'implémenter le mappage in-db au lieu de in-memory, de cette façon plus de métadonnées peuvent être utilisées pour identifier les connexions zombifiées. J'espère cependant une solution au problème en mémoire, au lieu de ré-architecturer loin d'une approche recommandée qui est déjà implémentée.

29
SB2055
20
davidfowl