Prise en charge des WebSockets dans ASP.NET Core

Cet article explique comment commencer avec les WebSockets dans ASP.NET Core. WebSocket (RFC 6455) est un protocole qui autorise des canaux de communication persistants bidirectionnels sur les connexions TCP. Son utilisation profite aux applications qui tirent parti d’une communication rapide et en temps réel, par exemple les applications de conversation, de tableau de bord et de jeu.

Affichez ou téléchargez l’exemple de code (procédure de téléchargement, procédure d’exécution).

Prise en charge des WebSockets Http/2

L’utilisation des WebSockets sur HTTP/2 tire parti des nouvelles fonctionnalités, dont les suivantes :

  • Compression des en-têtes.
  • Multiplexage, qui réduit le temps et les ressources nécessaires lors de l’envoi de plusieurs demandes au serveur.

Ces fonctionnalités prises en charge sont disponibles dans Kestrel sur toutes les plateformes prenant en charge HTTP/2. La négociation de version étant automatique dans les navigateurs et Kestrel, aucune nouvelle API n’est nécessaire.

.NET 7 a introduit la prise en charge des WebSockets sur HTTP/2 pour Kestrel, le client JavaScript SignalR et SignalR avec Blazor WebAssembly.

Notes

Les WebSockets HTTP/2 utilisent des demandes CONNECT plutôt que GET, de sorte que vos propres routes et contrôleurs peuvent nécessiter une mise à jour. Pour plus d’informations, consultez Ajouter la prise en charge des WebSockets HTTP/2 pour les contrôleurs existants dans cet article.

Les WebSockets HTTP/2 sont activés par défaut dans Chrome et Edge, et vous pouvez les activer dans FireFox sur la page about:config avec l’indicateur network.http.spdy.websockets.

Les WebSockets ont été initialement conçus pour HTTP/1.1, mais ils ont depuis été adaptés pour fonctionner sur HTTP/2. (RFC 8441)

SignalR

ASP.NET Core SignalR est une bibliothèque qui simplifie l’ajout de fonctionnalités web en temps réel dans les applications. Elle utilise des WebSockets dans la mesure du possible.

Pour la plupart des applications, nous recommandons SignalR plutôt que des WebSockets bruts. SignalR:

  • Fournit un transport de secours pour les environnements où les WebSockets ne sont pas disponibles.
  • Fournit un modèle d’application d’appel de procédure distante de base.
  • Ne présente aucun inconvénient majeur concernant le niveau de performances par rapport à l’utilisation de WebSockets bruts dans la plupart des scénarios.

Les WebSockets sur HTTP/2 sont pris en charge pour :

  • Client JavaScript SignalR ASP.NET Core
  • ASP.NET Core SignalR avec Blazor WebAssembly

Pour certaines applications, gRPC sur .NET fournit une alternative aux WebSockets.

Prérequis

  • Tout système d’exploitation prenant en charge ASP.NET Core :
    • Windows 7 / Windows Server 2008 ou ultérieur
    • Linux
    • macOS
  • Si l’application s’exécute sur Windows avec IIS :
  • Si l’application s’exécute sur HTTP.sys :
    • Windows 8/Windows Server 2012 ou ultérieur
  • Pour les navigateurs pris en charge, consultez Can I use.

Configurer les middlewares

Ajoutez l’intergiciel WebSockets dans Program.cs :

app.UseWebSockets();

Les paramètres suivants peuvent être configurés :

  • KeepAliveInterval : fréquence d’envoi de frames de « ping » au client pour garantir que les proxys maintiennent la connexion ouverte. La valeur par défaut est deux minutes.
  • AllowedOrigins : liste des valeurs d’en-tête Origin autorisées pour les requêtes WebSocket. Par défaut, toutes les origines sont autorisées. Pour plus d’informations, consultez Restriction d’origine WebSocket dans cet article.
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

app.UseWebSockets(webSocketOptions);

Accepter les requêtes WebSocket

Un peu plus tard dans le cycle de vie de la demande (plus loin dans Program.cs ou dans une méthode d’action, par exemple), vérifiez s’il s’agit d’une demande WebSocket, puis acceptez-la.

L’exemple suivant est extrait d’une partie ultérieure dans Program.cs :

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            context.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
    else
    {
        await next(context);
    }

});

Une requête WebSocket peut figurer sur toute URL, mais cet exemple de code accepte uniquement les requêtes pour /ws.

Une approche similaire peut être adoptée dans une méthode de contrôleur :

public class WebSocketController : ControllerBase
{
    [Route("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }

Lorsque vous utilisez un WebSocket, vous devez conserver le pipeline de l’intergiciel en cours d’exécution pendant la durée de la connexion. Si vous tentez d’envoyer ou de recevoir un message WebSocket une fois que le pipeline de l’intergiciel se termine, vous pouvez obtenir une exception comme suit :

System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake. ---> System.ObjectDisposedException: Cannot write to the response body, the response has completed.
Object name: 'HttpResponseStream'.

Si vous utilisez un service en arrière-plan pour écrire des données dans un WebSocket, assurez-vous que vous conservez le pipeline de l’intergiciel en cours d’exécution. Pour ce faire, utilisez un TaskCompletionSource<TResult>. Passez le TaskCompletionSource à l’arrière-plan de votre service et faites appeler TrySetResult lorsque vous avez terminé avec le WebSocket. Utilisez ensuite await sur la propriété Task durant l’envoi de la requête, comme indiqué dans l’exemple suivant :

app.Run(async (context) =>
{
    using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
    var socketFinishedTcs = new TaskCompletionSource<object>();

    BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

    await socketFinishedTcs.Task;
});

L’exception relative à la fermeture de WebSocket peut également se produire si vous effectuez trop tôt un retour à partir d’une méthode d’action. Si vous acceptez un socket dans une méthode d’action, attendez que le code qui l’utilise soit entièrement exécuté, avant d’effectuer un retour à partir de la méthode d’action.

N’utilisez jamais Task.Wait, Task.Result ou des appels de blocage similaires pour attendre la fin de l’exécution du socket, car cela peut entraîner de graves problèmes de thread. Utilisez toujours await.

Ajouter la prise en charge des WebSockets HTTP/2 pour les contrôleurs existants

.NET 7 a introduit la prise en charge des WebSockets sur HTTP/2 pour Kestrel, le client JavaScript SignalR et SignalR avec Blazor WebAssembly. Les WebSockets HTTP/2 utilisent des requêtes CONNECT plutôt que GET. Si vous avez déjà utilisé [HttpGet("/path")] sur votre méthode d’action de contrôleur pour les demandes Websocket, mettez-la à jour pour utiliser [Route("/path")] à la place.

public class WebSocketController : ControllerBase
{
    [Route("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }

Compression

Avertissement

L’activation de la compression sur des connexions chiffrées peut exposer une application à des attaques CRIME/BREACH. Si vous envoyez des informations sensibles, évitez d’activer la compression ou utilisez WebSocketMessageFlags.DisableCompression lorsque vous appelez WebSocket.SendAsync. Cela s’applique aux deux côtés du WebSocket. Notez que l’API WebSockets dans le navigateur n’a pas de configuration pour désactiver la compression par envoi.

Si la compression des messages sur WebSockets est souhaitée, le code d’acceptation doit spécifier qu’il autorise la compression comme suit :

using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(
    new WebSocketAcceptContext { DangerousEnableCompression = true }))
{

}

WebSocketAcceptContext.ServerMaxWindowBits et WebSocketAcceptContext.DisableServerContextTakeover sont des options avancées qui contrôlent le fonctionnement de la compression.

La compression est négociée entre le client et le serveur lors de l’établissement initial d’une connexion. Vous pouvez en apprendre davantage sur la négociation dans le document RFC sur les extensions de compression pour WebSocket.

Notes

Si la négociation de compression n’est pas acceptée par le serveur ou le client, la connexion est toujours établie. Toutefois, la connexion n’utilise pas la compression lors de l’envoi et de la réception de messages.

Envoyer et recevoir des messages

La méthode AcceptWebSocketAsync met à niveau la connexion TCP vers une connexion WebSocket, et fournit un objet WebSocket. Utilisez l’objet WebSocket pour envoyer et recevoir des messages.

Le code montré précédemment qui accepte la requête WebSocket passe l’objet WebSocket à une méthode Echo. Le code reçoit un message et renvoie immédiatement le même message. Les messages sont envoyés et reçus dans une boucle jusqu’à ce que le client ferme la connexion :

private static async Task Echo(WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    var receiveResult = await webSocket.ReceiveAsync(
        new ArraySegment<byte>(buffer), CancellationToken.None);

    while (!receiveResult.CloseStatus.HasValue)
    {
        await webSocket.SendAsync(
            new ArraySegment<byte>(buffer, 0, receiveResult.Count),
            receiveResult.MessageType,
            receiveResult.EndOfMessage,
            CancellationToken.None);

        receiveResult = await webSocket.ReceiveAsync(
            new ArraySegment<byte>(buffer), CancellationToken.None);
    }

    await webSocket.CloseAsync(
        receiveResult.CloseStatus.Value,
        receiveResult.CloseStatusDescription,
        CancellationToken.None);
}

Quand vous acceptez le WebSocket avant de commencer cette boucle, le pipeline de middlewares se termine. À la fermeture du socket, le pipeline se déroule. Autrement dit, la requête cesse d’avancer dans le pipeline quand le WebSocket est accepté. Quand la boucle est terminée et que le socket est fermé, la requête recule dans le pipeline.

Gérer la déconnexion du client

Le serveur n’est pas automatiquement informé lorsque le client se déconnecte en raison d’une perte de connectivité. Le serveur reçoit un message de déconnexion uniquement si le client l’envoie, ce qui n’est pas possible si la connexion Internet est perdue. Si vous souhaitez prendre des mesures quand cela se produit, définissez un délai d’attente lorsque rien n’est reçu du client dans un certain laps de temps.

Si le client n’envoie pas toujours de messages et que vous ne souhaitez pas appliquer le délai d’attente uniquement parce que la connexion devient inactive, demandez au client d’utiliser un minuteur pour envoyer un message ping toutes les X secondes. Sur le serveur, si un message n’arrive pas dans un délai de 2*X secondes après le précédent, mettez fin à la connexion et signalez que le client s’est déconnecté. Attendez deux fois la durée de l’intervalle de temps attendu pour autoriser les délais réseau qui peuvent retarder le message ping.

Restriction d’origine WebSocket

Les protections fournies par CORS ne s’appliquent pas aux WebSockets. Les navigateurs :

  • n’effectuent pas de requêtes préalables CORS ;
  • respectent les restrictions spécifiées dans les en-têtes Access-Control quand ils effectuent des requêtes WebSocket.

Toutefois, les navigateurs envoient l’en-tête Origin au moment de l’émission des requêtes WebSocket. Les applications doivent être configurées de manière à valider ces en-têtes, le but étant de vérifier que seuls les WebSockets provenant des origines attendues sont autorisés.

Si vous hébergez votre serveur sur « https://server.com" et votre client sur « https://client.com", ajoutez « https://client.com" à la liste AllowedOrigins à vérifier par les WebSockets.

var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");

app.UseWebSockets(webSocketOptions);

Notes

L’en-tête Origin est contrôlé par le client et, comme l’en-tête Referer, peut être falsifié. N’utilisez pas ces en-têtes comme mécanisme d’authentification.

Prise en charge d’IIS/IIS Express

Windows Server 2012 ou version ultérieure et Windows 8 ou version ultérieure avec IIS/IIS Express 8 ou version ultérieure prennent en charge le protocole WebSocket, mais pas les WebSockets sur HTTP/2.

Notes

WebSockets sont toujours activés lorsque vous utilisez IIS Express.

Activation de WebSockets sur IIS

Pour activer la prise en charge du protocole WebSocket sur Windows Server 2012 ou ultérieur :

Notes

Ces étapes ne sont pas nécessaires si vous utilisez IIS Express

  1. Utilisez l’Assistant Ajouter des rôles et des fonctionnalités par le biais du menu Gérer ou du lien dans Gestionnaire de serveur.
  2. Sélectionnez Installation basée sur un rôle ou une fonctionnalité. Sélectionnez Suivant.
  3. Sélectionnez le serveur approprié (le serveur local est sélectionné par défaut). Sélectionnez Suivant.
  4. Développez Serveur web (IIS) dans l’arborescence Rôles, développez Serveur Web, puis développez Développement d’applications.
  5. Sélectionnez Protocole WebSocket. Sélectionnez Suivant.
  6. Si vous n’avez pas besoin d’autres fonctionnalités, sélectionnez Suivant.
  7. Sélectionnez Installer.
  8. Une fois l’installation terminée, sélectionnez Fermer pour quitter l’Assistant.

Pour activer la prise en charge du protocole WebSocket sur Windows 8 ou ultérieur :

Notes

Ces étapes ne sont pas nécessaires si vous utilisez IIS Express

  1. Naviguez jusqu’à Panneau de configuration>Programmes>Programmes et fonctionnalités>Activer ou désactiver des fonctionnalités Windows (à gauche de l’écran).
  2. Ouvrez les nœuds suivants : Internet Information Services>Services World Wide Web>Fonctionnalités de développement d’applications.
  3. Sélectionnez la fonctionnalité Protocole WebSocket. Sélectionnez OK.

Désactiver WebSocket lors de l’utilisation de socket.io sur Node.js

Si vous utilisez la prise en charge de WebSocket dans socket.io sur Node.js, désactivez le module WebSocket IIS par défaut à l’aide de l’élément webSocket dans web.config ou applicationHost.config. Si cette étape n’est pas effectuée, le module WebSocket IIS tente de gérer la communication WebSocket, au lieu de celle de Node.js et de l’application.

<system.webServer>
  <webSocket enabled="false" />
</system.webServer>

Exemple d'application

L’exemple d’application qui accompagne cet article est une application d’écho. Elle comporte une page web qui établit des connexions WebSocket, et le serveur renvoie au client les messages qu’il reçoit. L’exemple d’application prend en charge les WebSockets sur HTTP/2 lors de l’utilisation d’un framework ciblé de .NET 7 ou version ultérieure.

Exécutez l’application :

  • Pour exécuter l’application dans Visual Studio : ouvrez l’exemple de projet dans Visual Studio et appuyez sur Ctrl+F5 pour l’exécuter sans le débogueur.
  • Pour exécuter l’application dans un interpréteur de commandes : exécutez la commande dotnet run et accédez dans un navigateur à http://localhost:<port>.

La page web affiche l’état de connexion :

Initial state of webpage before WebSockets connection

Sélectionnez Connect (Connexion) pour envoyer une requête WebSocket à l’URL affichée. Entrez un message de test, puis sélectionnez Send (Envoyer). Quand vous avez terminé, sélectionnez Close Socket (Fermer le socket). La section Communication Log (Journal des communications) signale chaque action d’ouverture, d’envoi et de fermeture en temps réel.

Final state of webpage after WebSockets connection and test messages are sent and received

Cet article explique comment commencer avec les WebSockets dans ASP.NET Core. WebSocket (RFC 6455) est un protocole qui autorise des canaux de communication persistants bidirectionnels sur les connexions TCP. Son utilisation profite aux applications qui tirent parti d’une communication rapide et en temps réel, par exemple les applications de conversation, de tableau de bord et de jeu.

Affichez ou téléchargez l’exemple de code (procédure de téléchargement, procédure d’exécution).

SignalR

ASP.NET Core SignalR est une bibliothèque qui simplifie l’ajout de fonctionnalités web en temps réel dans les applications. Elle utilise des WebSockets dans la mesure du possible.

Pour la plupart des applications, nous recommandons SignalR sur des WebSockets bruts. SignalR fournit un transport de secours pour les environnements où les WebSockets ne sont pas disponibles. Il fournit également un modèle d’application d’appel de procédure distante de base. De plus, dans la plupart des scénarios, SignalR ne présente aucun inconvénient majeur concernant le niveau de performances par rapport à l’utilisation de WebSockets bruts.

Pour certaines applications, gRPC sur .NET fournit une alternative aux WebSockets.

Prérequis

  • Tout système d’exploitation prenant en charge ASP.NET Core :
    • Windows 7 / Windows Server 2008 ou ultérieur
    • Linux
    • macOS
  • Si l’application s’exécute sur Windows avec IIS :
  • Si l’application s’exécute sur HTTP.sys :
    • Windows 8/Windows Server 2012 ou ultérieur
  • Pour les navigateurs pris en charge, consultez Can I use.

Configurer les middlewares

Ajoutez l’intergiciel WebSockets dans Program.cs :

app.UseWebSockets();

Les paramètres suivants peuvent être configurés :

  • KeepAliveInterval : fréquence d’envoi de frames de « ping » au client pour garantir que les proxys maintiennent la connexion ouverte. La valeur par défaut est deux minutes.
  • AllowedOrigins : liste des valeurs d’en-tête Origin autorisées pour les requêtes WebSocket. Par défaut, toutes les origines sont autorisées. Pour plus d’informations, consultez Restriction d’origine WebSocket dans cet article.
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

app.UseWebSockets(webSocketOptions);

Accepter les requêtes WebSocket

Un peu plus tard dans le cycle de vie de la demande (plus loin dans Program.cs ou dans une méthode d’action, par exemple), vérifiez s’il s’agit d’une demande WebSocket, puis acceptez-la.

L’exemple suivant est extrait d’une partie ultérieure dans Program.cs :

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            context.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
    else
    {
        await next(context);
    }

});

Une requête WebSocket peut figurer sur toute URL, mais cet exemple de code accepte uniquement les requêtes pour /ws.

Une approche similaire peut être adoptée dans une méthode de contrôleur :

public class WebSocketController : ControllerBase
{
    [HttpGet("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }

Lorsque vous utilisez un WebSocket, vous devez conserver le pipeline de l’intergiciel en cours d’exécution pendant la durée de la connexion. Si vous tentez d’envoyer ou de recevoir un message WebSocket une fois que le pipeline de l’intergiciel se termine, vous pouvez obtenir une exception comme suit :

System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake. ---> System.ObjectDisposedException: Cannot write to the response body, the response has completed.
Object name: 'HttpResponseStream'.

Si vous utilisez un service en arrière-plan pour écrire des données dans un WebSocket, assurez-vous que vous conservez le pipeline de l’intergiciel en cours d’exécution. Pour ce faire, utilisez un TaskCompletionSource<TResult>. Passez le TaskCompletionSource à l’arrière-plan de votre service et faites appeler TrySetResult lorsque vous avez terminé avec le WebSocket. Utilisez ensuite await sur la propriété Task durant l’envoi de la requête, comme indiqué dans l’exemple suivant :

app.Run(async (context) =>
{
    using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
    var socketFinishedTcs = new TaskCompletionSource<object>();

    BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

    await socketFinishedTcs.Task;
});

L’exception relative à la fermeture de WebSocket peut également se produire si vous effectuez trop tôt un retour à partir d’une méthode d’action. Si vous acceptez un socket dans une méthode d’action, attendez que le code qui l’utilise soit entièrement exécuté, avant d’effectuer un retour à partir de la méthode d’action.

N’utilisez jamais Task.Wait, Task.Result ou des appels de blocage similaires pour attendre la fin de l’exécution du socket, car cela peut entraîner de graves problèmes de thread. Utilisez toujours await.

Compression

Avertissement

L’activation de la compression sur des connexions chiffrées peut exposer une application à des attaques CRIME/BREACH. Si vous envoyez des informations sensibles, évitez d’activer la compression ou utilisez WebSocketMessageFlags.DisableCompression lorsque vous appelez WebSocket.SendAsync. Cela s’applique aux deux côtés du WebSocket. Notez que l’API WebSockets dans le navigateur n’a pas de configuration pour désactiver la compression par envoi.

Si la compression des messages sur WebSockets est souhaitée, le code d’acceptation doit spécifier qu’il autorise la compression comme suit :

using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(
    new WebSocketAcceptContext { DangerousEnableCompression = true }))
{

}

WebSocketAcceptContext.ServerMaxWindowBits et WebSocketAcceptContext.DisableServerContextTakeover sont des options avancées qui contrôlent le fonctionnement de la compression.

La compression est négociée entre le client et le serveur lors de l’établissement initial d’une connexion. Vous pouvez en apprendre davantage sur la négociation dans le document RFC sur les extensions de compression pour WebSocket.

Notes

Si la négociation de compression n’est pas acceptée par le serveur ou le client, la connexion est toujours établie. Toutefois, la connexion n’utilise pas la compression lors de l’envoi et de la réception de messages.

Envoyer et recevoir des messages

La méthode AcceptWebSocketAsync met à niveau la connexion TCP vers une connexion WebSocket, et fournit un objet WebSocket. Utilisez l’objet WebSocket pour envoyer et recevoir des messages.

Le code montré précédemment qui accepte la requête WebSocket passe l’objet WebSocket à une méthode Echo. Le code reçoit un message et renvoie immédiatement le même message. Les messages sont envoyés et reçus dans une boucle jusqu’à ce que le client ferme la connexion :

private static async Task Echo(WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    var receiveResult = await webSocket.ReceiveAsync(
        new ArraySegment<byte>(buffer), CancellationToken.None);

    while (!receiveResult.CloseStatus.HasValue)
    {
        await webSocket.SendAsync(
            new ArraySegment<byte>(buffer, 0, receiveResult.Count),
            receiveResult.MessageType,
            receiveResult.EndOfMessage,
            CancellationToken.None);

        receiveResult = await webSocket.ReceiveAsync(
            new ArraySegment<byte>(buffer), CancellationToken.None);
    }

    await webSocket.CloseAsync(
        receiveResult.CloseStatus.Value,
        receiveResult.CloseStatusDescription,
        CancellationToken.None);
}

Quand vous acceptez le WebSocket avant de commencer cette boucle, le pipeline de middlewares se termine. À la fermeture du socket, le pipeline se déroule. Autrement dit, la requête cesse d’avancer dans le pipeline quand le WebSocket est accepté. Quand la boucle est terminée et que le socket est fermé, la requête recule dans le pipeline.

Gérer la déconnexion du client

Le serveur n’est pas automatiquement informé lorsque le client se déconnecte en raison d’une perte de connectivité. Le serveur reçoit un message de déconnexion uniquement si le client l’envoie, ce qui n’est pas possible si la connexion Internet est perdue. Si vous souhaitez prendre des mesures quand cela se produit, définissez un délai d’attente lorsque rien n’est reçu du client dans un certain laps de temps.

Si le client n’envoie pas toujours de messages et que vous ne souhaitez pas appliquer le délai d’attente uniquement parce que la connexion devient inactive, demandez au client d’utiliser un minuteur pour envoyer un message ping toutes les X secondes. Sur le serveur, si un message n’arrive pas dans un délai de 2*X secondes après le précédent, mettez fin à la connexion et signalez que le client s’est déconnecté. Attendez deux fois la durée de l’intervalle de temps attendu pour autoriser les délais réseau qui peuvent retarder le message ping.

Restriction d’origine WebSocket

Les protections fournies par CORS ne s’appliquent pas aux WebSockets. Les navigateurs :

  • n’effectuent pas de requêtes préalables CORS ;
  • respectent les restrictions spécifiées dans les en-têtes Access-Control quand ils effectuent des requêtes WebSocket.

Toutefois, les navigateurs envoient l’en-tête Origin au moment de l’émission des requêtes WebSocket. Les applications doivent être configurées de manière à valider ces en-têtes, le but étant de vérifier que seuls les WebSockets provenant des origines attendues sont autorisés.

Si vous hébergez votre serveur sur « https://server.com" et votre client sur « https://client.com", ajoutez « https://client.com" à la liste AllowedOrigins à vérifier par les WebSockets.

var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");

app.UseWebSockets(webSocketOptions);

Notes

L’en-tête Origin est contrôlé par le client et, comme l’en-tête Referer, peut être falsifié. N’utilisez pas ces en-têtes comme mécanisme d’authentification.

Prise en charge d’IIS/IIS Express

Windows Server 2012 ou ultérieur et Windows 8 ou ultérieur avec IIS/IIS Express 8 ou ultérieure prennent en charge le protocole WebSocket.

Notes

WebSockets sont toujours activés lorsque vous utilisez IIS Express.

Activation de WebSockets sur IIS

Pour activer la prise en charge du protocole WebSocket sur Windows Server 2012 ou ultérieur :

Notes

Ces étapes ne sont pas nécessaires si vous utilisez IIS Express

  1. Utilisez l’Assistant Ajouter des rôles et des fonctionnalités par le biais du menu Gérer ou du lien dans Gestionnaire de serveur.
  2. Sélectionnez Installation basée sur un rôle ou une fonctionnalité. Sélectionnez Suivant.
  3. Sélectionnez le serveur approprié (le serveur local est sélectionné par défaut). Sélectionnez Suivant.
  4. Développez Serveur web (IIS) dans l’arborescence Rôles, développez Serveur Web, puis développez Développement d’applications.
  5. Sélectionnez Protocole WebSocket. Sélectionnez Suivant.
  6. Si vous n’avez pas besoin d’autres fonctionnalités, sélectionnez Suivant.
  7. Sélectionnez Installer.
  8. Une fois l’installation terminée, sélectionnez Fermer pour quitter l’Assistant.

Pour activer la prise en charge du protocole WebSocket sur Windows 8 ou ultérieur :

Notes

Ces étapes ne sont pas nécessaires si vous utilisez IIS Express

  1. Naviguez jusqu’à Panneau de configuration>Programmes>Programmes et fonctionnalités>Activer ou désactiver des fonctionnalités Windows (à gauche de l’écran).
  2. Ouvrez les nœuds suivants : Internet Information Services>Services World Wide Web>Fonctionnalités de développement d’applications.
  3. Sélectionnez la fonctionnalité Protocole WebSocket. Sélectionnez OK.

Désactiver WebSocket lors de l’utilisation de socket.io sur Node.js

Si vous utilisez la prise en charge de WebSocket dans socket.io sur Node.js, désactivez le module WebSocket IIS par défaut à l’aide de l’élément webSocket dans web.config ou applicationHost.config. Si cette étape n’est pas effectuée, le module WebSocket IIS tente de gérer la communication WebSocket, au lieu de celle de Node.js et de l’application.

<system.webServer>
  <webSocket enabled="false" />
</system.webServer>

Exemple d'application

L’exemple d’application qui accompagne cet article est une application d’écho. Elle comporte une page web qui établit des connexions WebSocket, et le serveur renvoie au client les messages qu’il reçoit. L’exemple d’application n’est pas configuré pour s’exécuter à partir de Visual Studio avec IIS Express. Par conséquent, exécutez l’application dans un interpréteur de commandes avec dotnet run et accédez dans un navigateur à http://localhost:<port>. La page web affiche l’état de connexion :

Initial state of webpage before WebSockets connection

Sélectionnez Connect (Connexion) pour envoyer une requête WebSocket à l’URL affichée. Entrez un message de test, puis sélectionnez Send (Envoyer). Quand vous avez terminé, sélectionnez Close Socket (Fermer le socket). La section Communication Log (Journal des communications) signale chaque action d’ouverture, d’envoi et de fermeture en temps réel.

Final state of webpage after WebSockets connection and test messages are sent and received

Cet article explique comment commencer avec les WebSockets dans ASP.NET Core. WebSocket (RFC 6455) est un protocole qui autorise des canaux de communication persistants bidirectionnels sur les connexions TCP. Son utilisation profite aux applications qui tirent parti d’une communication rapide et en temps réel, par exemple les applications de conversation, de tableau de bord et de jeu.

Affichez ou téléchargez un exemple de code (procédure de téléchargement). Comment exécuter.

SignalR

ASP.NET Core SignalR est une bibliothèque qui simplifie l’ajout de fonctionnalités web en temps réel dans les applications. Elle utilise des WebSockets dans la mesure du possible.

Pour la plupart des applications, nous recommandons SignalR sur des WebSockets bruts. SignalR fournit un transport de secours pour les environnements où les WebSockets ne sont pas disponibles. Il fournit également un modèle d’application d’appel de procédure distante de base. De plus, dans la plupart des scénarios, SignalR ne présente aucun inconvénient majeur concernant le niveau de performances par rapport à l’utilisation de WebSockets bruts.

Pour certaines applications, gRPC sur .NET fournit une alternative aux WebSockets.

Prérequis

  • Tout système d’exploitation prenant en charge ASP.NET Core :
    • Windows 7 / Windows Server 2008 ou ultérieur
    • Linux
    • macOS
  • Si l’application s’exécute sur Windows avec IIS :
  • Si l’application s’exécute sur HTTP.sys :
    • Windows 8/Windows Server 2012 ou ultérieur
  • Pour les navigateurs pris en charge, consultez Can I use.

Configurer les middlewares

Ajoutez le middleware WebSocket dans la méthode Configure de la classe Startup :

app.UseWebSockets();

Notes

Si vous souhaitez accepter les demandes WebSocket dans un contrôleur, l’appel à app.UseWebSockets doit avoir lieu avant app.UseEndpoints.

Les paramètres suivants peuvent être configurés :

  • KeepAliveInterval : fréquence d’envoi de frames de « ping » au client pour garantir que les proxys maintiennent la connexion ouverte. La valeur par défaut est deux minutes.
  • AllowedOrigins : liste des valeurs d’en-tête Origin autorisées pour les requêtes WebSocket. Par défaut, toutes les origines sont autorisées. Pour plus d’informations, consultez « Restriction d’origine WebSocket » ci-dessous.
var webSocketOptions = new WebSocketOptions()
{
    KeepAliveInterval = TimeSpan.FromSeconds(120),
};

app.UseWebSockets(webSocketOptions);

Accepter les requêtes WebSocket

Un peu plus tard dans le cycle de vie de la requête (plus loin dans la méthode Configure ou dans une méthode action, par exemple), vérifiez s’il s’agit d’une requête WebSocket, puis acceptez-la.

L’exemple suivant est extrait d’une partie ultérieure de la méthode Configure :

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            using (WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync())
            {
                await Echo(context, webSocket);
            }
        }
        else
        {
            context.Response.StatusCode = (int) HttpStatusCode.BadRequest;
        }
    }
    else
    {
        await next();
    }

});

Une requête WebSocket peut figurer sur toute URL, mais cet exemple de code accepte uniquement les requêtes pour /ws.

Une approche similaire peut être adoptée dans une méthode de contrôleur :

public class WebSocketController : ControllerBase
{
    [HttpGet("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }

Lorsque vous utilisez un WebSocket, vous devez conserver le pipeline de l’intergiciel en cours d’exécution pendant la durée de la connexion. Si vous tentez d’envoyer ou de recevoir un message WebSocket une fois que le pipeline de l’intergiciel se termine, vous pouvez obtenir une exception comme suit :

System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake. ---> System.ObjectDisposedException: Cannot write to the response body, the response has completed.
Object name: 'HttpResponseStream'.

Si vous utilisez un service en arrière-plan pour écrire des données dans un WebSocket, assurez-vous que vous conservez le pipeline de l’intergiciel en cours d’exécution. Pour ce faire, utilisez un TaskCompletionSource<TResult>. Passez le TaskCompletionSource à l’arrière-plan de votre service et faites appeler TrySetResult lorsque vous avez terminé avec le WebSocket. Utilisez ensuite await sur la propriété Task durant l’envoi de la requête, comme indiqué dans l’exemple suivant :

app.Use(async (context, next) =>
{
    using (WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync())
    {
        var socketFinishedTcs = new TaskCompletionSource<object>();

        BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

        await socketFinishedTcs.Task;
    }
});

L’exception relative à la fermeture de WebSocket peut également se produire si vous effectuez trop tôt un retour à partir d’une méthode d’action. Si vous acceptez un socket dans une méthode d’action, attendez que le code qui l’utilise soit entièrement exécuté, avant d’effectuer un retour à partir de la méthode d’action.

N’utilisez jamais Task.Wait, Task.Result ou des appels de blocage similaires pour attendre la fin de l’exécution du socket, car cela peut entraîner de graves problèmes de thread. Utilisez toujours await.

Envoyer et recevoir des messages

La méthode AcceptWebSocketAsync met à niveau la connexion TCP vers une connexion WebSocket, et fournit un objet WebSocket. Utilisez l’objet WebSocket pour envoyer et recevoir des messages.

Le code montré précédemment qui accepte la requête WebSocket passe l’objet WebSocket à une méthode Echo. Le code reçoit un message et renvoie immédiatement le même message. Les messages sont envoyés et reçus dans une boucle jusqu’à ce que le client ferme la connexion :

private async Task Echo(HttpContext context, WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
    while (!result.CloseStatus.HasValue)
    {
        await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);

        result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
    }
    await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}

Quand vous acceptez le WebSocket avant de commencer cette boucle, le pipeline de middlewares se termine. À la fermeture du socket, le pipeline se déroule. Autrement dit, la requête cesse d’avancer dans le pipeline quand le WebSocket est accepté. Quand la boucle est terminée et que le socket est fermé, la requête recule dans le pipeline.

Gérer la déconnexion du client

Le serveur n’est pas automatiquement informé lorsque le client se déconnecte en raison d’une perte de connectivité. Le serveur reçoit un message de déconnexion uniquement si le client l’envoie, ce qui n’est pas possible si la connexion Internet est perdue. Si vous souhaitez prendre des mesures quand cela se produit, définissez un délai d’attente lorsque rien n’est reçu du client dans un certain laps de temps.

Si le client n’envoie pas toujours de messages et que vous ne souhaitez pas appliquer le délai d’attente uniquement parce que la connexion devient inactive, demandez au client d’utiliser un minuteur pour envoyer un message ping toutes les X secondes. Sur le serveur, si un message n’arrive pas dans un délai de 2*X secondes après le précédent, mettez fin à la connexion et signalez que le client s’est déconnecté. Attendez deux fois la durée de l’intervalle de temps attendu pour autoriser les délais réseau qui peuvent retarder le message ping.

Remarque

Le système interne ManagedWebSocket gère implicitement les trames Ping/Pong pour maintenir la connexion en vie si l’option KeepAliveInterval est supérieure à zéro, ce qui correspond par défaut à 30 secondes (TimeSpan.FromSeconds(30)).

Restriction d’origine WebSocket

Les protections fournies par CORS ne s’appliquent pas aux WebSockets. Les navigateurs :

  • n’effectuent pas de requêtes préalables CORS ;
  • respectent les restrictions spécifiées dans les en-têtes Access-Control quand ils effectuent des requêtes WebSocket.

Toutefois, les navigateurs envoient l’en-tête Origin au moment de l’émission des requêtes WebSocket. Les applications doivent être configurées de manière à valider ces en-têtes, le but étant de vérifier que seuls les WebSockets provenant des origines attendues sont autorisés.

Si vous hébergez votre serveur sur « https://server.com" et votre client sur « https://client.com", ajoutez « https://client.com" à la liste AllowedOrigins à vérifier par les WebSockets.

var webSocketOptions = new WebSocketOptions()
{
    KeepAliveInterval = TimeSpan.FromSeconds(120),
};
webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");

app.UseWebSockets(webSocketOptions);

Notes

L’en-tête Origin est contrôlé par le client et, comme l’en-tête Referer, peut être falsifié. N’utilisez pas ces en-têtes comme mécanisme d’authentification.

Prise en charge d’IIS/IIS Express

Windows Server 2012 ou ultérieur et Windows 8 ou ultérieur avec IIS/IIS Express 8 ou ultérieure prennent en charge le protocole WebSocket.

Notes

WebSockets sont toujours activés lorsque vous utilisez IIS Express.

Activation de WebSockets sur IIS

Pour activer la prise en charge du protocole WebSocket sur Windows Server 2012 ou ultérieur :

Notes

Ces étapes ne sont pas nécessaires si vous utilisez IIS Express

  1. Utilisez l’Assistant Ajouter des rôles et des fonctionnalités par le biais du menu Gérer ou du lien dans Gestionnaire de serveur.
  2. Sélectionnez Installation basée sur un rôle ou une fonctionnalité. Sélectionnez Suivant.
  3. Sélectionnez le serveur approprié (le serveur local est sélectionné par défaut). Sélectionnez Suivant.
  4. Développez Serveur web (IIS) dans l’arborescence Rôles, développez Serveur Web, puis développez Développement d’applications.
  5. Sélectionnez Protocole WebSocket. Sélectionnez Suivant.
  6. Si vous n’avez pas besoin d’autres fonctionnalités, sélectionnez Suivant.
  7. Sélectionnez Installer.
  8. Une fois l’installation terminée, sélectionnez Fermer pour quitter l’Assistant.

Pour activer la prise en charge du protocole WebSocket sur Windows 8 ou ultérieur :

Notes

Ces étapes ne sont pas nécessaires si vous utilisez IIS Express

  1. Naviguez jusqu’à Panneau de configuration>Programmes>Programmes et fonctionnalités>Activer ou désactiver des fonctionnalités Windows (à gauche de l’écran).
  2. Ouvrez les nœuds suivants : Internet Information Services>Services World Wide Web>Fonctionnalités de développement d’applications.
  3. Sélectionnez la fonctionnalité Protocole WebSocket. Sélectionnez OK.

Désactiver WebSocket lors de l’utilisation de socket.io sur Node.js

Si vous utilisez la prise en charge de WebSocket dans socket.io sur Node.js, désactivez le module WebSocket IIS par défaut à l’aide de l’élément webSocket dans web.config ou applicationHost.config. Si cette étape n’est pas effectuée, le module WebSocket IIS tente de gérer la communication WebSocket, au lieu de celle de Node.js et de l’application.

<system.webServer>
  <webSocket enabled="false" />
</system.webServer>

Exemple d'application

L’exemple d’application qui accompagne cet article est une application d’écho. Elle comporte une page web qui établit des connexions WebSocket, et le serveur renvoie au client les messages qu’il reçoit. L’exemple d’application n’est pas configuré pour s’exécuter à partir de Visual Studio avec IIS Express. Par conséquent, exécutez l’application dans un interpréteur de commandes avec dotnet run et accédez dans un navigateur à http://localhost:5000. La page web affiche l’état de connexion :

Initial state of webpage before WebSockets connection

Sélectionnez Connect (Connexion) pour envoyer une requête WebSocket à l’URL affichée. Entrez un message de test, puis sélectionnez Send (Envoyer). Quand vous avez terminé, sélectionnez Close Socket (Fermer le socket). La section Communication Log (Journal des communications) signale chaque action d’ouverture, d’envoi et de fermeture en temps réel.

Final state of webpage after WebSockets connection and test messages are sent and received