«1. Обзор

В этой статье мы собираемся создать быстрый пример, используя новый Spring 5 WebSockets API вместе с реактивными функциями, предоставляемыми Spring WebFlux.

WebSocket — это широко известный протокол, обеспечивающий полнодуплексную связь между клиентом и сервером, обычно используемый в веб-приложениях, где клиент и сервер должны обмениваться событиями с высокой частотой и с малой задержкой.

Spring Framework 5 модернизировал поддержку WebSockets в рамках, добавив реактивные возможности в этот канал связи.

Мы можем найти больше о Spring WebFlux здесь.

2. Зависимости Maven

Мы собираемся использовать зависимости spring-boot-starters для spring-boot-integration и spring-boot-starter-webflux, которые в настоящее время доступны в репозитории Spring Milestone.

В этом примере мы используем последнюю доступную версию 2.0.0.M7, но всегда следует получать последнюю версию, доступную в репозитории Maven:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

3. Конфигурация WebSocket в Spring ~~ ~ Наша конфигурация довольно проста: мы внедрим WebSocketHandler для обработки сеанса сокета в нашем приложении Spring WebSocket.

Кроме того, давайте создадим аннотированный bean-компонентом метод HandlerMapping, который будет отвечать за сопоставление между запросами и объектами обработчика:

@Autowired
private WebSocketHandler webSocketHandler;

URL-адрес, к которому мы можем подключиться, будет следующим: ws://localhost: \u003cпорт\u003e/event-emitter.

@Bean
public HandlerMapping webSocketHandlerMapping() {
    Map<String, WebSocketHandler> map = new HashMap<>();
    map.put("/event-emitter", webSocketHandler);

    SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
    handlerMapping.setOrder(1);
    handlerMapping.setUrlMap(map);
    return handlerMapping;
}

4. Обработка сообщений WebSocket в Spring

Наш класс ReactiveWebSocketHandler будет отвечать за управление сеансом WebSocket на стороне сервера.

Он реализует интерфейс WebSocketHandler, поэтому мы можем переопределить метод handle, который будет использоваться для отправки сообщения клиенту WebSocket:

5. Создание простого реактивного клиента WebSocket

@Component
public class ReactiveWebSocketHandler implements WebSocketHandler {
    
    // private fields ...

    @Override
    public Mono<Void> handle(WebSocketSession webSocketSession) {
        return webSocketSession.send(intervalFlux
          .map(webSocketSession::textMessage))
          .and(webSocketSession.receive()
            .map(WebSocketMessage::getPayloadAsText)
            .log());
    }
}

Давайте теперь создадим Клиент Spring Reactive WebSocket, который сможет подключаться и обмениваться информацией с нашим сервером WebSocket.

5.1. Зависимость Maven

Во-первых, зависимости Maven.

Здесь мы используем тот же spring-boot-starter-webflux, который использовался ранее для настройки нашего реактивного серверного приложения WebSocket.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

5.2. Клиент WebSocket

Теперь давайте создадим класс ReactiveClientWebSocket, отвечающий за запуск связи с сервером:

В приведенном выше коде мы видим, что мы используем ReactorNettyWebSocketClient, который является реализацией WebSocketClient для использовать с Reactor Netty.

public class ReactiveJavaClientWebSocket {
 
    public static void main(String[] args) throws InterruptedException {
 
        WebSocketClient client = new ReactorNettyWebSocketClient();
        client.execute(
          URI.create("ws://localhost:8080/event-emitter"), 
          session -> session.send(
            Mono.just(session.textMessage("event-spring-reactive-client-websocket")))
            .thenMany(session.receive()
              .map(WebSocketMessage::getPayloadAsText)
              .log())
            .then())
            .block(Duration.ofSeconds(10L));
    }
}

Кроме того, клиент подключается к серверу WebSocket через URL-адрес ws://localhost:8080/event-emitter, устанавливая сеанс сразу после подключения к серверу.

Мы также можем видеть, что мы отправляем сообщение на сервер («event-spring-reactive-client-websocket») вместе с запросом на подключение.

Кроме того, метод send вызывается, ожидая в качестве параметра переменную типа Publisher\u003cT\u003e, в нашем случае наш Publisher\u003cT\u003e — это Mono\u003cT\u003e, а T — это простая строка “event-me-. from-reactive-java-client-websocket».

Более того, вызывается метод thenMany(…), ожидающий Flux типа String. Метод Receive() получает поток входящих сообщений, которые позже преобразуются в строки.

Наконец, метод block() заставляет клиента отключиться от сервера по истечении заданного времени (10 секунд в нашем примере).

5.3. Запуск клиента

Чтобы запустить его, убедитесь, что Reactive WebSocket Server запущен и работает. Затем запустите класс ReactiveJavaClientWebSocket, и мы увидим в журнале sysout генерируемые события:

Мы также можем увидеть в журнале нашего сервера Reactive WebSocket сообщение, отправленное клиентом во время попытки подключения: ~ ~~

[reactor-http-nio-4] INFO reactor.Flux.Map.1 - 
onNext({"eventId":"6042b94f-fd02-47a1-911d-dacf97f12ba6",
"eventDt":"2018-01-11T23:29:26.900"})

Также мы можем увидеть сообщение об окончании соединения после того, как клиент закончил свои запросы (в нашем случае, через 10 секунд):

[reactor-http-nio-2] reactor.Flux.Map.1: 
onNext(event-me-from-reactive-java-client)

6. Создание браузерного клиента WebSocket

[reactor-http-nio-2] reactor.Flux.Map.1: onComplete()

Создадим простой клиент HTML/Javascript WebSocket для использования нашего реактивного серверного приложения WebSocket.

«

<div class="events"></div>
<script>
    var clientWebSocket = new WebSocket("ws://localhost:8080/event-emitter");
    clientWebSocket.onopen = function() {
        console.log("clientWebSocket.onopen", clientWebSocket);
        console.log("clientWebSocket.readyState", "websocketstatus");
        clientWebSocket.send("event-me-from-browser");
    }
    clientWebSocket.onclose = function(error) {
        console.log("clientWebSocket.onclose", clientWebSocket, error);
        events("Closing connection");
    }
    clientWebSocket.onerror = function(error) {
        console.log("clientWebSocket.onerror", clientWebSocket, error);
        events("An error occured");
    }
    clientWebSocket.onmessage = function(error) {
        console.log("clientWebSocket.onmessage", clientWebSocket, error);
        events(error.data);
    }
    function events(responseEvent) {
        document.querySelector(".events").innerHTML += responseEvent + "<br>";
    }
</script>

«При запущенном сервере WebSocket при открытии этого HTML-файла в браузере (например, Chrome, Internet Explorer, Mozilla Firefox и т. д.) мы должны увидеть события, распечатываемые на экране, с задержкой в ​​1 секунду для каждого события, как определено в наш сервер WebSocket.

{"eventId":"c25975de-6775-4b0b-b974-b396847878e6","eventDt":"2018-01-11T23:56:09.780"}
{"eventId":"ac74170b-1f71-49d3-8737-b3f9a8a352f9","eventDt":"2018-01-11T23:56:09.781"}
{"eventId":"40d8f305-f252-4c14-86d7-ed134d3e10c6","eventDt":"2018-01-11T23:56:09.782"}

7. Заключение

Здесь мы представили пример того, как создать связь WebSocket между сервером и клиентом с помощью Spring 5 Framework, реализуя новые реактивные функции, предоставляемые Spring Webflux.

Как всегда, полный пример можно найти в нашем репозитории GitHub.