«1. Обзор

WebSocket обеспечивает альтернативу ограничениям эффективной связи между сервером и веб-браузером, обеспечивая двунаправленную полнодуплексную связь клиент/сервер в реальном времени. Сервер может отправить данные клиенту в любое время. Поскольку он работает по протоколу TCP, он также обеспечивает низкоуровневую связь с малой задержкой и снижает накладные расходы на каждое сообщение.

В этой статье мы рассмотрим Java API для WebSockets, создав приложение, похожее на чат.

2. JSR 356

JSR 356 или API Java для WebSocket определяет API, который Java-разработчики могут использовать для интеграции WebSockets со своими приложениями — как на стороне сервера, так и на стороне клиента Java.

Этот Java API предоставляет как серверные, так и клиентские компоненты:

    Сервер: все в пакете javax.websocket.server. Клиент: содержимое пакета javax.websocket, который состоит из клиентских API, а также общих библиотек для сервера и клиента.

3. Создание чата с использованием WebSockets

Мы создадим очень простое приложение, похожее на чат. Любой пользователь сможет открыть чат из любого браузера, ввести свое имя, авторизоваться в чате и начать общение со всеми, кто подключился к чату.

Начнем с добавления последней зависимости в файл pom.xml:

<dependency>
    <groupId>javax.websocket</groupId>
    <artifactId>javax.websocket-api</artifactId>
    <version>1.1</version>
</dependency>

Последнюю версию можно найти здесь.

Чтобы преобразовать объекты Java в их представления JSON и наоборот, мы будем использовать Gson:

<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.0</version>
</dependency>

Последняя версия доступна в репозитории Maven Central.

3.1. Конфигурация конечных точек

Существует два способа настройки конечных точек: на основе аннотаций и на основе расширений. Вы можете либо расширить класс javax.websocket.Endpoint, либо использовать специальные аннотации на уровне метода. Поскольку модель аннотаций приводит к более чистому коду по сравнению с программной моделью, аннотации стали обычным выбором кодирования. В этом случае события жизненного цикла конечной точки WebSocket обрабатываются следующими аннотациями:

    @ServerEndpoint: при наличии @ServerEndpoint контейнер обеспечивает доступность класса в качестве сервера WebSocket, прослушивающего определенное пространство URI @ClientEndpoint: класс, оформленный с этой аннотацией рассматривается как клиент WebSocket @OnOpen: метод Java с @OnOpen вызывается контейнером, когда инициируется новое соединение WebSocket @OnMessage: метод Java, аннотированный @OnMessage, получает информацию из контейнера WebSocket, когда сообщение отправляется в конечную точку @OnError: метод с @OnError вызывается при возникновении проблемы со связью @OnClose: используется для оформления метода Java, который вызывается контейнером при закрытии соединения WebSocket

3.2. Написание конечной точки сервера

Мы объявляем конечную точку сервера WebSocket класса Java, аннотируя ее с помощью @ServerEndpoint. Мы также указываем URI, где развернута конечная точка. URI определяется относительно корня контейнера сервера и должен начинаться с косой черты:

@ServerEndpoint(value = "/chat/{username}")
public class ChatEndpoint {

    @OnOpen
    public void onOpen(Session session) throws IOException {
        // Get session and WebSocket connection
    }

    @OnMessage
    public void onMessage(Session session, Message message) throws IOException {
        // Handle new messages
    }

    @OnClose
    public void onClose(Session session) throws IOException {
        // WebSocket connection closes
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        // Do error handling here
    }
}

Приведенный выше код является каркасом конечной точки сервера для нашего приложения, похожего на чат. Как видите, у нас есть 4 аннотации, сопоставленные с соответствующими методами. Ниже вы можете увидеть реализацию таких методов:

@ServerEndpoint(value="/chat/{username}")
public class ChatEndpoint {
 
    private Session session;
    private static Set<ChatEndpoint> chatEndpoints 
      = new CopyOnWriteArraySet<>();
    private static HashMap<String, String> users = new HashMap<>();

    @OnOpen
    public void onOpen(
      Session session, 
      @PathParam("username") String username) throws IOException {
 
        this.session = session;
        chatEndpoints.add(this);
        users.put(session.getId(), username);

        Message message = new Message();
        message.setFrom(username);
        message.setContent("Connected!");
        broadcast(message);
    }

    @OnMessage
    public void onMessage(Session session, Message message) 
      throws IOException {
 
        message.setFrom(users.get(session.getId()));
        broadcast(message);
    }

    @OnClose
    public void onClose(Session session) throws IOException {
 
        chatEndpoints.remove(this);
        Message message = new Message();
        message.setFrom(users.get(session.getId()));
        message.setContent("Disconnected!");
        broadcast(message);
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        // Do error handling here
    }

    private static void broadcast(Message message) 
      throws IOException, EncodeException {
 
        chatEndpoints.forEach(endpoint -> {
            synchronized (endpoint) {
                try {
                    endpoint.session.getBasicRemote().
                      sendObject(message);
                } catch (IOException | EncodeException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

При входе нового пользователя (@OnOpen) сразу сопоставляется со структурой данных активных пользователей. Затем создается сообщение, которое отправляется на все конечные точки с использованием широковещательного метода.

Этот метод также используется всякий раз, когда новое сообщение отправляется (@OnMessage) любым из подключенных пользователей — это основная цель чата.

Если в какой-то момент возникает ошибка, ее обрабатывает метод с аннотацией @OnError. Вы можете использовать этот метод для регистрации информации об ошибке и очистки конечных точек.

Наконец, когда пользователь больше не подключен к чату, метод @OnClose очищает конечную точку и сообщает всем пользователям, что пользователь был отключен.

4. Типы сообщений

«Спецификация WebSocket поддерживает два формата передаваемых данных — текстовый и двоичный. API поддерживает оба этих формата, добавляет возможности для работы с объектами Java и сообщениями проверки работоспособности (пинг-понг), как определено в спецификации:

    Текст: любые текстовые данные (java.lang.String, примитивы или их эквивалентные классы-оболочки ) Двоичные: Двоичные данные (например, аудио, изображения и т. д.), представленные java.nio.ByteBuffer или byte[] (байтовым массивом) Объекты Java: API позволяет работать с собственными представлениями (объект Java) в вашем коде. и использовать специальные преобразователи (кодировщики/декодеры) для преобразования их в совместимые сетевые форматы (текстовый, двоичный), разрешенные протоколом WebSocket. Пинг-понг: запрос проверки работоспособности (ping)

Для нашего приложения мы будем использовать объекты Java. Мы создадим классы для кодирования и декодирования сообщений.

4.1. Кодировщик

Кодировщик берет объект Java и создает типичное представление, подходящее для передачи в виде сообщения, такого как JSON, XML или двоичное представление. Кодировщики можно использовать путем реализации интерфейсов Encoder.Text\u003cT\u003e или Encoder.Binary\u003cT\u003e.

В приведенном ниже коде мы определяем класс Message для кодирования, а в методе encode мы используем Gson для кодирования объекта Java в JSON:

public class Message {
    private String from;
    private String to;
    private String content;
    
    //standard constructors, getters, setters
}
public class MessageEncoder implements Encoder.Text<Message> {

    private static Gson gson = new Gson();

    @Override
    public String encode(Message message) throws EncodeException {
        return gson.toJson(message);
    }

    @Override
    public void init(EndpointConfig endpointConfig) {
        // Custom initialization logic
    }

    @Override
    public void destroy() {
        // Close resources
    }
}

4.2. Декодер

Декодер является противоположностью кодировщика и используется для обратного преобразования данных в объект Java. Декодеры могут быть реализованы с помощью интерфейсов Decoder.Text\u003cT\u003e или Decoder.Binary\u003cT\u003e.

public class MessageDecoder implements Decoder.Text<Message> {

    private static Gson gson = new Gson();

    @Override
    public Message decode(String s) throws DecodeException {
        return gson.fromJson(s, Message.class);
    }

    @Override
    public boolean willDecode(String s) {
        return (s != null);
    }

    @Override
    public void init(EndpointConfig endpointConfig) {
        // Custom initialization logic
    }

    @Override
    public void destroy() {
        // Close resources
    }
}

Как мы видели с кодировщиком, метод декодирования заключается в том, что мы берем JSON, полученный в сообщении, отправленном в конечную точку, и используем Gson для преобразования его в класс Java с именем Message:

4.3. Настройка кодировщика и декодера в конечной точке сервера

@ServerEndpoint( 
  value="/chat/{username}", 
  decoders = MessageDecoder.class, 
  encoders = MessageEncoder.class )

Давайте соберем все вместе, добавив классы, созданные для кодирования и декодирования данных, на уровне класса аннотации @ServerEndpoint:

Каждый раз, когда сообщения отправляются в конечную точку, они будут автоматически преобразовываться в объекты JSON или Java.

5. Заключение

В этой статье мы рассмотрели, что такое Java API для WebSockets и как он может помочь нам в создании таких приложений, как этот чат в реальном времени.

Мы рассмотрели две модели программирования для создания конечной точки: аннотации и программную. Мы определили конечную точку, используя модель аннотации для нашего приложения вместе с методами жизненного цикла.

Кроме того, чтобы иметь возможность обмениваться данными между сервером и клиентом, мы увидели, что нам нужны кодировщики и декодеры для преобразования объектов Java в JSON и наоборот.

API JSR 356 очень прост, а модель программирования на основе аннотаций упрощает создание приложений WebSocket.