Compatibilidad con WebSockets en ASP.NET Core

En este artículo se ofrece una introducción a WebSockets en ASP.NET Core. WebSocket (RFC 6455) es un protocolo que habilita canales de comunicación bidireccional persistentes a través de conexiones TCP. Se usa en aplicaciones que sacan partido de comunicaciones rápidas y en tiempo real, como las aplicaciones de chat, panel y juegos.

Vea o descargue el código de ejemplo (cómo descargarlo, cómo ejecutarlo).

Compatibilidad con Http/2 WebSockets

El uso de WebSockets mediante HTTP/2 aprovecha las nuevas características, como las siguientes:

  • Compresión de encabezados.
  • Multiplexación, que reduce el tiempo y los recursos necesarios al realizar varias solicitudes al servidor.

Estas características admitidas están disponibles en Kestrel en todas las plataformas habilitadas para HTTP/2. La negociación de versiones es automática en exploradores y Kestrel, por lo que no se necesitan API nuevas.

En .NET 7 se ha incorporado Websockets mediante la compatibilidad con HTTP/2 para Kestrel, el cliente de JavaScript SignalR y SignalR con Blazor WebAssembly.

Nota

En WebSockets Http/2 se usan solicitudes CONNECT en lugar de GET, por lo que es posible que tenga que actualizar las rutas y los controladores propios. Para más información, vea Adición de compatibilidad con WebSockets HTTP/2 para controladores existentes en este artículo.

En Chrome y Edge WebSockets HTTP/2 está habilitado de manera predeterminada, y en FireFox se puede habilitar en la página about:config con la marca network.http.spdy.websockets.

WebSockets se diseñó originalmente para HTTP/1.1, pero desde entonces se ha adaptado para funcionar en HTTP/2. (RFC 8441)

SignalR

ASP.NET CoreSignalR es una biblioteca que simplifica la adición de funcionalidad web en tiempo real a las aplicaciones. Usa WebSockets siempre que sea posible.

Para la mayoría de las aplicaciones, se recomienda SignalR en lugar de WebSockets sin procesar. SignalR:

  • Proporciona transporte de reserva para entornos donde WebSockets no está disponible.
  • Proporciona un modelo básico de aplicación de llamada a procedimiento remoto.
  • En la mayoría de los escenarios, no tiene ninguna desventaja significativa de rendimiento en comparación con WebSockets sin procesar.

WebSockets sobre HTTP/2 es compatible con:

  • Cliente de JavaScript SignalR en ASP.NET Core
  • SignalR de ASP.NET Core con Blazor WebAssembly

En algunas aplicaciones, gRPC en .NET proporciona una alternativa a WebSockets.

Requisitos previos

  • Cualquier sistema operativo que admita ASP.NET Core:
    • Windows 7/Windows Server 2008 o posterior
    • Linux
    • macOS
  • Si la aplicación se ejecuta en Windows con IIS:
  • Si la aplicación se ejecuta en HTTP.sys:
    • Windows 8/Windows Server 2012 o versiones posteriores
  • Para saber qué exploradores son compatibles, vea Can I use.

Configurar el middleware

Agregue el middleware de WebSockets en Program.cs:

app.UseWebSockets();

Se pueden configurar estas opciones:

  • KeepAliveInterval: la frecuencia con que se envían marcos "ping" al cliente, para asegurarse de que los servidores proxy mantienen abierta la conexión. El valor predeterminado es de dos minutos.
  • AllowedOrigins - Una lista de valores de encabezado de origen permitidos para las solicitudes WebSocket. De forma predeterminada, se permiten todos los orígenes. Para obtener más información, vea Restricción de los orígenes de WebSocket en este artículo.
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

app.UseWebSockets(webSocketOptions);

Aceptar solicitudes WebSocket

En algún momento posterior del ciclo de las solicitudes (más adelante en Program.cs o en un método de acción, por ejemplo) debe comprobar si se trata de una solicitud WebSocket y aceptarla.

El ejemplo siguiente es de un momento posterior en 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);
    }

});

Una solicitud WebSocket puede proceder de cualquier dirección URL, pero este código de ejemplo solo acepta solicitudes de /ws.

Se puede adoptar un enfoque similar en un método de controlador:

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

Cuando se usa un WebSocket, debe mantener la canalización de middleware en ejecución durante la duración de la conexión. Si intenta enviar o recibir un mensaje de WebSocket después de que finalice la canalización de middleware, es posible que obtenga una excepción como la siguiente:

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 utiliza un servicio en segundo plano para escribir datos en un WebSocket, asegúrese de mantener en ejecución el canal de middleware. Para ello, utilice un TaskCompletionSource<TResult>. Pase TaskCompletionSource a su servicio de segundo plano y pídale que llame a TrySetResult cuando termine con WebSocket. Después, espere (con await) la propiedad Task durante la solicitud, como se muestra en el ejemplo siguiente:

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

    BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

    await socketFinishedTcs.Task;
});

La excepción de cierre de WebSocket también puede producirse si la devolución de un método de acción ocurre demasiado pronto. Al aceptar un socket en un método de acción, espere a que finalice el código que usa el socket antes de devolver el método de acción.

No use nunca Task.Wait, Task.Result ni llamadas de bloqueo similares para esperar a que se complete el socket, ya que pueden causar graves problemas de subprocesamiento. Use siempre await.

Adición de compatibilidad con WebSockets HTTP/2 para controladores existentes

En .NET 7 se ha incorporado Websockets mediante la compatibilidad con HTTP/2 para Kestrel, el cliente de JavaScript SignalR y SignalR con Blazor WebAssembly. En WebSockets HTTP/2 se usan solicitudes CONNECT en lugar de GET. Si anteriormente usó [HttpGet("/path")] en el método de acción del controlador para las solicitudes de Websocket, actualícelo para usar [Route("/path")] en su lugar.

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

Compresión

Advertencia

La habilitación de la compresión a través de conexiones cifradas puede hacer que una aplicación esté sujeta a ataques CRIME/BREACH. Si envía información confidencial, evite habilitar la compresión o use WebSocketMessageFlags.DisableCompression al llamar a WebSocket.SendAsync. Esta recomendación se aplica a ambos lados de WebSocket. Tenga en cuenta que la API de WebSockets en el explorador no tiene configuración para deshabilitar la compresión por envío.

Si se quiere la compresión de mensajes a través de WebSockets, el código de aceptación debe especificar que permite la compresión de la siguiente manera:

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

}

WebSocketAcceptContext.ServerMaxWindowBits y WebSocketAcceptContext.DisableServerContextTakeover son opciones avanzadas que controlan cómo funciona la compresión.

La compresión se negocia entre el cliente y el servidor al establecer por primera vez una conexión. Puede leer más sobre la negociación en Extensiones de compresión para WebSocket RFC.

Nota

Aunque el servidor o el cliente no acepten la negociación de compresión, la conexión se puede establecer. Sin embargo, la conexión no usa compresión al enviar y recibir mensajes.

Enviar y recibir mensajes

El método AcceptWebSocketAsync actualiza la conexión TCP a una conexión de WebSocket y proporciona un objeto WebSocket. Use el objeto WebSocket para enviar y recibir mensajes.

El código antes mostrado que acepta la solicitud WebSocket pasa el objeto WebSocket a un método Echo. El código recibe un mensaje y devuelve inmediatamente el mismo mensaje. Los mensajes se envían y reciben en un bucle hasta que el cliente cierra la conexión:

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

Cuando la conexión WebSocket se acepta antes de que el bucle comience, la canalización de middleware finaliza. Tras cerrar el socket, se desenreda la canalización. Es decir, la solicitud deja de avanzar en la canalización cuando WebSocket se acepta, pero cuando el bucle termina y el socket se cierra, la solicitud vuelve a recorrer la canalización.

Control de las desconexiones del cliente

No se informa automáticamente al servidor cuando el cliente se desconecta debido a la pérdida de conectividad. El servidor recibe un mensaje de desconexión solo si el cliente lo envía, acción que no se puede realizar si se pierde la conexión a Internet. Si desea realizar alguna acción cuando eso suceda, establezca un tiempo de expiración después de que no se reciba del cliente dentro de un determinado período.

Si el cliente no está siempre enviando mensajes y no quiere que se agote el tiempo de espera solo porque la conexión está inactiva, haga que el cliente utilice un temporizador para enviar un mensaje de ping cada X segundos. En el servidor, si aún no ha llegado un mensaje transcurridos 2*X segundos desde el anterior, termine la conexión e informe de que el cliente se ha desconectado. Espere el doble del intervalo de tiempo esperado para dejar tiempo extra para los retrasos de la red que podrían retener el mensaje de ping.

Restricción de los orígenes de WebSocket

Las protecciones proporcionadas por CORS no se aplican a WebSockets. Los exploradores no hacen lo siguiente:

  • Efectúan solicitudes preparatorias CORS.
  • Respetan las restricciones especificadas en los encabezados Access-Control al efectuar solicitudes de WebSocket.

En cambio, sí que envían el encabezado Origin al emitir solicitudes de WebSocket. Las aplicaciones deben configurarse para validar estos encabezados a fin de garantizar que solo se permitan los WebSockets procedentes de los orígenes esperados.

Si hospeda su servidor en "https://server.com"" y su cliente en "https://client.com"", agregue "https://client.com"" a la lista AllowedOrigins de WebSockets para comprobarlo.

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

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

app.UseWebSockets(webSocketOptions);

Nota

El encabezado Origin está controlado por el cliente y, al igual que el encabezado Referer, se puede falsificar. No use estos encabezados como mecanismo de autenticación.

Compatibilidad con IIS/IIS Express

El protocolo WebSocket se puede usar en Windows Server 2012 o versiones posteriores, y en Windows 8 versiones posteriores con IIS o IIS Express 8 versiones posteriores, pero no para WebSockets sobre HTTP/2.

Nota

WebSocket siempre está habilitado cuando se usa IIS Express.

Habilitación de WebSocket en IIS

Para habilitar la compatibilidad con el protocolo WebSocket en Windows Server 2012 o posterior:

Nota

Estos pasos no son necesarios cuando se usa IIS Express

  1. Use el asistente Agregar roles y características del menú Administrar o el vínculo de Administrador del servidor.
  2. Seleccione Instalación basada en características o en roles. Seleccione Siguiente.
  3. Seleccione el servidor que corresponda (el servidor local está seleccionado de forma predeterminada). Seleccione Siguiente.
  4. Expanda Servidor web (IIS) en el árbol Roles, expanda Servidor web y, por último, expanda Desarrollo de aplicaciones.
  5. Seleccione Protocolo WebSocket. Seleccione Siguiente.
  6. Si no necesita más características, haga clic en Siguiente.
  7. Haga clic en Instalar.
  8. Cuando la instalación finalice, haga clic en Cerrar para salir del asistente.

Para habilitar la compatibilidad con el protocolo WebSocket en Windows Server 8 o posterior:

Nota

Estos pasos no son necesarios cuando se usa IIS Express

  1. Vaya a Panel de control>Programas>Programas y características>Activar o desactivar las características de Windows (lado izquierdo de la pantalla).
  2. Abra los nodos siguientes: Internet Information Services>Servicios World Wide Web>Características de desarrollo de aplicaciones.
  3. Seleccione la característica Protocolo WebSocket. Seleccione Aceptar.

Deshabilitar WebSocket al usar socket.io en Node.js

Si usa la compatibilidad de WebSocket en socket.io en Node.js, deshabilite el módulo IIS WebSocket predeterminado, usando para ello el elemento webSocket de web.config o de applicationHost.config. Si este paso no se lleva a cabo, el módulo IIS WebSocket intenta controlar la comunicación de WebSocket en lugar de Node.js y la aplicación.

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

Aplicación de ejemplo

La aplicación de ejemplo que acompaña a este artículo es una aplicación de eco. Tiene una página web que realiza las conexiones de WebSocket y el servidor reenvía de vuelta al cliente todos los mensajes que recibe. La aplicación de ejemplo admite WebSockets sobre HTTP/2 cuando se usa un marco de destino con .NET 7 o versiones posteriores.

Ejecute la aplicación:

  • Para ejecutar la aplicación en Visual Studio: abra el proyecto de ejemplo en Visual Studio y presione Ctrl+F5 para ejecutarlo sin el depurador.
  • Para ejecutar la aplicación en un shell de comandos: ejecute el comando dotnet run y vaya a http://localhost:<port> en un explorador.

La página web muestra el estado de la conexión:

Initial state of webpage before WebSockets connection

Seleccione Connect (Conectar) para enviar una solicitud WebSocket para la URL mostrada. Escriba un mensaje de prueba y seleccione Send (Enviar). Cuando haya terminado, seleccione Close Socket (Cerrar socket). Los informes de la sección Communication Log (Registro de comunicación) informan de cada acción de abrir, enviar y cerrar a medida que se producen.

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

En este artículo se ofrece una introducción a WebSockets en ASP.NET Core. WebSocket (RFC 6455) es un protocolo que habilita canales de comunicación bidireccional persistentes a través de conexiones TCP. Se usa en aplicaciones que sacan partido de comunicaciones rápidas y en tiempo real, como las aplicaciones de chat, panel y juegos.

Vea o descargue el código de ejemplo (cómo descargarlo, cómo ejecutarlo).

SignalR

ASP.NET CoreSignalR es una biblioteca que simplifica la adición de funcionalidad web en tiempo real a las aplicaciones. Usa WebSockets siempre que sea posible.

Para la mayoría de las aplicaciones, se recomienda SignalR en lugar de WebSockets sin procesar. SignalR proporciona transporte de reserva para entornos donde WebSockets no está disponible. También proporciona un modelo básico de aplicación de llamada a procedimiento remoto. Además, en la mayoría de los escenarios, SignalR no tiene ninguna desventaja significativa de rendimiento en comparación con WebSockets sin procesar.

En algunas aplicaciones, gRPC en .NET proporciona una alternativa a WebSockets.

Requisitos previos

  • Cualquier sistema operativo que admita ASP.NET Core:
    • Windows 7/Windows Server 2008 o posterior
    • Linux
    • macOS
  • Si la aplicación se ejecuta en Windows con IIS:
  • Si la aplicación se ejecuta en HTTP.sys:
    • Windows 8/Windows Server 2012 o versiones posteriores
  • Para saber qué exploradores son compatibles, vea Can I use.

Configurar el middleware

Agregue el middleware de WebSockets en Program.cs:

app.UseWebSockets();

Se pueden configurar estas opciones:

  • KeepAliveInterval: la frecuencia con que se envían marcos "ping" al cliente, para asegurarse de que los servidores proxy mantienen abierta la conexión. El valor predeterminado es de dos minutos.
  • AllowedOrigins - Una lista de valores de encabezado de origen permitidos para las solicitudes WebSocket. De forma predeterminada, se permiten todos los orígenes. Para obtener más información, vea Restricción de los orígenes de WebSocket en este artículo.
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

app.UseWebSockets(webSocketOptions);

Aceptar solicitudes WebSocket

En algún momento posterior del ciclo de las solicitudes (más adelante en Program.cs o en un método de acción, por ejemplo) debe comprobar si se trata de una solicitud WebSocket y aceptarla.

El ejemplo siguiente es de un momento posterior en 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);
    }

});

Una solicitud WebSocket puede proceder de cualquier dirección URL, pero este código de ejemplo solo acepta solicitudes de /ws.

Se puede adoptar un enfoque similar en un método de controlador:

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

Cuando se usa un WebSocket, debe mantener la canalización de middleware en ejecución durante la duración de la conexión. Si intenta enviar o recibir un mensaje de WebSocket después de que finalice la canalización de middleware, es posible que obtenga una excepción como la siguiente:

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 utiliza un servicio en segundo plano para escribir datos en un WebSocket, asegúrese de mantener en ejecución el canal de middleware. Para ello, utilice un TaskCompletionSource<TResult>. Pase TaskCompletionSource a su servicio de segundo plano y pídale que llame a TrySetResult cuando termine con WebSocket. Después, espere (con await) la propiedad Task durante la solicitud, como se muestra en el ejemplo siguiente:

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

    BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

    await socketFinishedTcs.Task;
});

La excepción de cierre de WebSocket también puede producirse si la devolución de un método de acción ocurre demasiado pronto. Al aceptar un socket en un método de acción, espere a que finalice el código que usa el socket antes de devolver el método de acción.

No use nunca Task.Wait, Task.Result ni llamadas de bloqueo similares para esperar a que se complete el socket, ya que pueden causar graves problemas de subprocesamiento. Use siempre await.

Compresión

Advertencia

La habilitación de la compresión a través de conexiones cifradas puede hacer que una aplicación esté sujeta a ataques CRIME/BREACH. Si envía información confidencial, evite habilitar la compresión o use WebSocketMessageFlags.DisableCompression al llamar a WebSocket.SendAsync. Esta recomendación se aplica a ambos lados de WebSocket. Tenga en cuenta que la API de WebSockets en el explorador no tiene configuración para deshabilitar la compresión por envío.

Si se quiere la compresión de mensajes a través de WebSockets, el código de aceptación debe especificar que permite la compresión de la siguiente manera:

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

}

WebSocketAcceptContext.ServerMaxWindowBits y WebSocketAcceptContext.DisableServerContextTakeover son opciones avanzadas que controlan cómo funciona la compresión.

La compresión se negocia entre el cliente y el servidor al establecer por primera vez una conexión. Puede leer más sobre la negociación en Extensiones de compresión para WebSocket RFC.

Nota

Aunque el servidor o el cliente no acepten la negociación de compresión, la conexión se puede establecer. Sin embargo, la conexión no usa compresión al enviar y recibir mensajes.

Enviar y recibir mensajes

El método AcceptWebSocketAsync actualiza la conexión TCP a una conexión de WebSocket y proporciona un objeto WebSocket. Use el objeto WebSocket para enviar y recibir mensajes.

El código antes mostrado que acepta la solicitud WebSocket pasa el objeto WebSocket a un método Echo. El código recibe un mensaje y devuelve inmediatamente el mismo mensaje. Los mensajes se envían y reciben en un bucle hasta que el cliente cierra la conexión:

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

Cuando la conexión WebSocket se acepta antes de que el bucle comience, la canalización de middleware finaliza. Tras cerrar el socket, se desenreda la canalización. Es decir, la solicitud deja de avanzar en la canalización cuando WebSocket se acepta, pero cuando el bucle termina y el socket se cierra, la solicitud vuelve a recorrer la canalización.

Control de las desconexiones del cliente

No se informa automáticamente al servidor cuando el cliente se desconecta debido a la pérdida de conectividad. El servidor recibe un mensaje de desconexión solo si el cliente lo envía, acción que no se puede realizar si se pierde la conexión a Internet. Si desea realizar alguna acción cuando eso suceda, establezca un tiempo de expiración después de que no se reciba del cliente dentro de un determinado período.

Si el cliente no está siempre enviando mensajes y no quiere que se agote el tiempo de espera solo porque la conexión está inactiva, haga que el cliente utilice un temporizador para enviar un mensaje de ping cada X segundos. En el servidor, si aún no ha llegado un mensaje transcurridos 2*X segundos desde el anterior, termine la conexión e informe de que el cliente se ha desconectado. Espere el doble del intervalo de tiempo esperado para dejar tiempo extra para los retrasos de la red que podrían retener el mensaje de ping.

Restricción de los orígenes de WebSocket

Las protecciones proporcionadas por CORS no se aplican a WebSockets. Los exploradores no hacen lo siguiente:

  • Efectúan solicitudes preparatorias CORS.
  • Respetan las restricciones especificadas en los encabezados Access-Control al efectuar solicitudes de WebSocket.

En cambio, sí que envían el encabezado Origin al emitir solicitudes de WebSocket. Las aplicaciones deben configurarse para validar estos encabezados a fin de garantizar que solo se permitan los WebSockets procedentes de los orígenes esperados.

Si hospeda su servidor en "https://server.com"" y su cliente en "https://client.com"", agregue "https://client.com"" a la lista AllowedOrigins de WebSockets para comprobarlo.

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

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

app.UseWebSockets(webSocketOptions);

Nota

El encabezado Origin está controlado por el cliente y, al igual que el encabezado Referer, se puede falsificar. No use estos encabezados como mecanismo de autenticación.

Compatibilidad con IIS/IIS Express

El protocolo WebSocket se puede usar en Windows Server 2012 o posterior, y en Windows 8 o posterior con IIS o IIS Express 8 o posterior.

Nota

WebSocket siempre está habilitado cuando se usa IIS Express.

Habilitación de WebSocket en IIS

Para habilitar la compatibilidad con el protocolo WebSocket en Windows Server 2012 o posterior:

Nota

Estos pasos no son necesarios cuando se usa IIS Express

  1. Use el asistente Agregar roles y características del menú Administrar o el vínculo de Administrador del servidor.
  2. Seleccione Instalación basada en características o en roles. Seleccione Siguiente.
  3. Seleccione el servidor que corresponda (el servidor local está seleccionado de forma predeterminada). Seleccione Siguiente.
  4. Expanda Servidor web (IIS) en el árbol Roles, expanda Servidor web y, por último, expanda Desarrollo de aplicaciones.
  5. Seleccione Protocolo WebSocket. Seleccione Siguiente.
  6. Si no necesita más características, haga clic en Siguiente.
  7. Haga clic en Instalar.
  8. Cuando la instalación finalice, haga clic en Cerrar para salir del asistente.

Para habilitar la compatibilidad con el protocolo WebSocket en Windows Server 8 o posterior:

Nota

Estos pasos no son necesarios cuando se usa IIS Express

  1. Vaya a Panel de control>Programas>Programas y características>Activar o desactivar las características de Windows (lado izquierdo de la pantalla).
  2. Abra los nodos siguientes: Internet Information Services>Servicios World Wide Web>Características de desarrollo de aplicaciones.
  3. Seleccione la característica Protocolo WebSocket. Seleccione Aceptar.

Deshabilitar WebSocket al usar socket.io en Node.js

Si usa la compatibilidad de WebSocket en socket.io en Node.js, deshabilite el módulo IIS WebSocket predeterminado, usando para ello el elemento webSocket de web.config o de applicationHost.config. Si este paso no se lleva a cabo, el módulo IIS WebSocket intenta controlar la comunicación de WebSocket en lugar de Node.js y la aplicación.

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

Aplicación de ejemplo

La aplicación de ejemplo que acompaña a este artículo es una aplicación de eco. Tiene una página web que realiza las conexiones de WebSocket y el servidor reenvía de vuelta al cliente todos los mensajes que recibe. La aplicación de ejemplo no está configurada para ejecutarse desde Visual Studio con IIS Express, por lo que debe ejecutarla en un shell de comandos con dotnet run e ir a http://localhost:<port> en un explorador. La página web muestra el estado de la conexión:

Initial state of webpage before WebSockets connection

Seleccione Connect (Conectar) para enviar una solicitud WebSocket para la URL mostrada. Escriba un mensaje de prueba y seleccione Send (Enviar). Cuando haya terminado, seleccione Close Socket (Cerrar socket). Los informes de la sección Communication Log (Registro de comunicación) informan de cada acción de abrir, enviar y cerrar a medida que se producen.

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

En este artículo se ofrece una introducción a WebSockets en ASP.NET Core. WebSocket (RFC 6455) es un protocolo que habilita canales de comunicación bidireccional persistentes a través de conexiones TCP. Se usa en aplicaciones que sacan partido de comunicaciones rápidas y en tiempo real, como las aplicaciones de chat, panel y juegos.

Vea o descargue el código de ejemplo (cómo descargarlo). Cómo ejecutar.

SignalR

ASP.NET CoreSignalR es una biblioteca que simplifica la adición de funcionalidad web en tiempo real a las aplicaciones. Usa WebSockets siempre que sea posible.

Para la mayoría de las aplicaciones, se recomienda SignalR en lugar de WebSockets sin procesar. SignalR proporciona transporte de reserva para entornos donde WebSockets no está disponible. También proporciona un modelo básico de aplicación de llamada a procedimiento remoto. Además, en la mayoría de los escenarios, SignalR no tiene ninguna desventaja significativa de rendimiento en comparación con WebSockets sin procesar.

En algunas aplicaciones, gRPC en .NET proporciona una alternativa a WebSockets.

Requisitos previos

  • Cualquier sistema operativo que admita ASP.NET Core:
    • Windows 7/Windows Server 2008 o posterior
    • Linux
    • macOS
  • Si la aplicación se ejecuta en Windows con IIS:
  • Si la aplicación se ejecuta en HTTP.sys:
    • Windows 8/Windows Server 2012 o versiones posteriores
  • Para saber qué exploradores son compatibles, vea Can I use.

Configurar el middleware

Agregue el middleware de WebSockets al método Configure de la clase Startup:

app.UseWebSockets();

Nota

Si quiere aceptar las solicitudes de WebSocket en un controlador, la llamada a app.UseWebSockets debe producirse antes de app.UseEndpoints.

Se pueden configurar estas opciones:

  • KeepAliveInterval: la frecuencia con que se envían marcos "ping" al cliente, para asegurarse de que los servidores proxy mantienen abierta la conexión. El valor predeterminado es de dos minutos.
  • AllowedOrigins - Una lista de valores de encabezado de origen permitidos para las solicitudes WebSocket. De forma predeterminada, se permiten todos los orígenes. Consulte "Restricción de los orígenes de WebSocket" a continuación para obtener información detallada.
var webSocketOptions = new WebSocketOptions()
{
    KeepAliveInterval = TimeSpan.FromSeconds(120),
};

app.UseWebSockets(webSocketOptions);

Aceptar solicitudes WebSocket

En algún momento posterior del ciclo de solicitudes (más adelante en el método Configure o en un método de acción, por ejemplo) debe comprobar si se trata de una solicitud WebSocket y aceptarla.

El siguiente ejemplo se corresponde con un momento más adelante en el método 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();
    }

});

Una solicitud WebSocket puede proceder de cualquier dirección URL, pero este código de ejemplo solo acepta solicitudes de /ws.

Se puede adoptar un enfoque similar en un método de controlador:

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

Cuando se usa un WebSocket, debe mantener la canalización de middleware en ejecución durante la duración de la conexión. Si intenta enviar o recibir un mensaje de WebSocket después de que finalice la canalización de middleware, es posible que obtenga una excepción como la siguiente:

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 utiliza un servicio en segundo plano para escribir datos en un WebSocket, asegúrese de mantener en ejecución el canal de middleware. Para ello, utilice un TaskCompletionSource<TResult>. Pase TaskCompletionSource a su servicio de segundo plano y pídale que llame a TrySetResult cuando termine con WebSocket. Después, espere (con await) la propiedad Task durante la solicitud, como se muestra en el ejemplo siguiente:

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

        BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

        await socketFinishedTcs.Task;
    }
});

La excepción de cierre de WebSocket también puede producirse si la devolución de un método de acción ocurre demasiado pronto. Al aceptar un socket en un método de acción, espere a que finalice el código que usa el socket antes de devolver el método de acción.

No use nunca Task.Wait, Task.Result ni llamadas de bloqueo similares para esperar a que se complete el socket, ya que pueden causar graves problemas de subprocesamiento. Use siempre await.

Enviar y recibir mensajes

El método AcceptWebSocketAsync actualiza la conexión TCP a una conexión de WebSocket y proporciona un objeto WebSocket. Use el objeto WebSocket para enviar y recibir mensajes.

El código antes mostrado que acepta la solicitud WebSocket pasa el objeto WebSocket a un método Echo. El código recibe un mensaje y devuelve inmediatamente el mismo mensaje. Los mensajes se envían y reciben en un bucle hasta que el cliente cierra la conexión:

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

Cuando la conexión WebSocket se acepta antes de que el bucle comience, la canalización de middleware finaliza. Tras cerrar el socket, se desenreda la canalización. Es decir, la solicitud deja de avanzar en la canalización cuando WebSocket se acepta, pero cuando el bucle termina y el socket se cierra, la solicitud vuelve a recorrer la canalización.

Control de las desconexiones del cliente

No se informa automáticamente al servidor cuando el cliente se desconecta debido a la pérdida de conectividad. El servidor recibe un mensaje de desconexión solo si el cliente lo envía, acción que no se puede realizar si se pierde la conexión a Internet. Si desea realizar alguna acción cuando eso suceda, establezca un tiempo de expiración después de que no se reciba del cliente dentro de un determinado período.

Si el cliente no está siempre enviando mensajes y no quiere que se agote el tiempo de espera solo porque la conexión está inactiva, haga que el cliente utilice un temporizador para enviar un mensaje de ping cada X segundos. En el servidor, si aún no ha llegado un mensaje transcurridos 2*X segundos desde el anterior, termine la conexión e informe de que el cliente se ha desconectado. Espere el doble del intervalo de tiempo esperado para dejar tiempo extra para los retrasos de la red que podrían retener el mensaje de ping.

Nota:

El ManagedWebSocket interno controla los fotogramas Ping/Pong implícitamente para mantener activa la conexión si la opción KeepAliveInterval es mayor que cero, que tiene como valor predeterminado 30 segundos (TimeSpan.FromSeconds(30)).

Restricción de los orígenes de WebSocket

Las protecciones proporcionadas por CORS no se aplican a WebSockets. Los exploradores no hacen lo siguiente:

  • Efectúan solicitudes preparatorias CORS.
  • Respetan las restricciones especificadas en los encabezados Access-Control al efectuar solicitudes de WebSocket.

En cambio, sí que envían el encabezado Origin al emitir solicitudes de WebSocket. Las aplicaciones deben configurarse para validar estos encabezados a fin de garantizar que solo se permitan los WebSockets procedentes de los orígenes esperados.

Si hospeda su servidor en "https://server.com"" y su cliente en "https://client.com"", agregue "https://client.com"" a la lista AllowedOrigins de WebSockets para comprobarlo.

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

app.UseWebSockets(webSocketOptions);

Nota

El encabezado Origin está controlado por el cliente y, al igual que el encabezado Referer, se puede falsificar. No use estos encabezados como mecanismo de autenticación.

Compatibilidad con IIS/IIS Express

El protocolo WebSocket se puede usar en Windows Server 2012 o posterior, y en Windows 8 o posterior con IIS o IIS Express 8 o posterior.

Nota

WebSocket siempre está habilitado cuando se usa IIS Express.

Habilitación de WebSocket en IIS

Para habilitar la compatibilidad con el protocolo WebSocket en Windows Server 2012 o posterior:

Nota

Estos pasos no son necesarios cuando se usa IIS Express

  1. Use el asistente Agregar roles y características del menú Administrar o el vínculo de Administrador del servidor.
  2. Seleccione Instalación basada en características o en roles. Seleccione Siguiente.
  3. Seleccione el servidor que corresponda (el servidor local está seleccionado de forma predeterminada). Seleccione Siguiente.
  4. Expanda Servidor web (IIS) en el árbol Roles, expanda Servidor web y, por último, expanda Desarrollo de aplicaciones.
  5. Seleccione Protocolo WebSocket. Seleccione Siguiente.
  6. Si no necesita más características, haga clic en Siguiente.
  7. Haga clic en Instalar.
  8. Cuando la instalación finalice, haga clic en Cerrar para salir del asistente.

Para habilitar la compatibilidad con el protocolo WebSocket en Windows Server 8 o posterior:

Nota

Estos pasos no son necesarios cuando se usa IIS Express

  1. Vaya a Panel de control>Programas>Programas y características>Activar o desactivar las características de Windows (lado izquierdo de la pantalla).
  2. Abra los nodos siguientes: Internet Information Services>Servicios World Wide Web>Características de desarrollo de aplicaciones.
  3. Seleccione la característica Protocolo WebSocket. Seleccione Aceptar.

Deshabilitar WebSocket al usar socket.io en Node.js

Si usa la compatibilidad de WebSocket en socket.io en Node.js, deshabilite el módulo IIS WebSocket predeterminado, usando para ello el elemento webSocket de web.config o de applicationHost.config. Si este paso no se lleva a cabo, el módulo IIS WebSocket intenta controlar la comunicación de WebSocket en lugar de Node.js y la aplicación.

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

Aplicación de ejemplo

La aplicación de ejemplo que acompaña a este artículo es una aplicación de eco. Tiene una página web que realiza las conexiones de WebSocket y el servidor reenvía de vuelta al cliente todos los mensajes que recibe. La aplicación de ejemplo no está configurada para ejecutarse desde Visual Studio con IIS Express, por lo que debe ejecutarla en un shell de comandos con dotnet run e ir a http://localhost:5000 en un explorador. La página web muestra el estado de la conexión:

Initial state of webpage before WebSockets connection

Seleccione Connect (Conectar) para enviar una solicitud WebSocket para la URL mostrada. Escriba un mensaje de prueba y seleccione Send (Enviar). Cuando haya terminado, seleccione Close Socket (Cerrar socket). Los informes de la sección Communication Log (Registro de comunicación) informan de cada acción de abrir, enviar y cerrar a medida que se producen.

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