WebSocket — это отличная новая (ish) технология, которая обеспечивает двустороннюю связь между браузером и сервером в режиме реального времени практически без затрат. То, что я хочу сделать здесь, это предоставить очень лаконичную, но достаточную информацию о том, как начать использовать технологию. Итак, несколько вещей для начала:
- между браузером и сервером открывается соединение через сокет tcp, и каждая сторона может отправлять сообщения другой стороне (т. е. сервер может передавать данные, когда у них есть такие данные — нет необходимости в опросе, длительном опросе, фреймах и т. д.)
- не все браузеры поддерживают его — IE 10 является первой версией IE, поддерживающей его, Android по-прежнему имеет проблемы. К счастью, есть SockJS , который использует другие push-эмуляции, если WebSocket не поддерживается.
- не все прокси-серверы поддерживают это / разрешают, поэтому может потребоваться повторный откат
- подходит для игр, торговых приложений и всего, что требует от сервера передачи данных в браузер
- Java имеет стандартный API (JSR-356) , который вы можете использовать на сервере для обработки соединений WebSocket.
- Spring предоставляет API поверх API Java. Хорошая особенность поддержки Spring состоит в том, что она поддерживает SockJS на стороне сервера, и вы можете использовать внедрение зависимостей без особых усилий. Spring также предоставляет поддержку STOMP для архитектуры, управляемой сообщениями. Обе весенние статьи содержат ссылки на примеры проектов GitHub, которые я рекомендую.
Прежде чем перейти к некоторому примеру кода, вот жизненный цикл сокета, включая клиент и сервер (при условии, что один из приведенных выше API):
- Браузер отправляет HTTP-запрос со специальным заголовком Upgrade со значением «websocket».
- Если сервер «говорит» weboscket, он отвечает со статусом 101 — протоколы переключения. Отныне мы больше не используем HTTP
- Когда сервер принимает соединение с сокетом tcp, вызывается метод инициализации, когда передается текущий сеанс websocket. Каждый сокет имеет уникальный идентификатор сеанса.
- Всякий раз, когда браузер отправляет сообщение на сервер, вызывается другой метод, где вы получаете сеанс и полезную нагрузку сообщения.
- На основе некоторого параметра полезной нагрузки код приложения выполняет одно из нескольких действий. Формат полезной нагрузки полностью зависит от разработчика. Обычно, однако, это JSON-сериализованный объект.
- Когда серверу необходимо отправить сообщение, ему нужно получить объект сеанса и использовать его для отправки сообщения.
- Когда браузер закрывает соединение, сервер получает уведомление, чтобы он мог очистить любые ресурсы, связанные с конкретным сеансом.
В настоящее время ни API, ни инфраструктура не поддерживают маршрутизацию на основе аннотаций. Java API поддерживает основанные на аннотациях обработчики конечных точек, но он предоставляет вам один класс для каждого URL-адреса соединения, и обычно вы хотите выполнить несколько операций с одним соединением. То есть вы подключаетесь к ws: //yourserver.com/game/ и затем хотите передать сообщение «joinGame», «покинуть игру». Кроме того, сервер должен отправить более одного типа сообщений обратно. Я реализовал это с помощью перечисления, содержащего все возможные типы действий / событий, и использования конструкции switch для определения того, что вызывать.
Поэтому я решил сделать простую игру для своего композитора алгоритмической музыки . Он использует Spring API. Вот слайды для соответствующей презентации, которую я сделал в компании, в которой я работаю. И ниже приведен пример кода:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
@Component public class GameHandler extends WebSocketHandlerAdapter { private Map players = new ConcurrentHashMap<>(); private Map playerGames = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { Player player = new Player(session); players.put(session.getId(), player); } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { leaveGame(session.getId()); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage textMessage) throws Exception { try { GameMessage message = getMessage(textMessage); //deserializes the JSON payload switch (message.getAction()) { case INITIALIZE: initialize(message, session); break ; case JOIN: join(message.getGameId(), message.getPlayerName(), session); break ; case LEAVE: leave(session.getId()); break ; case START: startGame(message); break ; case ANSWER: answer(message, session.getId()); break ; } } catch (Exception ex) { logger.error( "Exception occurred while handling message" , ex); } } |
Давайте посмотрим на пример secnario, где серверу нужно отправлять сообщения клиентам. Давайте рассмотрим случай, когда игрок присоединяется к игре, и все остальные игроки должны быть уведомлены о новом прибытии. Центральным классом в системе является Game, в котором есть список игроков, и, как вы можете видеть, Player содержит ссылку на сеанс WebSocket. Итак, когда игрок присоединяется, вызывается следующий метод Game:
1
2
3
4
5
6
7
|
public boolean playerJoined(Player player) { for (Player otherPlayer : players.values()) { otherPlayer.playerJoined(player); } players.put(player.getSession().getId(), player); return true ; } |
И player.playerJoined (..) отправляет сообщение через базовое соединение, уведомляя браузер о присоединении нового игрока:
01
02
03
04
05
06
07
08
09
10
|
public void playerJoined(Player player) { GameEvent event = new GameEvent(GameEventType.PLAYER_JOINED); event.setPlayerId(player.getSession().getId()); event.setPlayerName(player.getName()); try { session.sendMessage( new TextMessage(event.toJson())); } catch (IOException e) { new IllegalStateException(e); } } |
Отправка сообщений с сервера в браузер также может быть инициирована запланированным заданием.
Дело в том, что у вас есть список всех подключенных браузеров, чтобы вы могли отправлять информацию обратно. Список может быть статическим полем, но в случае одноэлементного пружинного компонента это не обязательно.
Теперь два важных аспекта — безопасность и аутентификация. Вот хорошая статья Heroku , обсуждающая оба. Вы должны предпочесть wss (который является websocket вместо TLS), если есть что-то чувствительное. Вы также должны проверить свои данные на обоих концах и не должны полагаться на заголовок Origin, поскольку злоумышленник может очень легко подделать браузер.
Аутентификация может полагаться на cookie-файл сеанса HTTP, но, по-видимому, некоторые люди предпочитают реализовывать свой собственный рабочий процесс, подобный cookie-файлу, для получения кратковременного токена, который можно использовать для выполнения аутентифицированных операций.
WebSocket делает DDD естественным. Вы больше не работаете с анемичными объектами — ваши объекты имеют соответствующее состояние, и операции выполняются в этом состоянии. В связи с этим веб-сокет-приложение легче тестируется.
Это общий набор вещей, которые следует иметь в виду при разработке приложения WebSocket. Обратите внимание, что вам не нужно использовать WebSocket везде — я бы ограничил его только функциями, для которых требуется «push».
В целом, WebSocket — это приятная и интересная технология, которая, надо надеяться, устареет от всех хакерских эмуляций push.