Spring Websocket + SockJs. How it works?

    Доброго времени суток уважаемые хабравчане. В данной статье хочу продолжить рассказ устройства Spring Websocket, рассмотрев серверную реализацию Spring Websocket + SockJs.

    SockJs — это JavaScript библиотека, которая обеспечивает двусторонний междоменный канал связи между клиентом и сервером. Другими словами SockJs имитирует WebSocket API. Под капотом SockJS сначала пытается использовать нативную реализацию WebSocket API. Если это не удается, используются различные транспортные протоколы, специфичные для браузера, и представляет их через абстракции, подобные WebSocket. Про порт данной библиотеки в мир Spring Frameworks мы сегодня и поговорим.

    Использование Websocket и SockJs доступно в Spring с версии 4.0.
    На данный момент, в зависимости от браузера, SockJs может использовать следующие транспортные протоколы:
    Browser Websockets Streaming Polling
    IE 6, 7 no no jsonp-polling
    IE 8, 9 (cookies=no) no xdr-streaming * xdr-polling *
    IE 8, 9 (cookies=yes) no iframe-htmlfile iframe-xhr-polling
    IE 10 rfc6455 xhr-streaming xhr-polling
    Chrome 6-13 hixie-76 xhr-streaming xhr-polling
    Chrome 14+ hybi-10 / rfc6455 xhr-streaming xhr-polling
    Firefox <10 no ** xhr-streaming xhr-polling
    Firefox 10+ hybi-10 / rfc6455 xhr-streaming xhr-polling
    Safari 5.x hixie-76 xhr-streaming xhr-polling
    Safari 6+ rfc6455 xhr-streaming xhr-polling
    Opera 10.70+ no ** iframe-eventsource iframe-xhr-polling
    Opera 12.10+ rfc6455 xhr-streaming xhr-polling
    Konqueror no no jsonp-polling

    * IE 8+ поддерживает XDomainRequest, который, по существу, является модифицированным AJAX / XHR, способный отправлять кроссдоменные запросы. Но не отправляет cookies.
    ** Firefox 4.0 и Opera 11.00 поставлялись с отключенным протоколом Websocket «hixie-76». Который можно активировать в настройках.

    Порой бывает необходимо загрузить html из файла. Однако в этом случае не предается заголовок Origin, что может сделать недоступным использование кроссдоменных запросов. Для разрешения этой ситуации используются следующие протоколы:
    Browser Websockets Streaming Polling
    IE 8, 9 same as above iframe-htmlfile iframe-xhr-polling
    Other same as above iframe-eventsource iframe-xhr-polling

    Рассмотрим основные протоколы чуть более подробно.

    WebSocket


    WebSocket обеспечивает двустороннюю связь между клиентом и сервером, используя одно TCP соединение.

    XhrPolling (long)


    Данный протокол взаимодействия характеризуется ситуацией, когда при отправке запроса на сервер соединение не закрывается до момента появления сообщения у сервера. При появлении сообщения происходит пересылка данных клиенту и создание нового соединения.

    JsonpPolling (long)


    JSONP («JSON with Padding»).
    Похож на предыдущий протокол, но используется для кроссдоменного взаимодействия. При пересылке данных сервер кодирует данные в JSON и оборачивает их в вызов функции, название которой получает из параметра callback.

    XhrStreaming


    Данный протокол основывается на возможности получения части данных до момента полной загрузки.

    Пример использования частичной загрузки
    <script>
    var xhr = new XMLHttpRequest();
    xhr.open('GET', '/stream');
    xhr.seenBytes = 0;
    
    xhr.onreadystatechange = function() {
      if(xhr.readyState == 3) {
        var newData = xhr.response.substr(xhr.seenBytes); 
    //обработка новых данных
        xhr.seenBytes = xhr.responseText.length;
      }
    };
    xhr.send();
    </script>


    EventSource


    В качестве реализации данного протокола на клиентской стороне используется объект EventSource. Данный объект предназначен для передачи текстовых сообщений используя Http. Главным преимуществом данного подхода является автоматическое переподключение и наличие идентификаторов сообщения для возобновления потока данных.

    IFrame


    Идея использования IFrame заключается в возможности последовательной обработки страницы по мере загрузки данных из сервера. Схема взаимодействия довольно проста — создается скрытых IFrame, идет запрос на сервер, который возвращает шапку документа и держит соединение. Каждый раз когда появляются новые данные сервер обрамляет их в тег script и отправляет в IFrame. IFrame получив новый блок script начнет его выполнение.

    HtmlFile


    Данный подход используется в IE и заключается в оборачивании IFrame в объект ActiveX. А основное преимущество использования — сокрытие действий в IFrame от пользователя.

    Структура SockJs


    Иерархия транспортных обработчиков



    Иерархия сессий



    Создание конфигурационного класса


    Для возможности использовать SockJs в Spring приложении достаточно вызвать метод .withSockJS() при регистрировании обработчиков (WebSocketHandler).

    @Configuration
    @EnableWebSocket
    public class WebSocketConfig implements WebSocketConfigurer {
       @Override
       public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
           registry.addHandler(new EchoWebSocketHandler(), "/init").withSockJS();
       }
    }

    Реализацию метода withSockJS() можно увидеть в классе AbstractWebSocketHandlerRegistration. Основная задача данного метода создать фабрику SockJsServiceRegistration, из которой создается главный класс обработки Http запросов SockJsService. После создания экземпляра SockJsService происходит связывание данного сервиса с WebSocketHandler и преобразование в HandlerMapping. Адаптером в данном случае выступает класс SockJsHttpRequestHandler.

    При создании экземпляра SockJsService в него передается планировщик задач (TaskScheduler), который в дальнейшем будет использоваться для отсылки Heartbeat сообщений.

    В качестве кодека преобразования сообщений по умолчанию используется Jackson2SockJsMessageCodec

    Для подключения SockJs на клиентской стороне необходимо добавить javascript библиотеку, и создать SockJS объект, при этом изменив протокол нашего endpoint с ws(wss) на http(https)

    <!DOCTYPE html>
    <html lang="en" ng-app="testSockJs">
    <head>
        <meta charset="utf-8">
        <title>Test SockJs</title>
    </head>
    <body>
            <script src="https://cdn.jsdelivr.net/npm/sockjs-client@1/dist/sockjs.min.js">
            </script>
    	<script>
    	    var ws = new SockJS("http://localhost:8080/init");
    	    ws.onmessage = function(data){  console.log(data);  }
    	</script>
    </body>
    </html>


    Описание алгоритма взаимодействия


    Работа начинается с клиентского запроса /info, в ответ на который сервер возвращает объект вида
    {"entropy":293909549,"origins":["*:*"],"cookie_needed":true,"websocket":true}

    который указывает на доступные url для обработки клиентских запросов. необходимы ли куки и есть ли возможность использовать webSocket. На основании этих данных клиентская библиотека выбирает транспортный протокол.

    Все клиентские запросы имеют вид
    http://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}

    {server-id} — случайный параметр от 000 до 999, единственное назначение которого упростить балансировку на серверной стороне.
    {session-id} -сопоставляет HTTP-запросы, принадлежащие сессии SockJS.
    {transport} — указывает на транспортный протокол «websocket», «xhr-streaming», и т.д.

    Для поддержания совместимости с Websocket Api SockJs использует кастомный протокол обмена сообщениями:
    o — (open frame) отправляется каждый раз при открытии новой сессии.
    c — (close frame) отправляется когда клиент запрашивает закрытие соединения.
    h — (heartbeat frame) проверка доступности соединения.
    a — (data frame) Массив json сообщений. К примеру: a[«message»].

    Пример fallback


    Рассмотрим пример когда у нас на сервере нет возможности обработать Websocket, сделать это довольно просто, установив переменную webSocketEnabled в false в классе SockJsServiceRegistration

    @Configuration
    @EnableWebSocket
    public class WebSocketConfig implements WebSocketConfigurer {
       @Override
       public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
           registry.addHandler(new EchoWebSocketHandler(), "/init").setAllowedOrigins("*").withSockJS().setWebSocketEnabled(false);
       }
    }

    Клиент проверит возможность открытия сокета вызовом /info. Получив негативный ответ, будут использоваться два канала для обмена сообщениями: один для приема сообщений — как правило streaming протокол, и один для отправки сообщений на сервер (http запросы). Данные каналы коммуникации будут связываться одной sessionId передаваемой в URL.

    При отправке сообщения с клиента запрос попадает на DispatcherServlet, от куда перенаправляется на наш адаптер SockJsHttpRequestHandler. Данный класс преобразовывает запрос и перенаправляет его в SockJsService, который делегирует функцию принятия сообщения на пользовательскую сессию SockJsSession. А так как наша сессия связана к обработчиком WebSocketHandler мы получаем отправленное сообщение в нашем обработчике.

    Для отправки сообщения клиенту, мы по прежнему используем WebSocketSession. Дело в том что SockJsSession является расширением WebSocketSession. А конкретные реализации SockJsSession привязаны к транспортному протокому. Поэтому на серверной стороне при вызове session.sendMessage(new TextMessage(«some message»)); происходит преобразование сообщения к конкретному типу протокола и отправка форматированного сообщения к клиенту.

    Вот, собственно, и вся магия возможности fallback при использовании SockJs.

    Использованные источники:
    Websocket SockJs
    SockJs protocol
    Поделиться публикацией

    Комментарии 2

      0
      Крайне мало данных.
      Websocket, как я помню, со Spring 4.
      Нужно уточнить, какие браузері поддерживают веб-сокеты.
      SockJs — более широкий набор вариантов соединения (намного шире, чем я знал), можно использовать для устаревших.
        +1
        Здравствуйте, ваши замечания учел и расширил информацию о транспортных протоколах.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое