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

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

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

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

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

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

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

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

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

код:

я использую в памяти подход к группировки.

Отправка Сообщений (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
        }
    }

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 альтернативным подходом было бы реализовать отображение в БД вместо в памяти, таким образом, больше метаданных можно использовать для идентификации зомбированных соединений. Я надеюсь на решение проблемы в памяти, хотя, вместо того, чтобы перепроектировать вдали от рекомендуемый подход это уже реализовано.

1 ответов


попробуйте следовать этому образцу здесь:

https://github.com/DamianEdwards/NDCLondon2013/tree/master/UserPresence