Spring Websocket + SockJs. How it works?

в 20:53, , рубрики: java, sockjs, spring framework, websockets

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

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

На данный момент SockJs использует следующие транспортные протоколы:
WebSocket, XhrPolling, XhrStreaming, EventSource, HtmlFile, JsonpPolling, IFrame.

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

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

Spring Websocket + SockJs. How it works? - 1

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

Spring Websocket + SockJs. How it works? - 2

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

Для возможности использовать 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

Автор: PavelMel

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js