ASP.NET Core SignalR, ou juste "SignalR" pour les intimes, est une bibliothèque pour ASP.NET Core qui va permettre
d'ajouter des fonctionnalités temps-réel à vos applications.
Vous pourrez par exemple créer envoyer des notifications en temps réel à vos utilisateurs, ou créer des jeux multijoueurs.
Le but de cet article n'est pas de parler de la technologie elle-même, mais de la façon de gérer sa mise à l'échelle.
Si vous ne connaissez pas déjà SignalR, je vous invite à aller la découvrir
dans sa documentation officielle.
Mise à l'échelle
Lorsqu'on utilise SignalR sur un seul serveur, tout fonctionne très bien puisque notre serveur garde en mémoire la liste des
clients connectés, et va pouvoir envoyer des messages à chacun d'entre eux.
Mais si on veut gérer beaucoup de clients connectés, on risque d'arriver aux limites de notre serveur
(en mémoire utilisée, ou en nombre de connexions ouvertes simultanées).
Si on peut bien sûr passer sur un serveur plus puissant, on envisagera aussi la possibilité de lancer plusieurs serveurs derrière un load-balancer. C'est bien sûr possible, mais il va nous falloir un peu de configuration pour que ça fonctionne.
Connexion du client au serveur
Si on déploie notre serveur SignalR sur un 2ème serveur avec un load-balancer, on va directement avoir des soucis, puisque les clients auront des erreurs à l'ouverture de la connexion.
Ceci est du au fait que SignalR va pouvoir gérer plusieurs transports :
un premier appel est faite pour négocier le meilleur transport
(WebSocket si le client le supporte, sinon Server-Sent Events ou Long-Polling),
puis un second qui ouvre la vraie connexion.
Entre ces deux appels, les informations restent en mémoire sur le serveur, et donc si le client ne se connecte pas à la même instance, la connexion échoue.
Pour remédier à ce problème, nous avons 3 possibilités :
Affinité de session
Ceci est à configurer au niveau du load-balacer. Chaque client sera lié à l'un de vos serveurs, et ses appels arriveront toujours sur le même.
Forcer l'usage de websocket
Si vous êtes sûr que les clients supportent WebSocket, alors vous pouvez
oublier l'étape de négociation, et forcer la connexion en SignalR.
Ainsi, plus besoin d'affinité de session, vu qu'il n'y a qu'une requête - elle fonctionnera quel que soit le serveur qui la traitera.
En Javascript, on modifiera la connexion de la manière suivante :
let connection = new signalR.HubConnectionBuilder()
.withUrl("/appHub", {
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets
})
.build();
Utiliser Azure SignalR Service
Si vous hébergez votre application dans le Cloud Azure, vous pouvez utiliser le service dédié Azure SignalR Service.
Ce service sert de proxy entre les clients et les serveurs, et va gérer ces problématiques de connexion pour vous.
graph BT subgraph Serveurs web1[Web] web2[Web] azsignalr(Azure SignalR) azsignalr<-->web1 azsignalr<-->web2 end clientA(Client A) clientB(Client B) clientC(Client C) clientA<--ws-->azsignalr clientB<--ws-->azsignalr clientC<--ws-->azsignalr
Pour configurer SignalR avec Redis, il faut référencer le package Nuget Microsoft.Azure.SignalR
,
et appeler la méthode suivante lors de la configuation de SignalR :
services.AddSignalR().AddAzureSignalR(azureSignalrConnectionString);
Communication entre clients sur instances différentes
Maintenant qu'on a résolu notre problème de connexion, il reste un problème à régler.
Les clients sont bien connectés, mais chacun de nos serveurs n'a connaissance que d'une partie des clients,
il n'est pas possible d'envoyer des messages à un client connecté sur un autre serveur.
Si on veut envoyer un message à tous les clients connectés, il va nous falloir trouver une solution. La documentation d'ASP.NET nous en propose deux.
Avec Azure SignalR Service
Si vous avez déjà choisi cette option dans la première partie, rien de plus à faire :
tous les clients sont bien connectés au même service Azure, tout est géré de manière transparente pour vous.
Avec Redis
L'autre solution est d'utiliser un backplane Redis. Cette fois, on n'a pas un proxy qui gère les connexions, mais Redis et ses fonctionnalités PUB/SUB permet aux serveurs de communiquer entre eux.
graph BT subgraph Serveurs web1[Web] web2[Web] redis[Redis] web1<-->redis web2<-->redis end clientA(Client A) clientB(Client B) clientC(Client C) clientA<--ws-->web1 clientB<--ws-->web2 clientC<--ws-->web2
Si l'un de vos serveurs veut envoyer un message, ça se passe en 3 temps :
- Il envoie le message au serveur Redis, avec le message et les clients visés
- Tous les serveurs (dont l'émetteur du message) reçoivent une notification du serveur Redis
- Chacun des serveurs envoie le message aux clients qu'il connait
Pour configurer SignalR avec Redis, il faut référencer le package Nuget Microsoft.AspNetCore.SignalR.StackExchangeRedis
,
et appeler la méthode suivante lors de la configuation de SignalR :
services.AddSignalR().AddStackExchangeRedis(redisConnectionString);
Multi application
Si vous avez plusieurs applications SignalR, il est possible de les configurer pour qu'elles utilisent le même serveur Redis, mais avec des canaux différents pour éviter qu'elles ne se marchent dessus :
services.AddSignalR()
.AddStackExchangeRedis(connectionString, options => {
options.Configuration.ChannelPrefix = "App1";
});
graph BT redis(Redis) subgraph Application web web1[Web] web2[Web] end subgraph API mobile api1[API] api2[API] end web1<--channel_web-->redis web2<--channel_web-->redis api1<--channel_api-->redis api2<--channel_api-->redis navigateur1<--ws-->web1 navigateur2<--ws-->web2 navigateur3<--ws-->web2 mobile1<--ws-->api1 mobile2<--ws-->api2 mobile3<--ws-->api2
Mais ce à quoi on ne pense pas forcément, c'est qu'on peut vouloir que ces deux applications utilisent
les mêmes canaux.
Imaginons que dans notre exemple, on veuille qu'une action d'un utilisateur mobile envoie des notifications
vers des utilisateurs du site web, en mettant le même ChannelPrefix
(ou en laissant la valeur vide), les
deux applications pourront communiquer sans problème
Le cas particulier de Blazor Server
Blazor permet de développer des applications web dynamiques, en utilisant uniquement C# et razor.
Il existe deux modes de déploiement : Webassembly, où le code est exécuté dans le navigateur, et Blazor Server, où le code est exécuté sur le serveur, et la
communication avec le client se fait via une connexion SignalR.
Et donc si on utilise Blazor Server, on risque de se retrouver avec les mêmes problèmes pour gérer la mise à l'échelle de la connexion SignalR. Mais Blazor arrive avec ses spécificités dont nous allons devoir tenir compte :
Concernant le premier problème de connexion, Blazor va lui aussi nécessiter un premier appel, avant d'initialiser la connexion SignalR. Ce premier appel initialise l'état du composant, et on ne pourra pas y couper, cette fois l'affinité de session est obligatoire dans tous les cas.
Si vous utilisez Azure SignalR Service, il faudra aussi configurer cette affinité auprès entre le serveur et le service :
builder.Services.AddServerSideBlazor().AddAzureSignalR(options =>
{
options.ServerStickyMode = Microsoft.Azure.SignalR.ServerStickyMode.Required;
});
Le deuxième problème ici n'en est finalement pas un : Blazor utilise la connexion SignalR pour avoir échanger entre le client et le serveur, mais les clients sur différentes instances n'ont pas forcément besoin d'échanger des messages. Dans ce cas, le backplane Redis n'est pas nécessaire.
Si vous souhaitez plus d'informations sur le déploiement d'une application Blazor Serveur, je vous renvoie vers la documentation dédiée.