Доброго времени суток уважаемые хабравчане. В данной статье хочу продолжить рассказ устройства 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)


  1. vlanko
    05.12.2017 14:46

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


    1. PavelMel Автор
      06.12.2017 00:03

      Здравствуйте, ваши замечания учел и расширил информацию о транспортных протоколах.