Gestion efficace de la connexion SignalR

Bonjour, Habrahabr. Je travaille actuellement sur un moteur de chat basé sur la bibliothèque SignalR . En plus du fascinant processus d'immersion dans le monde des applications en temps réel, j'ai également dû faire face à un certain nombre de défis techniques. À propos de l'un d'eux, je veux partager avec vous dans cet article.

Présentation


Qu'est-ce que SignalR - c'est une sorte de façade sur les WebSockets , l'interrogation longue et les technologies d' événements envoyés par le serveur . Grâce à cette façade, vous pouvez travailler uniformément avec n'importe laquelle de ces technologies et ne pas vous soucier des détails. De plus, grâce à la technologie d'interrogation longue, vous pouvez prendre en charge des clients qui, pour une raison quelconque, ne peuvent pas travailler sur des sockets Web, tels que IE-8. La façade est représentée par une API RPC de haut niveau. De plus, SignalR propose de construire des communications selon le principe de «l'éditeur-abonné», qui dans la terminologie API est appelé groupes. Ceci sera discuté plus loin.

Défis


La chose la plus intéressante en programmation est peut-être la capacité de résoudre des problèmes non standard. Et aujourd'hui, nous allons désigner l'une de ces tâches et envisager sa solution.

À l'ère du développement des idées d'évolutivité et, tout d'abord, horizontale, le principal défi est la nécessité d'avoir plus d'un serveur. Et les développeurs de la bibliothèque indiquée ont déjà répondu à cet appel, une description de la solution peut être trouvée sur MSDN . En résumé, il est proposé, en utilisant le principe éditeur-abonné, de synchroniser les appels entre serveurs. Chaque serveur est abonné à un bus partagé et toutes les commandes envoyées par ce serveur sont d'abord envoyées au bus. De plus, la commande s'applique à tous les serveurs et seulement aux clients:

image

Il est important de noter que chaque client connecté au serveur a son propre identifiant de connexion unique - ConnectionId - et tous les messages sont finalement adressés à l'aide de cet identifiant. Par conséquent, chaque serveur stocke ces connexions.

Cependant, pour des raisons inconnues, l'API de la bibliothèque SignalR ne donne pas accès à ces données. Et ici, nous sommes confrontés à une question très aiguë d'accès à ces connexions. Tel est notre défi.

Pourquoi devons-nous nous connecter


Comme indiqué précédemment, SignalR propose un modèle éditeur-abonné. Ici, l'unité de routage des messages n'est pas un ConnectionId mais un groupe. Un groupe est un ensemble de connexions. En envoyant un message à un groupe, nous envoyons un message à tous les ConnectionId qui se trouvent dans ce groupe. Il est pratique de créer des groupes - lorsque vous connectez un client au serveur, nous appelons simplement la méthode API AddToGroupAsync :

public override async Task OnConnectedAsync() { foreach (var chat in _options.Chats) await Groups.AddToGroupAsync(ConnectionId, chat); await Groups.AddToGroupAsync(ConnectionId, Client); } 

Et comment quitter le groupe? Les développeurs proposent la méthode API RemoveFromGroupAsync :

 public override async Task OnDisconnectedAsync(Exception exception) { foreach (var chat in _options.Chats) await Groups.RemoveFromGroupAsync(ConnectionId, chat); await Groups.RemoveFromGroupAsync(ConnectionId, Client); } 

Notez que l'unité de données est ConnectionId. Cependant, du point de vue du modèle de domaine, ConnectionId n'existe pas, mais des clients existent. À cet égard, l'organisation du mappage client vers le tableau ConnectionId et vice versa est attribuée aux utilisateurs de la bibliothèque spécifiée.

C'est le tableau de tous les clients ConnectionId qui est nécessaire lorsqu'il quitte le groupe. Cependant, un tel tableau n'existe pas. Vous devez l'organiser vous-même. La tâche devient beaucoup plus intéressante dans le cas d'un système à l'échelle horizontale. Dans ce cas, une partie des connexions peut se faire sur un serveur, le reste sur d'autres serveurs.

Façons de mapper les clients aux connexions


Une section entière sur MSDN est consacrée à ce problème. Les méthodes suivantes sont proposées pour examen:

  • Stockage en mémoire
  • "Groupe d'utilisateurs"
  • Stockage externe permanent

Comment suivre les connexions?
Vous pouvez suivre les connexions à l'aide des méthodes de concentrateur OnConnectedAsync et OnDisconnectedAsync .

Immédiatement, je note que les options qui ne prennent pas en charge la mise à l'échelle ne sont pas prises en compte. Il s'agit notamment de la possibilité de stocker les connexions dans la mémoire du serveur. Il n'y a aucun accès aux connexions client sur les autres serveurs, le cas échéant. L'option de stockage dans un stockage persistant externe est associée à ses inconvénients, qui incluent le problème du nettoyage des connexions inactives. De telles connexions se produisent en cas de redémarrage dur du serveur. Détecter et nettoyer ces connexions n'est pas une tâche triviale.

Parmi les options ci-dessus, l'option «groupe d'utilisateurs» est intéressante. La simplicité s'applique certainement à ses avantages: aucune bibliothèque, aucun référentiel n'est requis. Tout aussi importante est la conséquence de la simplicité de cette méthode - la fiabilité.

Mais qu'en est-il de Redis?
Soit dit en passant, l'utilisation de Redis pour stocker des connexions est également une mauvaise option. Il y a un problème aigu d'organisation des données en mémoire. D'une part, la clé est le client, d'autre part, le groupe.

"Groupe d'utilisateurs"


Qu'est-ce qu'un «groupe d'utilisateurs»? Il s'agit d'un groupe dans la terminologie SignalR où un seul client peut être client - lui-même. Cela garantit 2 choses:

  1. Les messages seront livrés à une seule personne
  2. Les messages seront livrés à tous les appareils humains

Comment cela nous aidera-t-il? Permettez-moi de vous rappeler que notre défi est de résoudre le problème de la sortie du client du groupe. Nous en avions besoin, en quittant le groupe d'un appareil, les autres se désabonneraient également, mais nous n'avions pas de liste de connexion pour ce client, à l'exception de celui à partir duquel nous avions initié la sortie.

Le "groupe d'utilisateurs" est la première étape vers la résolution de ce problème. La deuxième étape consiste à construire un «miroir» sur le client. Oui, oui, des miroirs.

Le miroir


La source des commandes envoyées du client au serveur sont les actions de l'utilisateur. Postez un message - envoyez une commande au serveur:

 this.state.hubConnection .invoke('post', {message, group, nick}) .catch(err => console.error(err)); 

Et nous informons tous les clients du groupe de la nouvelle publication:

 public async Task PostMessage(PostMessage message) { await Clients.Group(message.Group).SendAsync("message", new { Message = message.Message, Group = message.Group, Nick = ClientNick }); } 

Cependant, un certain nombre de commandes doivent être exécutées de manière synchrone sur tous les appareils. Comment y parvenir? Soit disposer d'un tableau de connexions et exécuter une commande pour chaque connexion sur un client spécifique, soit utiliser la méthode décrite ci-dessous. Considérez cette méthode en quittant le chat.

L'équipe arrivant du client ira d'abord dans le «groupe d'utilisateurs» pour une méthode spéciale, qui la redirigera simplement vers le serveur, c'est-à-dire " Miroirs ." Ainsi, non le serveur désabonnera les appareils, mais les appareils eux-mêmes seront invités à se désinscrire.

Voici un exemple de commande de désabonnement de chat sur le serveur:

 public async Task LeaveChat(LeaveChatMessage message) { await Clients.OthersInGroup(message.Group).SendAsync("lost", new ClientCommand { Group = message.Group, Nick = Client }); await Clients.Group(Client).SendAsync("mirror", new MirrorChatCommand { Method = "unsubscribe", Payload = new UnsubscribeChatMessage { Group = message.Group } }); } 

 public async Task Unsubscribe(UnsubscribeChatMessage message) { await Groups.RemoveFromGroupAsync(ConnectionId, message.Group); } 

Et voici le code client:

 connection.on('mirror', (message) => { connection .invoke(message.method, message.payload) .catch(err => console.error(err)); }); 

Examinons plus en détail ce qui se passe ici:

  1. Le client lance la désinscription - envoie la commande "quitter" au serveur
  2. Le serveur envoie la commande «unsubscribe» au «groupe d'utilisateurs» sur le «miroir»
  3. Le message est transmis à tous les appareils clients.
  4. Un message sur le client est renvoyé au serveur en utilisant la méthode spécifiée par le serveur
  5. Sur chaque serveur, le client est désabonné du groupe

Par conséquent, tous les appareils eux-mêmes se désinscrireont des serveurs auxquels ils sont connectés. Chacun se désabonnera du sien et nous n'avons rien à stocker. Aucun problème ne se posera également en cas de redémarrage dur du serveur.

Alors, pourquoi devons-nous nous connecter?


Le fait d'avoir un «groupe d'utilisateurs» et un «miroir» sur le client élimine le besoin de travailler avec des connexions. Qu'en pensez-vous, chers lecteurs, à ce sujet? Partagez votre opinion dans les commentaires.

Code source pour des exemples:

github.com/aesamson/signalr-server
github.com/aesamson/signalr-client

Source: https://habr.com/ru/post/fr470299/


All Articles