Подтвердить что ты не робот

SignalR OnDisconnected - надежный способ обработки "Пользователь в сети" для чата?

Я реализую чат. Пока что так хорошо - пользователи могут отправлять сообщения из своих браузеров через JS-клиент, и я могу использовать клиент С# для выполнения того же: эти сообщения передаются другим пользователям. Теперь я пытаюсь внедрить "онлайн-пользователей".

Мой подход следующий:

  • OnConnected - обновить пользователя в db как IsOnline = true
  • OnDisconnected - если у пользователя нет других подключений, обновите пользователя в db, чтобы он был IsOnline = false
  • Я сохраняю состояние в БД, потому что мне нужно запросить db для миниатюр пользователя в любом случае - это казалось простой альтернативой работе со словарями в концентраторе.

Проблема, с которой я сталкиваюсь, заключается в том, что OnDisconnected не всегда вызывается для каждого идентификатора клиента - устаревшие соединения препятствуют тому, чтобы бит "если пользователь не имеет каких-либо других подключений" от разрешения до true, поэтому пользователь всегда "в сети".

Одно хакерское решение, о котором я могу думать, заключается в том, чтобы всегда отключать пользователя в db на OnDisconnect, но это означает, что если пользователь открывает две вкладки и закрывает одну, они будут "автономными". Затем я мог повторно настроить пользователя на онлайн для каждого отправляемого сообщения, но это похоже на полную потерю циклов обработки и все еще оставляет кучу времени, когда пользователь рассматривается как офлайн, когда он действительно онлайн.

Я считаю, что если бы был способ гарантировать, что OnDisconnected будет вызван для каждого клиента, эта проблема исчезнет. Похоже, если я оставлю клиентов открытыми в течение длительного времени ( > 10 минут), а затем отключится, OnDisconnected никогда не будет вызван. Я постараюсь изо всех сил определить шаги воспроизведения и сохранить это обновление.

Итак - Это действительный подход к обработке онлайн-статуса? Если да, что еще можно сделать, чтобы обеспечить OnDisconnected для каждого соединения, в конечном итоге?

Эта проблема беспокоит меня, потому что существующие соединения будут продолжать расти со временем, если я не ошибаюсь, в конце концов переполняю из-за необработанных соединений состояния.

Код:

Я использую метод In-memory для группировок.

Отправка сообщений (С#):

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

Обновление

В предложении dfowler альтернативным подходом было бы внедрение in-db-сопоставления вместо встроенной памяти, таким образом, для идентификации зомбированных соединений можно использовать больше метаданных. Я надеюсь на решение проблемы с памятью, но вместо того, чтобы повторно архитектировать от рекомендуемого подхода, который уже реализован.

4b9b3361