«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.