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 = trueOnDisconnected
- 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
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.
Essayez de suivre cet exemple ici:
https://github.com/DamianEdwards/NDCLondon2013/tree/master/UserPresence