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. Создание чата с использованием веб-сокетов
Мы создадим очень простое приложение, похожее на чат. Любой пользователь сможет открыть чат из любого браузера, ввести свое имя, авторизоваться в чате и начать общение со всеми, кто подключился к чату.
Мы начнем с добавления последней зависимости в файл 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 и сообщениями проверки работоспособности (пинг-понг), как определено в спецификации:
Text
: любые текстовые данные (java.lang.String
, примитивы или их эквивалентные классы-оболочки)Двоичные
: двоичные данные (например, аудио, изображение и т. д.), представленныеjava.nio.ByteBuffer
илиbyte[]
(массив байтов) .Объекты Java
: API позволяет работать с собственными представлениями (объект Java) в вашем коде и использовать настраиваемые преобразователи (кодировщики/декодеры) для преобразования их в совместимые форматы (текстовые, двоичные), разрешенные протоколом WebSocket.Ping-Pong
:javax.websocket.PongMessage
— это подтверждение, отправленное узлом WebSocket в ответ на запрос проверки работоспособности (ping).
Для нашего приложения мы будем использовать объекты Java.
Мы создадим классы для кодирования и декодирования сообщений.
4.1. Кодер
Кодер берет объект Java и создает типичное представление, подходящее для передачи в виде сообщения, такого как JSON, XML или двоичное представление. Кодировщики можно использовать путем реализации интерфейсов Encoder.Text<T>
или Encoder.Binary<T>
.
В приведенном ниже коде мы определяем класс 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<T>
или Decoder.Binary<T>
.
Как мы видели с кодировщиком, метод декодирования
— это то, где мы берем JSON, полученный в сообщении, отправленном в конечную точку, и используем Gson для преобразования его в класс Java с именем Message:
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
}
}
4.3. Настройка кодировщика и декодера в конечной точке сервера
Давайте соберем все вместе, добавив классы, созданные для кодирования и декодирования данных, в аннотацию уровня класса @ServerEndpoint
:
@ServerEndpoint(
value="/chat/{username}",
decoders = MessageDecoder.class,
encoders = MessageEncoder.class )
Каждый раз, когда сообщения отправляются на конечную точку, они автоматически преобразуются в объекты JSON или Java.
5. Вывод
В этой статье мы рассмотрели, что такое Java API для WebSockets и как он может помочь нам в создании таких приложений, как этот чат в реальном времени.
Мы видели две модели программирования для создания конечной точки: аннотации и программную. Мы определили конечную точку, используя модель аннотации для нашего приложения вместе с методами жизненного цикла.
Кроме того, чтобы иметь возможность обмениваться данными между сервером и клиентом, мы увидели, что нам нужны кодировщики и декодеры для преобразования объектов Java в JSON и наоборот.
API JSR 356 очень прост, а модель программирования на основе аннотаций упрощает создание приложений WebSocket.
Чтобы запустить приложение, которое мы создали в примере, все, что нам нужно сделать, это развернуть файл войны на веб-сервере и перейти по URL-адресу: http://localhost:8080/java-websocket/.
Ссылку на репозиторий вы можете найти здесь .