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
- Título
Upgrade
. - 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
- 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:
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) {
// ...
}
}
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:
@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);
}
}
@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:
@Configuration
class WebConfig {
// ...
@Bean
public WebSocketHandlerAdapter handlerAdapter() {
return new WebSocketHandlerAdapter();
}
}
@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 |
---|---|
|
Otorga acceso al flujo de mensajes entrantes y sale cuando se cierra la conexión. |
|
|
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
paraWebSocketSession
.
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:
class ExampleHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
return session.receive()
.doOnNext(message -> {
// ... (2)
})
.concatMap(message -> {
// ... (3)
})
.then();
}
- Accediendo a la transmisión entrante mensajes.
- Realizamos algunas acciones en cada mensaje.
- Realizamos operaciones asincrónicas anidadas que utilizan el contenido del mensaje.
- Devolver
Mono< Void>
, que termina cuando se recibe "completa".
class ExampleHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
return session.receive()
.doOnNext {
// ... (2)
}
.concatMap {
// ... (3)
}
.then()
}
}
- Obtenemos acceso al flujo de mensajes entrantes.
- Realizamos algunas acciones sobre cada mensaje.
- Realiza operaciones asincrónicas anidadas que utilizan el contenido del mensaje.
- Devuelve
Mono<Void>
, que se completa cuando se recibe "completa".
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:
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);
}
}
- Procesando el flujo de mensajes entrantes.
- Crea un mensaje saliente produciendo una secuencia fusionada.
- Devuelve un
Mono<Void>
, que no se completará mientras sigamos recibiendo datos.
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)
}
}
- Procesar el flujo de mensajes entrantes.
- Crear un mensaje saliente produciendo un flujo combinado.
- 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:
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();
}
}
- Procesando el flujo de mensajes entrantes.
- Enviar mensajes salientes.
- Combina hilos y devuelve
Mono<Void>
, que finaliza si alguno de los hilos termina.
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()
}
}
- Procesar el flujo de mensajes entrantes.
- Enviar mensajes salientes.
- 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
RequestUpgradeStrategy
para 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:
@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);
}
}
@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).
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
:
WebSocketClient client = new ReactorNettyWebSocketClient();
URI url = new URI("ws://localhost:8080/path");
client.execute(url, session ->
session.receive()
.doOnNext(System.out::println)
.then());
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.