Effizientes SignalR-Verbindungsmanagement

Hallo Habrahabr. Ich arbeite derzeit an einer Chat-Engine, die auf der SignalR- Bibliothek basiert. Neben dem faszinierenden Prozess des Eintauchens in die Welt der Echtzeitanwendungen musste ich mich auch einer Reihe technischer Herausforderungen stellen. Über einen von ihnen möchte ich in diesem Artikel mit Ihnen teilen.

Einführung


Was ist SignalR? Es ist eine Art Fassade über WebSockets , Long Polling und Server-Send-Ereignistechnologien . Dank dieser Fassade können Sie mit jeder dieser Technologien einheitlich arbeiten und müssen sich nicht um Details kümmern. Dank der Long Polling-Technologie können Sie außerdem Kunden unterstützen, die aus irgendeinem Grund nicht an Web-Sockets wie IE-8 arbeiten können. Die Fassade wird durch eine RPC- basierte API auf hoher Ebene dargestellt. Darüber hinaus bietet SignalR an, die Kommunikation nach dem Prinzip „Publisher-Subscriber“ aufzubauen, das in der API-Terminologie als Gruppen bezeichnet wird. Dies wird weiter diskutiert.

Herausforderungen


Das vielleicht interessanteste an der Programmierung ist die Fähigkeit, nicht standardmäßige Probleme zu lösen. Und heute werden wir eine dieser Aufgaben benennen und ihre Lösung prüfen.

Im Zeitalter der Entwicklung von Skalierungsideen und insbesondere der Horizontalen besteht die größte Herausforderung darin, mehr als einen Server zu haben. Und die Entwickler der angegebenen Bibliothek haben diesen Aufruf bereits bewältigt, eine Beschreibung der Lösung finden Sie auf MSDN . Kurz gesagt, es wird vorgeschlagen, nach dem Publisher-Subscriber-Prinzip Anrufe zwischen Servern zu synchronisieren. Jeder Server abonniert einen gemeinsam genutzten Bus und alle von diesem Server gesendeten Befehle werden zuerst an den Bus gesendet. Außerdem gilt der Befehl für alle Server und nur dann für Clients:

Bild

Es ist wichtig zu beachten, dass jeder mit dem Server verbundene Client seine eigene eindeutige Verbindungskennung - ConnectionId - hat und alle Nachrichten letztendlich mit dieser Kennung adressiert werden. Daher speichert jeder Server diese Verbindungen.

Aus unbekannten Gründen bietet die SignalR-Bibliotheks-API jedoch keinen Zugriff auf diese Daten. Und hier stehen wir vor einer sehr akuten Frage des Zugangs zu diesen Verbindungen. Das ist unsere Herausforderung.

Warum müssen wir uns verbinden?


Wie bereits erwähnt, bietet SignalR ein Publisher-Subscriber-Modell an. Hier ist die Einheit des Nachrichtenroutings keine Verbindungs-ID, sondern eine Gruppe. Eine Gruppe ist eine Sammlung von Verbindungen. Durch Senden einer Nachricht an eine Gruppe senden wir eine Nachricht an alle ConnectionId, die sich in dieser Gruppe befinden. Es ist praktisch, Gruppen zu erstellen. Wenn Sie einen Client mit dem Server verbinden, rufen Sie einfach die AddToGroupAsync- API-Methode auf:

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

Und wie verlässt man die Gruppe? Entwickler bieten API-Methode RemoveFromGroupAsync an :

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

Beachten Sie, dass die Dateneinheit ConnectionId ist. Aus Sicht des Domänenmodells ist ConnectionId jedoch nicht vorhanden, es gibt jedoch Clients. In dieser Hinsicht wird die Organisation der Clientzuordnung zum ConnectionId-Array und umgekehrt Benutzern der angegebenen Bibliothek zugewiesen.

Es ist das Array aller ConnectionId-Clients, die beim Verlassen der Gruppe benötigt werden. Ein solches Array existiert jedoch nicht. Sie müssen es selbst organisieren. Die Aufgabe wird bei einem horizontal skalierten System viel interessanter. In diesem Fall kann sich ein Teil der Verbindungen auf einem Server befinden, der Rest auf anderen Servern.

Möglichkeiten, Clients Verbindungen zuzuordnen


Ein ganzer Abschnitt über MSDN ist diesem Thema gewidmet. Die folgenden Methoden werden zur Prüfung vorgeschlagen:

  • In-Memory-Speicher
  • "Benutzergruppe"
  • Permanenter externer Speicher

Wie verfolge ich Verbindungen?
Sie können Verbindungen mit den Hub-Methoden OnConnectedAsync und OnDisconnectedAsync verfolgen .

Ich stelle sofort fest, dass Optionen, die keine Skalierung unterstützen, nicht berücksichtigt werden. Dazu gehört die Option, Verbindungen im Serverspeicher zu speichern. Es gibt keinen Zugriff auf Clientverbindungen auf anderen Servern, falls vorhanden. Die Option zum Speichern in einem externen dauerhaften Speicher ist mit seinen Nachteilen verbunden, zu denen das Problem der Reinigung inaktiver Verbindungen gehört. Solche Verbindungen treten bei einem harten Neustart des Servers auf. Das Erkennen und Reinigen dieser Verbindungen ist keine triviale Aufgabe.

Unter den oben genannten Optionen ist die Option "Benutzergruppe" interessant. Einfachheit gilt sicherlich für seine Vorteile - es sind keine Bibliotheken, Repositorys erforderlich. Ebenso wichtig ist die Konsequenz der Einfachheit dieser Methode - Zuverlässigkeit.

Aber was ist mit Redis?
Die Verwendung von Redis zum Speichern von Verbindungen ist übrigens auch eine schlechte Option. Es gibt ein akutes Problem beim Organisieren von Daten im Speicher. Der Schlüssel ist einerseits der Client, andererseits die Gruppe.

"Benutzergruppe"


Was ist eine Benutzergruppe? Dies ist eine Gruppe in der SignalR-Terminologie, in der nur ein Client Kunde sein kann - er selbst. Dies garantiert 2 Dinge:

  1. Nachrichten werden nur an eine Person zugestellt
  2. Nachrichten werden an alle menschlichen Geräte übermittelt

Wie wird uns das helfen? Ich möchte Sie daran erinnern, dass unsere Herausforderung darin besteht, das Problem zu lösen, den Kunden aus der Gruppe zu entfernen. Wir brauchten das, um die Gruppe von einem Gerät zu verlassen, der Rest würde sich ebenfalls abmelden, aber wir hatten keine Verbindungsliste für diesen Client, außer der, von der aus wir den Exit initiiert haben.

"Benutzergruppe" ist der erste Schritt zur Lösung dieses Problems. Der zweite Schritt besteht darin, einen „Spiegel“ auf dem Client zu erstellen. Ja, ja, Spiegel.

Der Spiegel


Die Quelle der vom Client an den Server gesendeten Befehle sind Benutzeraktionen. Nachricht senden - Befehl an den Server senden:

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

Und wir benachrichtigen alle Kunden der Gruppe über den neuen Beitrag:

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

Auf allen Geräten muss jedoch eine Reihe von Befehlen synchron ausgeführt werden. Wie erreicht man das? Verfügen Sie entweder über ein Array von Verbindungen und führen Sie einen Befehl für jede Verbindung auf einem bestimmten Client aus, oder verwenden Sie die unten beschriebene Methode. Betrachten Sie diese Methode, indem Sie den Chat beenden.

Das vom Client ankommende Team geht zuerst zur "Benutzergruppe", um eine spezielle Methode zu erhalten, die es einfach zurück zum Server umleitet, d. H. " Spiegel ." Daher wird nicht der Server die Geräte abbestellen, sondern die Geräte selbst werden aufgefordert, sich abzumelden.

Hier ist ein Beispiel für einen Befehl zum Abbestellen des Server-Chats:

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

Und hier ist der Client-Code:

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

Lassen Sie uns genauer untersuchen, was hier passiert:

  1. Der Client initiiert das Abbestellen - sendet den Befehl "Leave" an den Server
  2. Der Server sendet den Befehl "Abbestellen" an die "Benutzergruppe" auf dem "Spiegel".
  3. Die Nachricht wird an alle Clientgeräte gesendet.
  4. Eine Nachricht auf dem Client wird mit der vom Server angegebenen Methode an den Server zurückgesendet
  5. Auf jedem Server wird der Client von der Gruppe abgemeldet

Infolgedessen werden alle Geräte selbst von den Servern abgemeldet, mit denen sie verbunden sind. Jeder wird sich von seinem eigenen abmelden und wir müssen nichts speichern. Auch bei einem harten Neustart des Servers treten keine Probleme auf.

Warum müssen wir uns also verbinden?


Wenn auf dem Client eine „Benutzergruppe“ und ein „Spiegel“ vorhanden sind, müssen Sie nicht mehr mit Verbindungen arbeiten. Was denkst du, liebe Leser, darüber? Teilen Sie Ihre Meinung in den Kommentaren.

Quellcode für Beispiele:

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

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


All Articles