Neste tutorial, apresentaremos como configurar Spring websocket com RabbitMQ rodando em um container docker e com o Gateway Zuul. Nós iremos configurar o servidor websocket junto com o gateway zuul, porque ele suporta somente o protocolo HTTP na sua versão 1, e não conseguiria fazer o forward para um microservice específico para websocket.
Adicione as dependências no maven
Estamos utilizando a versão 2.1.3.RELEASE por isso iremos precisar do netty para utilizar o websocket
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
<version>2.3.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-reactor-netty</artifactId>
</dependency>
Criando configuração do Websocket
Um detalhe importante sobre a configuração é definir o atributor setSystemLogin e setSystemPassword, caso não seja configurado seu servidor websocket não irá conseguir se conectar com o rabbitmq, porque o usuário padrão de conexão é ‘guest’, você pode conferir a descrição e solução do erro aqui.
@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {
private final FilterHandshakeHandler filterHandshakeHandler;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/socket")
.setHandshakeHandler(filterHandshakeHandler)
.setAllowedOrigins("*");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.setApplicationDestinationPrefixes("/send")
.enableStompBrokerRelay("/topic")
.setRelayHost("127.0.0.1")
.setRelayPort(61613)
.setSystemLogin("admin") // Utilizado para sobreescrever o usuário guest padrão
.setSystemPasscode("123456")
.setClientLogin("admin")
.setClientPasscode("123456");
}
}
Criando Handler/Filtro para Registrar na Sessão
Nessa configuração estamos definindo um name da sessão de conexão do cliente com o servidor websocket, esse name é gerado de forma randômica com o UUID.
@Component
public class FilterHandshakeHandler extends DefaultHandshakeHandler {
// Custom class for storing principal
@Override
protected Principal determineUser(ServerHttpRequest request,
WebSocketHandler wsHandler,
Map<String, Object> attributes) {
return new StompPrincipal(UUID.randomUUID().toString());
}
public class StompPrincipal implements Principal {
private String name;
public StompPrincipal(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
}
}
Criando Controladores
Vamos criar uma classe chamada Message Controller, nela temos dois exemplos, um no qual iremos enviar e receber mensagens em “grupo” ou seja todos que tiverem inscritos (subscribe) nesse recurso chamado “/chat”, para enviar mensagens a url ficará “/send/chat” e para se inscrever nessa url “/topic/chat”.
Temos também outro exemplo que é o envio de uma mensagem específica para um usuário de acordo com sua sessão, neste caso o usuário irá se inscrever na url “/user/topic/notify”, esse /user é inserido automaticamente pelo spring para a identificação que é um envio específico.
@Controller
public class MessageController {
// Example sending and receiving message in group
@MessageMapping("/chat")
@SendTo("/topic/chat")
public String send(@RequestBody String message) {
return message;
}
// Example sending message to specific user
@SendToUser("/topic/notify")
public String sendSpecific(@Payload String message, Principal principal) {
System.out.println(String.format("principal %s", principal.getName()));
return message;
}
}
Para testarmos esse fluxo de envio de uma mensagem específica para um cliente conectado vamos criar um controlador Rest para enviar internamente essa mensagem. Você pode perceber que utilizamos o SimpMessagingTemplate que permite que enviamos através do método convertAndSendToUser para aquela url definida em @SendToUser.
@RestController
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class MessageRestController {
private final SimpMessagingTemplate simpMessagingTemplate;
@RequestMapping("/new")
public HttpStatus send(@RequestParam String id, @RequestParam String msg) {
simpMessagingTemplate.convertAndSendToUser(id, "/topic/notify", msg);
return HttpStatus.OK;
}
}
Configurando RabbitMQ com docker compose
Crie um arquivo chamado rabbitmq_enabled_plugins com o conteúdo abaixo na raiz do projeto junto com seu arquivo stack.yml no passo seguinte.
[rabbitmq_federation_management,rabbitmq_management,rabbitmq_stomp,rabbitmq_web_stomp,rabbitmq_web_stomp_examples].
Crie um arquivo stack.yml com o conteúdo abaixo e execute com o comando “docker-compose -f stack.yml up -d”.
version: '2.2'
services:
rabbitmq:
image: rabbitmq:3.8.5-management-alpine
hostname: rabbitmq
ports:
- 15672:15672
- 5671:5671
- 5672:5672
- 61613:61613
# restart: always
volumes:
- rabbitmq_data:/var/lib/rabbitmq
- ./rabbitmq_enabled_plugins:/etc/rabbitmq/enabled_plugins
environment:
- "RABBITMQ_HIPE_COMPILE=1"
- RABBITMQ_DEFAULT_VHOST=/
- RABBITMQ_DEFAULT_USER=admin
- RABBITMQ_DEFAULT_PASS=123456
- RABBITMQ_ERLANG_COOKIE=7184f46085f541589ec6bdd03b45e452
healthcheck:
timeout: 5s
interval: 30s
retries: 120
test: "nc -z localhost 5672 || exit 1"
networks:
- app_network
networks:
app_network:
volumes:
rabbitmq_data:
Configurando application.yml
Insira as configurações abaixo no seu application.yml ou application.properties
spring:
application:
name: gateway
rabbitmq:
username: admin
password: 123456
addresses: 127.0.0.1:5672
requested-heartbeat: 40
host: ms-gateway
listener:
default:
default-requeue-rejected: false
zuul:
sensitive-headers: Cookie
routes:
gateway:
path: /socket/**
url: forward:/socket
Testando configurações com Cliente
Agora vamos criar um arquivo chamado index.html, e insira o conteúdo abaixo. Se quisermos testar o envio individual de mensagens precisamos chamar o endpoint “/new” enviando como request param o id e mensagem, onde o id é o ID da sessão que poderá ser obtida na interface html depois que o cliente se conectar ao servidor websocket.
<!DOCTYPE html>
<html>
<head>
<title>Cliente Websocket</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<style>
body {
background: #E5DDD5 url("https://www.toptal.com/designers/subtlepatterns/patterns/sports.png") fixed;
}
.page-header {
background: #1f1f1f;
margin: 0;
padding: 20px 0 10px;
color: #FFFFFF;
position: fixed;
width: 100%;
z-index: 1
}
.main {
height: 100vh;
padding-top: 70px;
}
.chat-log {
padding: 40px 0 114px;
height: auto;
overflow: auto;
}
.chat-log__item {
background: #fafafa;
padding: 10px;
margin: 0 auto 20px;
max-width: 80%;
float: left;
border-radius: 4px;
box-shadow: 0 1px 2px rgba(0,0,0,.1);
clear: both;
}
.chat-log__item.chat-log__item--own {
float: right;
background: #DCF8C6;
text-align: right;
}
.chat-form {
background: #DDDDDD;
padding: 40px 0;
position: fixed;
bottom: 0;
width: 100%;
}
.chat-log__author {
margin: 0 auto .5em;
font-size: 14px;
font-weight: bold;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.5.0/sockjs.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script type="text/javascript">
var stompClient = null;
function setConnected(connected, sessionId = "") {
document.getElementById('connect').disabled = connected;
document.getElementById('disconnect').disabled = !connected;
//document.getElementById('sendMessage').style.visibility = connected ? 'visible' : 'hidden';
document.getElementById('sendMessage').disabled = !connected;
document.getElementById('message').disabled = !connected;
document.getElementById('response-value').innerHTML = '<b>Anote sua sessão: </b>'+sessionId;
document.getElementById('date').innerText = new Date().toLocaleString('pt-BR', {timeZone: 'UTC'});;
document.getElementById('response-item').style.visibility = connected ? 'visible' : 'hidden'
//document.getElementById('session-content').removeAttribute("hidden");
}
function connect() {
stompClient = Stomp.client("ws://localhost:8080/gateway/socket");
stompClient.debug = () => {};
stompClient.connect({}, function(frame) {
setConnected(true, frame.headers['user-name']);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/chat', function(response) {
showNewMessage(JSON.parse(response.body)['message']);
});
stompClient.subscribe('/user/topic/notify', function(response) {
showNewMessage(response.body);
});
});
}
function disconnect() {
stompClient.disconnect();
setConnected(false);
console.log("Disconnected");
var chat = document.querySelector('.chat-log');
while (chat.children.length > 1) {
chat.removeChild(chat.lastChild);
}
}
function sendMessage() {
var input = document.getElementById('message');
var message = input.value;
stompClient.send("/send/chat", {}, JSON.stringify({
'message' : message
}));
input.value = "";
}
function showNewMessage(message) {
var elem = document.querySelector('#response-item');
var clone = elem.cloneNode(true);
var id = 'response-item'+'_' + Math.random().toString(36).substr(2, 9);
clone.id = id;
clone.querySelector("#response-value").innerText = message;
document.querySelector('.chat-log').appendChild(clone);
location.hash = "#" + id;
bot(message);
}
function copy() {
/* Get the text field */
var copyText = document.getElementById("session");
/* Select the text field */
copyText.select();
copyText.setSelectionRange(0, 99999); /* For mobile devices */
/* Copy the text inside the text field */
document.execCommand("copy");
/* Alert the copied text */
alert("Texto copiado: " + copyText.value);
}
function bot(message) {
var link = message.match(/\bhttps?:\/\/\S+/gi)
if(link && link[0].match(/\.(jpeg|jpg|gif|png)$/) && message.includes("bot") && message.includes("background")) {
console.log(link[0]);
document.body.style.background = '#E5DDD5 url('+link[0]+') fixed';
}
}
</script>
</head>
<body>
<noscript>
<h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable Javascript and reload this page!</h2>
</noscript>
<header class="page-header">
<div class="container ">
<div class="d-flex justify-content-between">
<div>
<h2>Chat socket</h2>
</div>
<div class="d-flex align-items-center justify-content-end">
<button class="btn btn-success" id="connect" onclick="connect();">Connect</button>
<button class="btn btn-danger ml-2" id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>
</div>
</div>
</div>
</header>
<div class="main">
<div class="container ">
<div class="chat-log">
<div class="chat-log__item chat-log__item--own" id="response-item" style="visibility: hidden;">
<!--<h3 class="chat-log__author">Anote sua sessão: </h3>-->
<small id="date">14:30</small>
<div class="chat-log__message" id="response-value">BRB</div>
</div>
</div>
</div>
<div class="chat-form">
<div class="container ">
<div class="row">
<div class="col-sm-10 col-xs-8">
<input type="text" class="form-control" id="message" placeholder="Message" disabled/>
</div>
<div class="col-sm-2 col-xs-4">
<button type="button" id="sendMessage" onclick="sendMessage();" class="btn btn-success btn-block" disabled>Enviar</button>
</div>
</div>
</div>
</div>
</div>
<!-- jQuery first, then Popper.js, then Bootstrap JS -->
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
</body>
</html>
Dúvidas?
Você tem outras dúvidas? Deixe seu feedback nos comentários abaixo. Bom, espero que essa dica tenha sido útil.