Spring Websocket на примере онлайн чата

Сегодня будет практичная но очень интересная тема: использование 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 

Ну а теперь сама работа приложения:

Websocket Spring boot результат работы чата

Я записал видео с пошаговыми инструкциями создания приложение на Spring Websocket. Можете посмотреть его, если все еще не понятно, как сделать чат на веб сокетах:

Это все, что касается Spring Websocket. Тема очень обширная и интересная. Указанный пример не единственный способ работы с данным фреймворком. В нем есть еще очень много полезных методов, которым можно передавать информацию с сервера на клиент и обратно.

3

  1. А как сделать так, чтоб было несколько чатов, и сообщения из одного отправлялись только в конкретный чат? В моем случае чатов может быть неограниченное количество, и я хотел бы иметь возможность сделать подписку на что-то типа /chat/:chatId

    1. Добрый день! Сделать это просто: нужно подписать пользователя на /chat/уникальный_идентификатор_чата. Например: stompClient.subscribe(‘/chat/messages/’ + currentChatId
      currentChatId- может быть например логин или номер чата. Когда передаете сообщение — нужно передавать также и идентификатор чата.
      Далее в джава коде нужно вынимать номер чата из сообщения и отправлять только подписчикам конкретного чата. Сделать это можно так
      Нужно подключить private SimpMessagingTemplate simpMessagingTemplate; — это стандартный класс который идет вместе со спринг вебсокет. И отправлять сообщение таким образом: simpMessagingTemplate.convertAndSend(«/chat/messages/» + topicId, message);
      Погуглите SimpMessagingTemplate и посмотрите детальней как это сделать.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *