
Сегодня будет практичная но очень интересная тема: использование Websocket в Spring Boot приложении на примере онлайн чата.
Очень часто бывает необходимым получать данные с сервера без перезагрузки страницы. Для этого используют технологию AJAX. Но что делать если клиент не знает, когда будет ответ от сервера?
Можно например делать запрос на сервер каждые несколько секунд и смотреть — готов ли сервер дать нам информацию. Такой подход не самое хорошее решение. Особенно когда речь заходит об высоконагруженных системах с большим количеством пользователей. Тогда на помощь приходят веб сокеты.
Использование технологии веб сокетов без сторонних фреймворков или библиотек — занятие не самое удобное. Поэтому в данной статье мы рассмотрим Spring Websocket с библиотекой StompJS. Ведь нужно будет не только передавать сообщения с сервера, но и принимать их на клиенте.
На примерах все выглядит значительно проще, чем в теории. Современное программирование стало настолько простым, что скоро Вам нужно будет нажимать Ctrl+Space и Eclipse будет сам писать код))
Первое что нужно сделать — создать простое Spring Boot приложение. Далее нужно добавить необходимые зависимости для веб сокетов:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
Для серверной части этого достаточно.
Напишем настройки для WebSocket:
import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfiguration extends AbstractWebSocketMessageBrokerConfigurer { public void configureMessageBroker(MessageBrokerRegistry confi) { confi.enableSimpleBroker("/chat"); confi.setApplicationDestinationPrefixes("/app"); } public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/chat-messaging").withSockJS(); } }
Не пугаться!!! Здесь все просто. Аннотация @Configuration говорит, что это конфигурационный класс спринг приложения. @EnableWebSocketMessageBroker — указывает, что мы разрешаем работу с веб сокетами. Далее в методе configureMessageBroker мы указываем префиксы и адреса нашего веб сокет эндпоинта. В registerStompEndpoints подключается конечный адрес, по котором мы будем слушать и передавать сообщения. .withSockJS() — говорит, что будет использоваться библиотека SockJS которая является оберткой для стандартных веб сокетов и обеспечивает более удобное их использование.
Это все настройки, которые нужны для передачи сообщение по Websocket.
Для того, чтобы приложение слушало входящие сообщения и посылало исходящие, необходимо воспользоваться контроллером, который мы привыкли видеть в Spring MVC приложениях. Только вместо аннотаций @GetMapping и т.д. Нужно воспользоваться аннотациями, которые предназначены для Spring Websocket:
import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Controller; import com.javamaster.domain.Message; @Controller public class ChatController { @MessageMapping("/message") @SendTo("/chat/messages") public Message getMessages(Message message) { System.out.println(message); return message; } }
@MessageMapping — урл, по которому будет слушать наш сервер. @SendTo — урл, куда он отправит сообщение. Message — простой объект:
package com.javamaster.domain; public class Message { private String from; private String message; public String getFrom() { return from; } public void setFrom(String from) { this.from = from; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } @Override public String toString() { return "Message [from=" + from + ", message=" + message + "]"; } }
Можно передавать как объекты, так и список объектов. Можно просто строки или числа передавать. Здесь разницы нет. Для наглядности и предметной области я создал простой объект в которого только два поля: from — будет содержать имя адресата и message — само сообщение.
Вы же можете изменить объект как только пожелаете.
Это все, что касается серверной части. Если запустить приложение — оно будет слушать входящие сообщения и как только поймает хоть одно — сразу отправит его на клиент.
На этом можно было бы и закончить статью, но как по мне — нужно увидеть приложение в действии.
Давайте напишем клиентскую часть. Будет использовано HTML, CSS, Javascript, JQuery. Если Вы не знакомы с этими технологиями и языками программирования — не спешите закрывать статью. Код будет максимально понятным и простым. Для подключения необходимых библиотек, вместо того, чтобы качать я подключу их через Maven.
Для полноценной работы с веб сокетами мне нужны быблиотеки:
- sockjs-client;
- stomp-websocket.
Для оформления и стилизации кода я подключу bootstrap и jquery.
Мой полный файл pom.xml теперь выглядит так:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.javamaster</groupId> <artifactId>websocketchat</artifactId> <version>0.0.1-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.8.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>sockjs-client</artifactId> <version>1.0.2</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>stomp-websocket</artifactId> <version>2.3.3</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>bootstrap</artifactId> <version>3.3.7</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <properties> <java.version>1.8</java.version> </properties> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Для того, чтобы подключиться к веб сокетам на сервере нужно создать экземпляр класса SockJS: var socket = new SockJS(‘/chat-messaging‘);
В конструкторе указывается адрес эндпоинта, который мы указали в классе настроек WebSocketConfiguration. Так мы позволим нашему клиенту установить соединение с сервером. Для того, чтобы использовать сокеты через библиотеку Stomp — создадим stopClient: stompClient = Stomp.over(socket);
Чтобы не мучить Вас утомительными комментариями даю полный код клиентской части. Файл script.js имеет вид:
function connect() { var socket = new SockJS('/chat-messaging'); stompClient = Stomp.over(socket); stompClient.connect({}, function(frame) { console.log("connected: " + frame); stompClient.subscribe('/chat/messages', function(response) { var data = JSON.parse(response.body); draw("left", data.message); }); }); } function draw(side, text) { console.log("drawing..."); var $message; $message = $($('.message_template').clone().html()); $message.addClass(side).find('.text').html(text); $('.messages').append($message); return setTimeout(function () { return $message.addClass('appeared'); }, 0); } function disconnect(){ stompClient.disconnect(); } function sendMessage(){ stompClient.send("/app/message", {}, JSON.stringify({'message': $("#message_input_value").val()})); }
После того, как мы создали stompClient — вызывается метод connect в котором клиент подписывается на урл /chat/messages. Таким образом он будет слушать все, что придет по этому адресу без перезагрузки страницы. С информацией, которая придет от сервера можно делать все что угодно. В данном случае я ее распарсиваю var data = JSON.parse(response.body); и передаю в метод draw(«left», data.message);.
Метод draw нас в этой теме не интересует. Он выполняет функцию красивого наполнения странички информацией. Более интересен метод sendMessage. Когда он вызывается — идет отправление сообщение по адресу /app/message. Если Вы вернетесь к Java коду то увидите, что app — это destination prefix (смотримWebSocketConfiguration), а message конечный адрес, по котором слушает контроллер.
Для полноты картины я добавлю код файла index.html в который подключен наш script.js:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Insert title here</title> <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet"> <link href="/style.css" rel="stylesheet"> <script src="/webjars/jquery/jquery.min.js"></script> <script src="/webjars/sockjs-client/sockjs.min.js"></script> <script src="/webjars/stomp-websocket/stomp.min.js"></script> <script src="/script.js"></script> </head> <body> <h1>Simple chat</h1> <div class="chat_window"> <div class="top_menu"> <div class="buttons"> <div class="button close"></div> <div class="button minimize"></div> <div class="button maximize"></div> </div> <div class="title">Chat</div> </div> <ul class="messages"></ul> <div class="bottom_wrapper clearfix"> <div class="message_input_wrapper"> <input id="message_input_value" class="message_input" placeholder="Type your message here..." /> </div> <div class="send_message"> <div class="icon"></div> </div> <button onclick="connect()">Connect to chat</button> <button onclick="sendMessage()" class="text">Send</button> <button onclick="disconnect()">Disconnect from chat</button> </div> </div> <div id="message_template" class="message_template"> <li class="message"><div class="avatar"></div> <div class="text_wrapper"> <div class="text"></div> </div></li> </div> </body> </html>
Стили оформления CSS файл style.css:
* { box-sizing: border-box; } body { background-color: #edeff2; font-family: "Calibri", "Roboto", sans-serif; } .chat_window { position: absolute; width: calc(100% - 20px); max-width: 800px; height: 500px; border-radius: 10px; background-color: #fff; left: 50%; top: 50%; transform: translateX(-50%) translateY(-50%); box-shadow: 0 10px 20px rgba(0, 0, 0, 0.15); background-color: #f8f8f8; overflow: hidden; } .top_menu { background-color: #fff; width: 100%; padding: 20px 0 15px; box-shadow: 0 1px 30px rgba(0, 0, 0, 0.1); } .top_menu .buttons { margin: 3px 0 0 20px; position: absolute; } .top_menu .buttons .button { width: 16px; height: 16px; border-radius: 50%; display: inline-block; margin-right: 10px; position: relative; } .top_menu .buttons .button.close { background-color: #f5886e; } .top_menu .buttons .button.minimize { background-color: #fdbf68; } .top_menu .buttons .button.maximize { background-color: #a3d063; } .top_menu .title { text-align: center; color: #bcbdc0; font-size: 20px; } .messages { position: relative; list-style: none; padding: 20px 10px 0 10px; margin: 0; height: 347px; overflow: scroll; } .messages .message { clear: both; overflow: hidden; margin-bottom: 20px; transition: all 0.5s linear; opacity: 0; } .messages .message.left .avatar { background-color: #f5886e; float: left; } .messages .message.left .text_wrapper { background-color: #ffe6cb; margin-left: 20px; } .messages .message.left .text_wrapper::after, .messages .message.left .text_wrapper::before { right: 100%; border-right-color: #ffe6cb; } .messages .message.left .text { color: #c48843; } .messages .message.right .avatar { background-color: #fdbf68; float: right; } .messages .message.right .text_wrapper { background-color: #c7eafc; margin-right: 20px; float: right; } .messages .message.right .text_wrapper::after, .messages .message.right .text_wrapper::before { left: 100%; border-left-color: #c7eafc; } .messages .message.right .text { color: #45829b; } .messages .message.appeared { opacity: 1; } .messages .message .avatar { width: 60px; height: 60px; border-radius: 50%; display: inline-block; } .messages .message .text_wrapper { display: inline-block; padding: 20px; border-radius: 6px; width: calc(100% - 85px); min-width: 100px; position: relative; } .messages .message .text_wrapper::after, .messages .message .text_wrapper:before { top: 18px; border: solid transparent; content: " "; height: 0; width: 0; position: absolute; pointer-events: none; } .messages .message .text_wrapper::after { border-width: 13px; margin-top: 0px; } .messages .message .text_wrapper::before { border-width: 15px; margin-top: -2px; } .messages .message .text_wrapper .text { font-size: 18px; font-weight: 300; } .bottom_wrapper { position: relative; width: 100%; background-color: #fff; padding: 20px 20px; position: absolute; bottom: 0; } .bottom_wrapper .message_input_wrapper { display: inline-block; height: 50px; border-radius: 25px; border: 1px solid #bcbdc0; width: calc(100% - 160px); position: relative; padding: 0 20px; } .bottom_wrapper .message_input_wrapper .message_input { border: none; height: 100%; box-sizing: border-box; width: calc(100% - 40px); position: absolute; outline-width: 0; color: gray; } .bottom_wrapper .send_message { width: 140px; height: 50px; display: inline-block; border-radius: 50px; background-color: #a3d063; border: 2px solid #a3d063; color: #fff; cursor: pointer; transition: all 0.2s linear; text-align: center; float: right; } .bottom_wrapper .send_message:hover { color: #a3d063; background-color: #fff; } .bottom_wrapper .send_message .text { font-size: 18px; font-weight: 300; display: inline-block; line-height: 48px; } .message_template { display: none; }
Эти файлы нужно поместить в src/main/resources/static тогда Spring Boot будет по умолчанию открывать файл index.html в котором и будет наш чат с подключенными стилями и js.
Полный код проекта Вы найдете по ссылке: https://github.com/caligula95/web-socket-chat
Ну а теперь сама работа приложения:
Я записал видео с пошаговыми инструкциями создания приложение на Spring Websocket. Можете посмотреть его, если все еще не понятно, как сделать чат на веб сокетах:
Это все, что касается Spring Websocket. Тема очень обширная и интересная. Указанный пример не единственный способ работы с данным фреймворком. В нем есть еще очень много полезных методов, которым можно передавать информацию с сервера на клиент и обратно.
А как сделать так, чтоб было несколько чатов, и сообщения из одного отправлялись только в конкретный чат? В моем случае чатов может быть неограниченное количество, и я хотел бы иметь возможность сделать подписку на что-то типа /chat/:chatId
Добрый день! Сделать это просто: нужно подписать пользователя на /chat/уникальный_идентификатор_чата. Например: stompClient.subscribe(‘/chat/messages/’ + currentChatId
currentChatId- может быть например логин или номер чата. Когда передаете сообщение — нужно передавать также и идентификатор чата.
Далее в джава коде нужно вынимать номер чата из сообщения и отправлять только подписчикам конкретного чата. Сделать это можно так
Нужно подключить private SimpMessagingTemplate simpMessagingTemplate; — это стандартный класс который идет вместе со спринг вебсокет. И отправлять сообщение таким образом: simpMessagingTemplate.convertAndSend(«/chat/messages/» + topicId, message);
Погуглите SimpMessagingTemplate и посмотрите детальней как это сделать.
Спасибо, все получилось!