Статьи

WebSocket и Java

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):

  1. Браузер отправляет HTTP-запрос со специальным заголовком Upgrade со значением «websocket».
  2. Если сервер «говорит» weboscket, он отвечает со статусом 101 — протоколы переключения. Отныне мы больше не используем HTTP
  3. Когда сервер принимает соединение с сокетом tcp, вызывается метод инициализации, когда передается текущий сеанс websocket. Каждый сокет имеет уникальный идентификатор сеанса.
  4. Всякий раз, когда браузер отправляет сообщение на сервер, вызывается другой метод, где вы получаете сеанс и полезную нагрузку сообщения.
  5. На основе некоторого параметра полезной нагрузки код приложения выполняет одно из нескольких действий. Формат полезной нагрузки полностью зависит от разработчика. Обычно, однако, это JSON-сериализованный объект.
  6. Когда серверу необходимо отправить сообщение, ему нужно получить объект сеанса и использовать его для отправки сообщения.
  7. Когда браузер закрывает соединение, сервер получает уведомление, чтобы он мог очистить любые ресурсы, связанные с конкретным сеансом.

В настоящее время ни 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.

Ссылка: WebSocket и Java от нашего партнера по JCG Глэмдринга в блоге Java Advent Calendar .