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