Spring Framework предоставляет API для протокола WebSocket, который можно использовать для написания клиентских и серверных приложений, обрабатывающих сообщения WebSocket.

WebSocketHandler

Создать сервер WebSocket так же просто, как реализовать WebSocketHandler или, что более вероятно, расширить TextWebSocketHandler или BinaryWebSocketHandler. В следующем примере используется TextWebSocketHandler:

import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;
public class MyHandler extends TextWebSocketHandler {
    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message) {
        // ...
    }
}

Существует специальная Java-конфигурация WebSocket и поддержка пространства имен XML для отображения предыдущего обработчика WebSocket на определенный URL-адрес, как показано в следующем примере:

import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/myHandler");
    }
    @Bean
    public WebSocketHandler myHandler() {
        return new MyHandler();
    }
}

В следующем примере показан XML-эквивалент конфигурации из предыдущего примера:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">
    <websocket:handlers>
        <websocket:mapping path="/myHandler" handler="myHandler"/>
    </websocket:handlers>
    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>

Предыдущий пример предназначен для использования в приложениях Spring MVC и должен быть включен в конфигурацию DispatcherServlet. Однако поддержка WebSocket в Spring не зависит от Spring MVC. С помощью WebSocketHandler относительно просто интегрировать WebSocketHandler в другие среды, работающие с HTTP, с помощью WebSocketHttpRequestHandler.

При использовании API WebSocketHandler напрямую или косвенно, например, через обмен сообщениями STOMP, приложение должно синхронизировать отправку сообщений, поскольку лежащая в основе стандартная сессия WebSocket (JSR-356) не допускает одновременной отправки. Один из вариантов – обернуть WebSocketSession с помощью ConcurrentWebSocketSessionDecorator.

Подтверждения установления связи по протоколу WebSocket

Самый простой способ настроить начальный HTTP-запрос подтверждения установления связи для WebSocket – это HandshakeInterceptor, который открывает методы "перед" и "после" подтверждения установления связи. Можно использовать такой перехватчик, чтобы исключить подтверждение установления связи или открыть доступ к каким-либо атрибутам для WebSocketSession. В следующем примере используется встроенный перехватчик для передачи атрибутов HTTP-сессии в WebSocket-сессию:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new MyHandler(), "/myHandler")
            .addInterceptors(new HttpSessionHandshakeInterceptor());
    }
}

В следующем примере показан XML-эквивалент конфигурации из предыдущего примера:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">
    <websocket:handlers>
        <websocket:mapping path="/myHandler" handler="myHandler"/>
        <websocket:handshake-interceptors>
            <bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
        </websocket:handshake-interceptors>
    </websocket:handlers>
    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>

Более продвинутым вариантом является расширение DefaultHandshakeHandler, который выполняет все шаги подтверждения установления связи для WebSocket, включая проверку происхождения клиента, согласование субпротокола и другое. Приложению также может понадобиться использовать эту опцию, если ему необходимо сконфигурировать кастомнуюRequestUpgradeStrategy для адаптации к движку и версии сервера WebSocket, которые еще не поддерживаются. Как Java-конфигурация, так и пространство имен XML позволяют сконфигурировать кастомный HandshakeHandler.

Spring предусматривает базовый класс WebSocketHandlerDecorator, который можно использовать для декорирования WebSocketHandler дополнительной логикой работы. Реализации журналирования и обработки исключений предусмотрены и добавляются по умолчанию при использовании Java-конфигурации WebSocket или пространства имен XML. ExceptionWebSocketHandlerDecorator перехватывает все неперехваченные исключения, возникающие из любого метода WebSocketHandler, и закрывает сессию WebSocket со статусом 1011, который указывает на ошибку сервера.

Развертывание

Spring WebSocket API легко интегрировать в приложение на Spring MVC, если DispatcherServlet обрабатывает как подтверждение установления связи по протоколу WebSocket через HTTP, так и другие HTTP запросы. Его также легко интегрировать в другие сценарии HTTP-обработки путём вызова WebSocketHttpRequestHandler. Это удобно и понятно. Однако в отношении сред выполнения JSR-356 действуют особые соображения.

WebSocket API на Java (JSR-356) предусматривает два механизма развертывания. Первый включает сканирование classpath контейнера сервлетов (функция Servlet 3) при запуске. Другой – это API регистрации для использования при инициализации контейнера сервлетов. Ни один из этих механизмов не позволяет использовать единый "фронтальный контроллер" для всех процедур HTTP-обработки – включая подтверждение установления связи по протоколу WebSocket и все остальные HTTP-запросы – такой как, например, DispatcherServlet в Spring MVC.

Это существенное ограничение JSR-356, которое средства поддержки WebSocket в Spring решают с помощью специфических для сервера реализаций RequestUpgradeStrategy даже при работе в среде выполнения JSR-356. В настоящее время такие стратегии существуют для Tomcat, Jetty, GlassFish, WebLogic, WebSphere и Undertow (и WildFly).

Был создан запрос на устранение предыдущего ограничения WebSocket API на Java, который можно найти по адресу eclipse-ee4j/websocket-api#211. Tomcat, Undertow и WebSphere предусматривают собственные альтернативные API, позволяющие сделать это, а также это возможно осуществить с помощью Jetty. Надеемся, что то же самое будет сделано и для других серверов.

Второстепенным моментом является то, что контейнеры сервлетов с поддержкой JSR-356 должны выполнять сканирование ServletContainerInitializer (SCI), которое может замедлить запуск приложения – в некоторых случаях значительно. Если после обновления до версии контейнера сервлетов с поддержкой JSR-356 наблюдается значительное замедление, должна быть возможность выборочно активировать или дезактивировать веб-фрагменты (и сканирование SCI) с помощью элемента <absolute-ordering /> в web.xml, как показано в следующем примере:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/javaee
        https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">
    <absolute-ordering/>
</web-app>

Затем можно выборочно активировать веб-фрагменты по имени, например, собственный SpringServletContainerInitializer из Spring, который обеспечивает поддержку API инициализации на Java.для Servlet 3 В следующем примере показано, как это сделать:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://java.sun.com/xml/ns/javaee
        https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">
    <absolute-ordering>
        <name>spring_web</name>
    </absolute-ordering>
</web-app>

Серверная конфигурация

Каждый базовый механизм протокола WebSocket открывает свойства конфигурации, которые управляют характеристиками времени выполнения, такими как размер буфера сообщений, время ожидания бездействия и другие.

Для Tomcat, WildFly и GlassFish можно добавить ServletServerContainerFactoryBean в Java-конфиг WebSocket, как показано в следующем примере:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Bean
    public ServletServerContainerFactoryBean createWebSocketContainer() {
        ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
        container.setMaxTextMessageBufferSize(8192);
        container.setMaxBinaryMessageBufferSize(8192);
        return container;
    }
}

В следующем примере показан XML-эквивалент конфигурации из предыдущего примера:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">
    <bean class="org.springframework...ServletServerContainerFactoryBean">
        <property name="maxTextMessageBufferSize" value="8192"/>
        <property name="maxBinaryMessageBufferSize" value="8192"/>
    </bean>
</beans>
Для конфигурации WebSocket на стороне клиента следует использовать WebSocketContainerFactoryBean (XML) или ContainerProvider.getWebSocketContainer() (Java-конфигурация).

В случае Jetty необходимо предоставить предварительно настроенную WebSocketServerFactory для Jetty и подключить ее к DefaultHandshakeHandler из Spring через Java-конфиг для WebSocket. В следующем примере показано, как это сделать:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(echoWebSocketHandler(),
            "/echo").setHandshakeHandler(handshakeHandler());
    }
    @Bean
    public DefaultHandshakeHandler handshakeHandler() {
        WebSocketPolicy policy = new WebSocketPolicy(WebSocketBehavior.SERVER);
        policy.setInputBufferSize(8192);
        policy.setIdleTimeout(600000);
        return new DefaultHandshakeHandler(
                new JettyRequestUpgradeStrategy(new WebSocketServerFactory(policy)));
    }
}

В следующем примере показан XML-эквивалент конфигурации из предыдущего примера:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">
    <websocket:handlers>
        <websocket:mapping path="/echo" handler="echoHandler"/>
        <websocket:handshake-handler ref="handshakeHandler"/>
    </websocket:handlers>
    <bean id="handshakeHandler" class="org.springframework...DefaultHandshakeHandler">
        <constructor-arg ref="upgradeStrategy"/>
    </bean>
    <bean id="upgradeStrategy" class="org.springframework...JettyRequestUpgradeStrategy">
        <constructor-arg ref="serverFactory"/>
    </bean>
    <bean id="serverFactory" class="org.eclipse.jetty...WebSocketServerFactory">
        <constructor-arg>
            <bean class="org.eclipse.jetty...WebSocketPolicy">
                <constructor-arg value="SERVER"/>
                <property name="inputBufferSize" value="8092"/>
                <property name="idleTimeout" value="600000"/>
            </bean>
        </constructor-arg>
    </bean>
</beans>

Допустимые источники

Начиная с версии Spring Framework 4.1.5, логика работы по умолчанию для WebSocket и SockJS состоит в том, чтобы принимать запросы только из одного и того же источника. Также можно разрешить все или определенный список источников. Данная проверка в основном предназначена для клиентов браузеров. Ничто не мешает другим типам клиентов изменять значение заголовка Origin (подробную информацию см.в разделе "RFC 6454: Концепция Web Origin").

Возможны следующие три варианта логики работы:

  • Разрешить только запросы из одного и того же источника (по умолчанию): В этом режиме, если SockJS активирован, встроенный фрейм (Iframe) HTTP-ответа для заголовка X-Frame-Options устанавливается в SAMEORIGIN, а механизм передачи JSONP отключен, поскольку он не позволяет проверить источник запроса. Как следствие, отсутствует поддержка IE6 и IE7, если этот режим активирован.

  • Разрешить заданный список источников: Каждый допустимый источник должен начинаться с http:// или https://. В этом режиме, если SockJS активирован, передача iframe отключена. Как следствие, отсутствует поддержка IE6 через IE9, если этот режим активирован.

  • Разрешить любые источники: Чтобы активировать этот режим, нужно указать * в качестве допустимого значения источника. В этом режиме доступны все механизмы передачи.

Вы можете настроить допустимые источники WebSocket и SockJS, как показано в следующем примере:

import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler(), "/myHandler").setAllowedOrigins("https://mydomain.com");
    }
    @Bean
    public WebSocketHandler myHandler() {
        return new MyHandler();
    }
}

В следующем примере показан XML-эквивалент конфигурации из предыдущего примера:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:websocket="http://www.springframework.org/schema/websocket"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/websocket
        https://www.springframework.org/schema/websocket/spring-websocket.xsd">
    <websocket:handlers allowed-origins="https://mydomain.com">
        <websocket:mapping path="/myHandler" handler="myHandler" />
    </websocket:handlers>
    <bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>