Protocolo WebSocket, RFC 6455 proporciona una forma estandarizada de establecer un canal de comunicación bidireccional full-duplex entre un cliente. y servidor a través de una única conexión TCP. Es un protocolo TCP diferente a HTTP, pero está diseñado para ejecutarse sobre HTTP, utiliza los puertos 80 y 443 y permite reutilizar las reglas de firewall existentes.

La comunicación WebSocket comienza con una solicitud HTTP que utiliza el encabezado HTTP Upgrade para actualizar o, en este caso, para migrar al protocolo WebSocket. El siguiente ejemplo muestra esta interacción:


GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket 
Connection: Upgrade 
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
  1. Título Upgrade.
  2. Usando la conexión Upgrade.

En lugar del código de estado 200 habitual, el servidor habilitado para WebSocket emite un mensaje similar al siguiente:


HTTP/1.1 101 Switching Protocols 
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
        
  1. Cambio de protocolo

Después de un protocolo de enlace exitoso, el socket TCP subyacente a la solicitud de actualización HTTP permanece abierto para que el cliente y el servidor puedan continuar enviando y recibiendo mensajes.

Una introducción completa a cómo funciona WebSockets está fuera del alcance de este documento. Consulte "RFC 6455", el capítulo sobre WebSocket en HTML5, o cualquiera de las muchas descripciones y tutoriales en Internet.

Tenga en cuenta que si el servidor WebSocket se ejecuta detrás de un servidor web (por ejemplo, nginx), entonces lo más probable es que necesites configurarlo para enviar solicitudes de actualización de WebSocket al servidor. Del mismo modo, si su aplicación se ejecuta en un entorno de nube, consulte las instrucciones de su proveedor de nube con respecto a la compatibilidad con WebSocket.

HTTP frente a WebSocket

Aunque WebSocket está diseñado para ser compatible con HTTP y proviene de una solicitud HTTP, es importante comprender que los dos protocolos implican arquitecturas y modelos de programación de aplicaciones completamente diferentes.

En HTTP y REST, una aplicación se modela como un conjunto de URL. Para interactuar con la aplicación, los clientes acceden a estas URL en un estilo de solicitud-respuesta. Los servidores enrutan las solicitudes al controlador apropiado según la URL, el método y los encabezados HTTP.

Por el contrario, WebSockets normalmente usa solo una URL para la conexión inicial. Posteriormente, todos los mensajes de la aplicación se transmiten a través de la misma conexión TCP. Esto apunta a una arquitectura de mensajería asincrónica basada en eventos completamente diferente.

WebSocket también es un protocolo de transporte de bajo nivel que, a diferencia de HTTP, no impone ninguna semántica al contenido del mensaje. Esto significa que no hay forma de enrutar o procesar un mensaje hasta que la semántica del mensaje sea acordada entre el cliente y el servidor.

Los clientes y servidores en WebSocket pueden negociar el uso de una mensajería de nivel superior. protocolo (como STOMP) con el uso del encabezado Sec-WebSocket-Protocol en la solicitud de protocolo de enlace HTTP. En ausencia de esto, necesitan crear sus propias convenciones.

¿Cuándo debería usar WebSockets?

WebSockets puede hacer que una página web sea dinámica e interactiva. Sin embargo, en muchos casos, una combinación de Ajax y un flujo HTTP o un sondeo de formato largo puede ser una solución simple y efectiva.

Por ejemplo, las noticias, el correo y las redes sociales deben actualizarse dinámicamente, pero no Es bastante aceptable hacer esto cada pocos minutos. Por otro lado, las aplicaciones de colaboración, los juegos y las aplicaciones financieras deben ejecutarse en tiempo real con más frecuencia.

La latencia en sí no es el factor decisivo. Si el volumen de mensajes es relativamente pequeño (por ejemplo, al monitorear fallas de la red), la transmisión por secuencias o el sondeo a través del protocolo HTTP pueden ser una solución eficaz. Es la combinación de baja latencia, alta frecuencia y alto volumen el mejor argumento para usar WebSocket.

Recuerde también que en Internet, los servidores proxy restrictivos que están fuera de su control pueden impedir la comunicación con WebSocket, ya sea porque no están configurados para enviar el encabezado Upgrade, ya sea porque cierran conexiones de larga duración que parecen estar inactivas. Esto significa que usar WebSocket para aplicaciones internas dentro de un firewall es una solución más simple que para aplicaciones públicas.

API de WebSocket

Spring Framework proporciona una API para el protocolo WebSocket que se puede usar para escribiendo aplicaciones de cliente y servidor que procesan mensajes WebSocket.

Servidor

Para crear un servidor WebSocket, primero puede crear un WebSocketHandler. El siguiente ejemplo muestra cómo hacer esto:

Java

import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.WebSocketSession;
public class MyWebSocketHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
// ...
}
}
Kotlin

import org.springframework.web.reactive.socket.WebSocketHandler
import org.springframework.web.reactive.socket.WebSocketSession
class MyWebSocketHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
// ...
}
}

Luego puedes asignarlo a una URL:

Java

@Configuration
class WebConfig {
@Bean
public HandlerMapping handlerMapping() {
Map<String, WebSocketHandler> map = new HashMap<>();
map.put("/path", new MyWebSocketHandler());
int order = -1; // before annotated controllers
return new SimpleUrlHandlerMapping(map, order);
}
}
Kotlin

@Configuration
class WebConfig {
@Bean
fun handlerMapping(): HandlerMapping {
val map = mapOf("/path" to MyWebSocketHandler())
val order = -1 // before annotated controllers
return SimpleUrlHandlerMapping(map, order)
}
}

Si está utilizando la configuración de WebFlux, no necesitará hacer nada más; de lo contrario, si no está utilizando la configuración de WebFlux, deberá declarar un WebSocketHandlerAdapter como se muestra a continuación:

Java

@Configuration
class WebConfig {
// ...
@Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter();
}
}
Kotlin

@Configuration
class WebConfig {
// ...
@Bean
fun handlerAdapter() =  WebSocketHandlerAdapter()
}

WebSocketHandler

El método handle en WebSocketHandler acepta WebSocketSession y devuelve Mono<Void> para indicar que la aplicación ha completado el procesamiento de la sesión. La sesión se procesa a través de dos hilos, uno para mensajes entrantes y otro para mensajes salientes. La siguiente tabla describe dos métodos que funcionan con transmisiones:

Método WebSocketSession Descripción

Flujo<WebSocketMessage> recibir()

Otorga acceso al flujo de mensajes entrantes y sale cuando se cierra la conexión.

Mono<Void> send(Publisher<WebSocketMessage>)

WebSocketHandler debe combinar los flujos entrantes y salientes en un solo flujo y devolver Mono<Void>, que indica la finalización de este hilo. Dependiendo de los requisitos de la aplicación, un único hilo termina cuando:

Acepta una fuente de mensaje saliente, escribe los mensajes y devuelve Mono<Void>, que finaliza cuando la fuente deja de ejecutarse y finaliza la grabación.

  • Finaliza el flujo de mensajes entrantes o salientes.

  • El flujo entrante termina (es decir, la conexión se cierra) y el flujo saliente es infinito.

  • En el momento seleccionado, a través del Método close para WebSocketSession.

Si los flujos de mensajes entrantes y salientes se combinan, no es necesario compruebe si la conexión está abierta porque Reactive Streams señala el final de la actividad. El hilo entrante recibe una señal de finalización o error, y el hilo saliente recibe una señal de cancelación.

La implementación del controlador más básica es la que maneja el hilo entrante. El siguiente ejemplo muestra dicha implementación:

Java

class ExampleHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
return session.receive()            
        .doOnNext(message -> {
            // ...                  (2)
        })
        .concatMap(message -> {
            // ...                  (3)
        })
        .then();                    
}
        
  1. Accediendo a la transmisión entrante mensajes.
  2. Realizamos algunas acciones en cada mensaje.
  3. Realizamos operaciones asincrónicas anidadas que utilizan el contenido del mensaje.
  4. Devolver Mono< Void>, que termina cuando se recibe "completa".
Kotlin
class ExampleHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
return session.receive()            
        .doOnNext {
            // ...                  (2)
        }
        .concatMap {
            // ...                  (3)
        }
        .then()                     
}
}
  1. Obtenemos acceso al flujo de mensajes entrantes.
  2. Realizamos algunas acciones sobre cada mensaje.
  3. Realiza operaciones asincrónicas anidadas que utilizan el contenido del mensaje.
  4. Devuelve Mono<Void>, que se completa cuando se recibe "completa".
Las operaciones asincrónicas anidadas pueden requerir llamar a message.retain() en el núcleo servidores que utilizan datos de buffers agrupados (por ejemplo, Netty). De lo contrario, el búfer de datos puede liberarse antes de que se lean los datos.

La siguiente implementación combina los flujos entrantes y salientes:

Java
class ExampleHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
Flux<WebSocketMessage> output = session.receive()               
        .doOnNext(message -> {
            // ...
        })
        .concatMap(message -> {
            // ...
        })
        .map(value -> session.textMessage("Echo " + value));    
return session.send(output);                                    
}
}
  1. Procesando el flujo de mensajes entrantes.
  2. Crea un mensaje saliente produciendo una secuencia fusionada.
  3. Devuelve un Mono<Void>, que no se completará mientras sigamos recibiendo datos.
Kotlin
class ExampleHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
val output = session.receive()                     
        .doOnNext {
            // ...
        }
        .concatMap {
            // ...
        }
        .map { session.textMessage("Echo $it") }    
return session.send(output)                         
}
} 
  1. Procesar el flujo de mensajes entrantes.
  2. Crear un mensaje saliente produciendo un flujo combinado.
  3. Devuelve Mono<Void>, que no se completará mientras sigamos recibiendo datos.

Las transmisiones entrantes y salientes pueden ser independiente y solo se puede combinar para completar, como se muestra en el siguiente ejemplo:

Java
class ExampleHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
Mono<Void> input = session.receive()                                 
        .doOnNext(message -> {
            // ...
        })
        .concatMap(message -> {
            // ...
        })
        .then();
Flux<String> source = ... ;
Mono<Void> output = session.send(source.map(session::textMessage));   
return Mono.zip(input, output).then();                                
}
}
  1. Procesando el flujo de mensajes entrantes.
  2. Enviar mensajes salientes.
  3. Combina hilos y devuelve Mono<Void>, que finaliza si alguno de los hilos termina.
Kotlin
class ExampleHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
val input = session.receive()                                  
        .doOnNext {
            // ...
        }
        .concatMap {
            // ...
        }
        .then()
val source: Flux<String> = ...
val output = session.send(source.map(session::textMessage))     
return Mono.zip(input, output).then()                           
}
}
  1. Procesar el flujo de mensajes entrantes.
  2. Enviar mensajes salientes.
  3. Combina subprocesos y devuelve Mono<Void>, que termina si alguno de los subprocesos termina.

DataBuffer

DataBuffer es una representación de un búfer de bytes en WebFlux. Es importante comprender que en algunos servidores, como Netty, los buffers de bytes se agrupan y cuentan por referencia, y deben desasignarse cuando se consumen para evitar pérdidas de memoria.

Cuando se ejecutan en Netty, las aplicaciones deben usar DataBufferUtils.retain(dataBuffer) si desea conservar los búferes de datos de entrada sin liberarlos y luego usar DataBufferUtils.release(dataBuffer) cuando se consuman los datos de los búferes .

Handshake

WebSocketHandlerAdapter delega autoridad a WebSocketService. De forma predeterminada, esta es una instancia HandshakeWebSocketService que realiza una validación básica de la solicitud WebSocket y luego usa RequestUpgradeStrategy para el servidor que se está utilizando. Actualmente existe soporte nativo para Reactor Netty, Tomcat, Jetty y Undertow.

HandshakeWebSocketService expone la propiedad sessionAttributePredicate, que le permite configurar Predicate<String> para extraer atributos de WebSession e insertarlos en los atributos de WebSocketSession.

Configuración del servidor

RequestUpgradeStrategypara cada servidor abre una configuración específica para el mecanismo del servidor WebSocket subyacente. Al usar la configuración de WebFlux Java, puede configurar las siguientes propiedades, o si no está usando la configuración de WebFlux, puede usar las siguientes propiedades:

Java

@Configuration
class WebConfig {
@Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter(webSocketService());
}
@Bean
public WebSocketService webSocketService() {
TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
strategy.setMaxSessionIdleTimeout(0L);
return new HandshakeWebSocketService(strategy);
}
}
Kotlin

@Configuration
class WebConfig {
@Bean
fun handlerAdapter() =
    WebSocketHandlerAdapter(webSocketService())
@Bean
fun webSocketService(): WebSocketService {
val strategy = TomcatRequestUpgradeStrategy().apply {
    setMaxSessionIdleTimeout(0L)
}
return HandshakeWebSocketService(strategy)
}
}

Consulte la estrategia de actualización de su servidor para ver sus opciones disponible. Actualmente, solo Tomcat y Jetty ofrecen este tipo de opciones.

CORS

La forma más sencilla de configurar CORS y limitar el acceso al punto final WebSocket es forzar su WebSocketHandler implemente CorsConfigurationSource y devuelva CorsConfiguration utilizando fuentes válidas, encabezados y otra información. Si esto no es posible, también puede configurar la propiedad corsConfigurations en SimpleUrlHandler para establecer la configuración de CORS por patrón de URL. Si se especifican ambos, se combinan usando el método combine para CorsConfiguration.

Client

Spring WebFlux proporciona el Abstracción de WebSocketClient con implementaciones para Reactor Netty, Tomcat, Jetty, Undertow y Java estándar (es decir, JSR-356).

El Tomcat client es en realidad una extensión del cliente Java estándar con algunas funciones adicionales relacionadas con el manejo de WebSocketSession, lo que le permite utilizar una API específica de Tomcat para pausar la recepción de mensajes y proporcionar comentarios.

Para Al iniciar una sesión de WebSocket, puede crear una instancia del cliente y utilizar sus métodos execute:

Java
WebSocketClient client = new ReactorNettyWebSocketClient();
URI url = new URI("ws://localhost:8080/path");
client.execute(url, session ->
session.receive()
        .doOnNext(System.out::println)
        .then());
Kotlin
val client = ReactorNettyWebSocketClient()
val url = URI("ws://localhost:8080/path")
client.execute(url) { session ->
    session.receive()
            .doOnNext(::println)
    .then()
}

Algunos clientes, como Jetty, implementan Lifecycle y deben detenerse e iniciarse antes de poder usarse. Todos los clientes tienen parámetros de constructor relacionados con la configuración del cliente WebSocket subyacente.