Protocol WebSocket, RFC 6455, provides a standardized way to establish a full-duplex two-way communication channel between a client and server over a single TCP connections. It is a different TCP protocol than HTTP, but is designed to run on top of HTTP, uses ports 80 and 443, and allows existing firewall rules to be reused.

WebSocket communication begins with an HTTP request that uses the HTTP header Upgrade to upgrade, or in this case, to migrate to the WebSocket protocol. The following example shows this interaction:


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. Heading Upgrade.
  2. Using Upgrade connection.

Instead of the usual 200 status code, the WebSocket-enabled server issues a message similar to the following:


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

After successful handshaking, the TCP socket underlying the HTTP refresh request remains open so that the client and server can continue to send and receive messages.

A full introduction to how WebSockets works is beyond the scope of this document. See "RFC 6455", the chapter on WebSocket in HTML5, or any of the many descriptions and tutorials on the Internet.

Note that if the WebSocket server is running behind a web server (eg nginx), then you will most likely need to configure it to send WebSocket update requests to the server. Likewise, if your application is running in a cloud environment, check your cloud provider's instructions regarding WebSocket support.

HTTP vs. WebSocket

Even though WebSocket is designed to be HTTP-compatible and comes from an HTTP request, it is important to understand that the two protocols entail completely different architectures and application programming models.

In HTTP and REST, an application is modeled as a set of URLs. To interact with the application, clients access these URLs in a request-response style. Servers route requests to the appropriate handler based on the URL, method, and HTTP headers.

In contrast, WebSockets typically uses only one URL for the initial connection. Subsequently, all application messages are transmitted over the same TCP connection. This points to a completely different asynchronous, event-driven messaging architecture.

WebSocket is also a low-level transport protocol that, unlike HTTP, does not impose any semantics on message content. This means that there is no way to route or process a message until the semantics of the message are agreed upon between the client and server.

Clients and servers on WebSocket can negotiate the use of a higher-level messaging protocol (such as STOMP) with using the Sec-WebSocket-Protocol header in the HTTP handshake request. In the absence of this, they need to come up with their own conventions.

When should you use WebSockets?

WebSockets can make a web page dynamic and interactive. However, in many cases, a combination of Ajax and an HTTP stream or long-form polling can be a simple and effective solution.

For example, news, mail and social feeds should be updated dynamically, but it is quite acceptable to do this every few times minutes. On the other hand, collaboration apps, games, and financial apps need to run in real time more often.

Latency itself is not the deciding factor. If the volume of messages is relatively small (for example, when monitoring network failures), streaming or polling over the HTTP protocol can be an effective solution. It is the combination of low latency, high frequency and high volume that is the best argument for using WebSocket.

Remember also that on the Internet, restrictive proxies that are beyond your control may prevent WebSocket communication, either because they are not configured to send the Upgrade header, either because they close long-lived connections that appear to be inactive. This means that using WebSocket for internal applications within a firewall is a simpler solution than for public applications.

WebSocket API

Spring Framework provides an API for the WebSocket protocol that can be used to writing client and server applications that process WebSocket messages.

Server

To create a WebSocket server, you can first create a WebSocketHandler. The following example shows how to do this:

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> {
// ...
}
}

You can then map it to a 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)
}
}

If you are using the WebFlux configuration then you will not need to do anything else, otherwise if you are not using the WebFlux configuration you will need to declare a WebSocketHandlerAdapter as shown below:

Java

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

WebSocketHandler

The handle method in WebSocketHandler accepts WebSocketSession and returns Mono<Void> to indicate that the application has completed processing the session. The session is processed through two threads, one for incoming and one for outgoing messages. The following table describes two methods that work with threads:

Method WebSocketSession Description

Flux<WebSocketMessage> receive()

Gives access to the incoming message stream and exits when the connection is closed.

Mono<Void> send(Publisher<WebSocketMessage>)

WebSocketHandler should combine the incoming and outgoing streams into a single stream and return Mono<Void> ;, which indicates the completion of this thread. Depending on the application's requirements, a single thread terminates when:

Accepts an outgoing message source, writes the messages, and returns Mono<Void>, which terminates when the source stops running and the recording is finished.

  • Either the incoming or outgoing message flow ends.

  • The incoming stream terminates (that is, the connection is closed), and the outgoing stream is infinite.

  • At the selected moment, through the close method for WebSocketSession.

If the incoming and outgoing message streams are combined together, there is no need to check whether the connection is open because Reactive Streams signal the end of the activity. The incoming thread receives a completion or error signal, and the outgoing thread receives a cancellation signal.

The most basic handler implementation is the one that handles the incoming thread. The following example shows such an implementation:

Java

class ExampleHandler implements WebSocketHandler {
@Override
public Mono<Void> handle(WebSocketSession session) {
return session.receive()            
        .doOnNext(message -> {
            // ...                  (2)
        })
        .concatMap(message -> {
            // ...                  (3)
        })
        .then();                    
}
  1. Accessing the stream incoming messages.
  2. We perform some actions on each message.
  3. We perform nested asynchronous operations that use the contents of the message.
  4. Return Mono<Void>, which terminates when "completes" is received.
Kotlin

class ExampleHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
return session.receive()            
        .doOnNext {
            // ...                  (2)
        }
        .concatMap {
            // ...                  (3)
        }
        .then()                     
}
}
        
  1. We gain access to the flow of incoming messages.
  2. We carry out some actions on each message.
  3. Perform nested asynchronous operations that use the contents of the message.
  4. Return Mono<Void>, which completes when "completes" is received.
Nested asynchronous operations may require calling message.retain() on core servers that use pooled buffers data (eg Netty). Otherwise, the data buffer may be freed before the data is read.

The following implementation combines the incoming and outgoing streams:

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. Processing the flow of incoming messages .
  2. Create an outgoing message by producing a merged stream.
  3. Return a Mono<Void>, which will not complete while we continue to receive data.
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. Process the flow of incoming messages.
  2. Create an outgoing message by producing a merged flow.
  3. Return Mono<Void>, which will not be completed while we continue to receive data.

Incoming and outgoing streams can be independent and can only be combined to complete, as shown in the following example:

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. Processing the flow of incoming messages.
  2. Send outgoing messages.
  3. Combine threads and return Mono<Void>, which terminates if any of the threads terminate.
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. Process the flow of incoming messages.
  2. Send outgoing messages.
  3. Combine threads and return Mono<Void>, which terminates if any of the threads terminate.

DataBuffer

DataBuffer is a representation for a byte buffer in WebFlux. It is important to understand that on some servers, such as Netty, byte buffers are pooled and reference-counted, and must be deallocated when consumed to avoid memory leaks.

When running on Netty, applications need to use DataBufferUtils.retain(dataBuffer) if you want to hold the input data buffers without releasing them, and then use DataBufferUtils.release(dataBuffer) when the data from the buffers is consumed.

Handshake

WebSocketHandlerAdapter delegates authority to WebSocketService. By default, this is a HandshakeWebSocketService instance that does basic validation of the WebSocket request and then uses RequestUpgradeStrategy for the server being used. Currently there is native support for Reactor Netty, Tomcat, Jetty and Undertow.

HandshakeWebSocketService exposes the sessionAttributePredicate property, which allows you to set Predicate<String> to extract attributes from WebSession and insert them into WebSocketSession attributes.

Server configuration

RequestUpgradeStrategyfor each server opens a configuration specific to the underlying WebSocket server mechanism. When using the WebFlux Java configuration, you can configure the following properties, or if you are not using the WebFlux configuration, you can use the properties below:

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

See the upgrade strategy for your server to see your options available. Currently, only Tomcat and Jetty provide such options.

CORS

The easiest way to configure CORS and limit access to the WebSocket endpoint is to force your WebSocketHandler implement CorsConfigurationSource and return CorsConfiguration using valid sources, headers, and other information. If this is not possible, you can also set the corsConfigurations property on the SimpleUrlHandler to set the CORS settings by URL pattern. If both are specified, they are combined using the combine method for CorsConfiguration.

Client

Spring WebFlux provides the WebSocketClient abstraction with implementations for Reactor Netty, Tomcat, Jetty, Undertow and standard Java (i.e. JSR-356).

The Tomcat client is actually an extension of the standard Java client with some additional functionality around WebSocketSession handling, allowing you to use a Tomcat-specific API to pause receiving messages to provide feedback.

To start a WebSocket session , you can instantiate the client and use its execute methods:

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

Some clients, such as Jetty, implement Lifecycle and must be stopped and started before they can be used. All clients have constructor parameters related to the configuration of the underlying WebSocket client.