У загальнодоступній частині інтернету обмежувальні проксі-сервери, що знаходяться поза твоїм контролем, можуть
перешкоджати взаємодії WebSocket або тому, що вони не налаштовані на передачу заголовка Upgrade
, або
тому, що вони закривають довготривалі з'єднання, які, здається, неактивні.
Рішенням цієї проблеми є емуляція WebSocket — тобто спроба спочатку використовувати WebSocket, а потім повернутися до технологій на основі HTTP, які емулюють взаємодію WebSocket і надають той же API на рівні програми.
У стеку сервлетів Spring Framework забезпечує серверну (а також клієнтську) підтримку протоколу SockJS.
Короткий опис
Мета SockJS — дозволити програмам використовувати WebSocket API, але в разі необхідності під час виконання повертатися до альтернатив, не пов'язаних з WebSocket, без необхідності зміни коду програми.
SockJS складається з:
Протоколу SockJS, визначеного у вигляді виконуваних тестів з коментарями.
Клієнта SockJS з JavaScript — клієнтська бібліотека для використання в браузерах.
Реалізації сервера SockJS, включно з одною в модулі
spring-websocket
зі Spring Framework.Java-клієнта SockJS у модулі
spring-websocket
(починаючи з версії 4.1 ).
SockJS призначений для використання в браузерах. Він використовує різні методи підтримки широкого спектру версій браузерів. Повний список типів механізмів передачі SockJS та браузерів дивися на сторінці клієнта SockJS. Механізми передачі поділяються на три загальні категорії: WebSocket, потокова передача за протоколом HTTP та довгий HTTP-поллінг. Огляд цих категорій див. у цій статті блога.
Клієнт SockJS починає з відправки GET /info
для
отримання
основної інформації від сервера. Після цього він має вирішити, який механізм передачі використовувати. Якщо можливо,
використовується протокол WebSocket. Якщо ні, то в більшості браузерів є принаймні одна опція потокової передачі за
протоколом HTTP. Якщо і такої немає, то використовується HTTP-поллінг (довгий).
Всі запити на передачу мають таку структуру URL-адреси:
https://host:port/myApp/myEndpoint/{server-id}/{session-id}/{transport}
де:
{server-id}
використовується для маршрутизації запитів у кластері, але в інших випадках не використовується.{session-id}
співвідносить HTTP-запити, що належать до сесії SockJS.{transport}
вказує тип механізму передачі (наприклад,websocket
,xhr-streaming
та інші).
Механізму передачі за протоколом WebSocket потрібен лише один HTTP-запит для підтвердження встановлення зв'язку за протоколом WebSocket. Всі наступні повідомлення надсилаються через цей сокет.
Механізмам передачі за протоколом HTTP потрібно більше запитів. Поточна передача Ajax/XHR, наприклад, використовує один тривалий запит для передачі повідомлень від сервера до клієнта і додаткові POST-запити за протоколом HTTP для передачі повідомлень від клієнта до сервера. Довгий полінг працює аналогічним способом, за винятком того, що він завершує поточний запит після кожного надсилання даних від сервера до клієнта.
SockJS додає простий фреймінг
повідомлень. Наприклад, сервер спочатку передає літеру o
("open" кадр), повідомлення передаються у
вигляді a["message1", "message2"]
(JSON-кодований масив), літеру h
("heartbeat" кадр),
якщо
протягом 25 секунд (за замовчуванням) не надходить жодних повідомлень, і літеру c
("close" кадр) для
закриття сесії.
Щоб дізнатися більше, запусти приклад у браузері та переглянь HTTP-запити. Клієнт SockJS
дозволяє фіксувати перелік механізмів передачі, тому можна переглядати кожен механізм по черзі. Клієнт SockJS також
передбачає прапор налагодження, що дозволяє виводити корисні повідомлення в консолі браузера. На стороні сервера
можна активувати журнал TRACE
для org.springframework.web.socket
. Ще більш детальну
інформацію можна знайти в тесті протоколу SockJS з коментарями.
Активація SockJS
Ти можеш активувати SockJS через Java-конфігурацію, як показано в наступному прикладі:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myHandler(), "/myHandler").withSockJS();
}
@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:sockjs/>
</websocket:handlers>
<bean id="myHandler" class="org.springframework.samples.MyHandler"/>
</beans>
Попередній приклад призначений для використання у програмах Spring MVC і повинен бути включений до
конфігурації DispatcherServlet
. Однак підтримка WebSocket та SockJS у Spring не залежить від Spring
MVC. За допомогою SockJsHttpRequestHandler
відносно просто інтегруватися в інші середовища,
що працюють з протоколом HTTP.
На стороні браузера програми можуть використовувати sockjs-client
(версія 1.0.x).
Він емулює API для WebSocket, стандартизований консорціумом W3C, та взаємодіє з сервером для вибору найкращого
варіанта передачі залежно від браузера, у якому він запущений. Див. сторінку sockjs-client та список механізмів
передачі, які підтримує браузер. Клієнт також передбачає кілька опцій конфігурації — наприклад, для зазначення того,
які механізми передачі включити до складу.
IE 8 та 9
Internet Explorer 8 та 9, як і раніше, використовуються. Вони є ключовою причиною наявності SockJS. У цьому розділі розглядаються важливі аспекти роботи в цих браузерах.
Клієнт SockJS підтримує потокову передачу Ajax/XHR у IE 8 та 9 за допомогою XDomainRequest
від Microsoft. Цей спосіб працює в різних доменах, але не
підтримує надсилання cookie. Файли cookie часто потрібні для роботи Java-додатків. Однак, оскільки клієнт SockJS
може використовуватися з багатьма типами серверів (не тільки Java), йому необхідно розуміти, чи мають файли cookie
якусь значимість. Якщо так, клієнт SockJS надасть перевагу Ajax/XHR для потокової передачі. В іншому випадку
використовується техніка на основі iframe.
Перший запит /info
від клієнта SockJS — це запит
інформації, яка може вплинути на вибір механізмом передачі клієнта. Частиною такої інформації є те, чи серверний
додаток використовує файли cookie (наприклад, для аутентифікації або кластеризації при липких сесіях (sticky
sessions)). Засоби підтримки SockJS у Spring включають властивість під назвою sessionCookieNeeded
. Цей
засіб активовано за замовчуванням, оскільки більшість Java-програм використовують cookie JSESSIONID
.
Якщо твоїй програмі вона не потрібна, то можна відключити цю опцію, і тоді клієнт SockJS повинен буде вибрати xdr-streaming
в IE 8 і 9.
Якщо ти використовуєш механізм передачі на основі iframe, май на увазі, що браузерам може бути
дана команда блокувати використання IFrame на даній сторінці, встановивши в заголовку HTTP-відповіді X-Frame-Options
значення DENY
, SAMEORIGIN
або ALLOW-FROM <origin>
. Це використовується
для запобігання клікджекінгу.
Spring Security 3.2+ забезпечує підтримку встановлення заголовка X-Frame-Options
для кожної відповіді. За замовчуванням Java-конфігурація Spring Security встановлює значення DENY
. У
версії 3.2 простір імен XML для Spring Security не встановлює цей заголовок за замовчуванням, але його можна
налаштувати на таку установку. У майбутньому воно зможе встановлювати цей заголовок за замовчуванням.
Див. розділ "Заголовки безпеки за замовчуванням" документації Spring Security для отримання
докладних
відомостей про те, як налаштувати параметри заголовка X-Frame-Options
. Ти також можеш ознайомитися
з gh-2718 для
отримання додаткової інформації.
Якщо твоя програма додає заголовок відповіді X-Frame-Options
(як і має бути!) і використовує
механізм передачі на основі iframe, то потрібно встановити значення заголовка в SAMEORIGIN
або ALLOW-FROM<origin>
.
Засобам підтримки Spring для SockJS також має бути відоме розташування клієнта SockJS,
оскільки він завантажується з iframe. За замовчуванням iframe налаштований на завантаження клієнта SockJS із CDN.
Хорошою ідеєю є налаштування цього параметра на використання URL-адреси з того ж джерела, що й програма.
У наступному прикладі показано, як це зробити за допомогою Java-конфігурації:
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS()
.setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js");
}
// ...
}
<websocket:sockjs>
.
Під час попередньої розробки активуй режим
devel
клієнта SockJS, який допоможе запобігти кешуванню браузером запитів SockJS (наприклад, iframe),
які в іншому разі були б кешовані. Докладніше про те, як його активувати, дивися на сторінці
клієнта SockJS.
Heartbeat-повідомлення
Протокол SockJS вимагає надсилання heartbeat-повідомлень з боку сервера, щоб
проксі-сервери не змогли дійти висновку, що з'єднання повисло. У конфігурації SockJS для Spring є властивість heartbeatTime
,
яку можна використовувати для налаштування частоти. За замовчуванням heartbeat надсилається через 25 секунд, за
умови, що для цього з'єднання не було надіслано жодних інших повідомлень. Це 25-секундне значення відповідає
наступній рекомендації IETF для інтернет-додатків
загального користування.
Засоби підтримки SockJS Spring також дозволяє налаштувати TaskScheduler
для планування завдань
передачі heartbeat-повідомлень. Планувальник завдань використовує пул потоків, налаштування якого за замовчуванням
залежить від кількості доступних процесорів. Слід врахувати можливість налаштування параметрів відповідно до
конкретних потреб.
Розрив з'єднання з клієнтом
Для потокової передачі за протоколом HTTP та довгого HTTP-поллінгу механізмів передачі SockJS потрібно, щоб з'єднання залишалося відкритим довше, ніж зазвичай. Короткий опис цих методів наведено в цій статті блога.
У контейнерах сервлетів це здійснюється завдяки асинхронній підтримці Servlet 3, яка дозволяє вийти з потоку контейнера сервлетів, обробити запит і продовжити запис у відповідь з іншого потоку.
Специфічна проблема полягає в тому, що Servlet API не надає повідомлень для клієнта, який зник. Див. eclipse-ee4j/servlet-api#44. Однак контейнери сервлетів генерують виняток при подальших спробах запису у відповідь. Оскільки служба Spring для SockJS підтримує надіслані сервером heartbeat-повідомлення (за замовчуванням кожні 25 секунд), це означає, що розрив з'єднання з клієнтом зазвичай виявляється протягом цього часу (або раніше, якщо повідомлення надсилаються частіше).
DISCONNECTED_CLIENT_LOG_CATEGORY
(визначену в Ab
). Якщо потрібно бачити трасування стека, можна встановити категорію журналу в TRACE.
SockJS і CORS
Якщо ти допускаєш запити між різними джерелами, протокол SockJS буде використовувати
механізм CORS для міждоменної підтримки в потоковому транспорті XHR та транспорті опитування. Тому CORS-заголовки
додаються автоматично, якщо наявність CORS-заголовків у відповіді не виявлено. Таким чином, якщо програма вже
налаштована на підтримку CORS (наприклад, через фільтр сервлетів), SockJsService
зі Spring пропускає цю
частину.
Також можна деактивувати додавання цих CORS-заголовків, встановивши властивість
suppressCors
у SockJsService зі Spring.
SockJS приймає такі заголовки та значення:
Access-Control-Allow-Origin
: Ініціалізується зі значення заголовка запитуOrigin
.Access-Control-Allow-Credentials
: Завжди встановлюється вtrue
.Access-Control-Request-Headers
: Ініціалізується зі значень еквівалентного заголовка запиту.Access-Control-Allow-Methods
: HTTP-методи, які підтримують механізм передачі (див. типTransportType
).Access-Control-Max-Age
: Встановлює значення 31536000 (1 рік).
Для точної реалізації див. addCorsHeaders
в AbstractSockJsService
і тип TransportType
,
що перераховується, у вихідному коді.
До того ж, якщо CORS-конфігурація дозволяє це, врахуй можливість
виключення URL-адрес з префіксом кінцевої точки SockJS, що дозволить тим самим SockJsService
зі Spring
обробляти їх.
SockJsClient
Spring передбачає Java-клієнт SockJS для підключення до віддалених кінцевих точок SockJS без використання браузера. Це може бути особливо корисним, якщо потрібно забезпечити двонаправлений зв'язок між двома серверами через загальнодоступну мережу (тобто коли мережні проксі-сервери можуть перешкоджати використанню протоколу WebSocket). Java-клієнт SockJS також дуже корисний для тестування (наприклад, для імітації великої кількості одночасних користувачів).
Java-клієнт SockJS підтримує механізми передачі websocket
, xhr-streaming
та xhr-polling
.
Інші мають сенс тільки для використання у браузері.
Можна конфігурувати WebSocketTransport
за допомогою:
StandardWebSocketClient
у середовищі виконання JSR-356.JettyWebSocketClient,
використовуючи WebSocket API, вбудований у Jetty 9+.Будь-якої реалізації
WebSocketClient
зі Spring.
XhrTransport
, за визначенням, підтримує як xhr-streaming
, так і
xhr-polling
, оскільки, з точки зору клієнта, немає жодної різниці, крім URL-адреси, яка
використовується для підключення до сервера. Наразі існує дві реалізації:
RestTemplateXhrTransport
використовуєRestTemplate
зі Spring для HTTP-запитів.JettyXhrTransport
використовуєHttpClient
з Jetty для HTTP-запитів.
У наступному прикладі показано, як створити SockJS-клієнт та підключитися до кінцевої точки SockJS:
List<Transport> transports = new ArrayList<>(2);
transports.add(new WebSocketTransport(new StandardWebSocketClient()));
transports.add(new RestTemplateXhrTransport());
SockJsClient sockJsClient = new SockJsClient(transports);
sockJsClient.doHandshake(new MyWebSocketHandler(), "ws://example.com:8080/sockjs");
SockJsMessageCodec
і налаштувати її на SockJsClient
.
Щоб використовувати SockJsClient
для імітації великої кількості одночасних користувачів, необхідно
налаштувати базовий HTTP-клієнт (для XHR-механізму передачі) на достатню кількість з'єднань та потоків. У
наступному прикладі показано, як це зробити за допомогою Maven:
HttpClient jettyHttpClient = new HttpClient();
jettyHttpClient.setMaxConnectionsPerDestination(1000);
jettyHttpClient.setExecutor(new QueuedThreadPool(1000));
У наступному прикладі показані властивості, пов'язані з SockJS на стороні сервера (докладніше див. javadoc), які також слід налаштувати:
@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/sockjs").withSockJS()
.setStreamBytesLimit(512 * 1024)
.setHttpMessageCacheSize(1000)
.setDisconnectDelay(30 * 1000);
}
// ...
}
- Встановлюємо властивість
streamBytesLimit
рівною 512 КБ (за замовчуванням 128 КБ -128 * 1024
). - Встановлюємо властивість
httpMessageCacheSize
рівною 1,000 (за замовчуванням100
). - Встановлюємо властивість
disconnectDelay
рівною 30 секундам (за замовчуванням це п'ять секунд -5 * 1000
).