Pular para o conteúdo principal

Capítulo 5: WebSockets e Comunicação em Tempo Real

Objetivos do Capítulo​

  • Compreender o protocolo WebSocket
  • Configurar Spring WebSocket com STOMP
  • Implementar um sistema de mensagens em tempo real
  • Gerenciar autenticação WebSocket
  • Criar aplicações de chat e notificações

1. Introdução aos WebSockets​

1.1 HTTP vs WebSocket​

HTTP (Request-Response):
Client ──GET──> Server
Client <──200── Server
Client ──GET──> Server
Client <──200── Server
(Connexion fermée après chaque échange)

WebSocket (Bidirectionnel):
Client ──Handshake──> Server
<──Upgrade────
←───────────→ (Connexion persistante)
←───────────→ (Messages bidirectionnels)

1.2 Quando Usar WebSocket​

Caso de UsoSolução
Chat em tempo realWebSocket
Notificações pushWebSocket
Jogos multiplayerWebSocket
Trading em tempo realWebSocket
Dashboards ao vivoWebSocket
Dados frequentes (polling)WebSocket
API REST padrãoHTTP

1.3 Protocolo STOMP​

STOMP (Simple Text Oriented Messaging Protocol) fornece uma camada de abstração sobre WebSocket:

STOMP Frame:
COMMAND
header1:value1
header2:value2

Body^@

Comandos principais:

  • CONNECT - Estabelecer conexão
  • SUBSCRIBE - Inscrever-se em um destino
  • SEND - Enviar uma mensagem
  • MESSAGE - Receber uma mensagem
  • DISCONNECT - Fechar conexão

2. Configuração Spring WebSocket​

2.1 Dependências Maven​

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<!-- Para fallback SockJS -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>sockjs-client</artifactId>
<version>1.5.1</version>
</dependency>

<!-- Para STOMP -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>stomp-websocket</artifactId>
<version>2.3.4</version>
</dependency>
</dependencies>

2.2 Configuração WebSocket​

package com.example.websocket.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// Prefixo para destinos que o servidor envia para os clientes
// /topic = broadcast para todos os assinantes
// /queue = mensagem privada para um usuário
registry.enableSimpleBroker("/topic", "/queue");

// Prefixo para destinos que os clientes enviam para o servidor
registry.setApplicationDestinationPrefixes("/app");

// Prefixo para mensagens privadas para um usuário específico
registry.setUserDestinationPrefix("/user");
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// Endpoint WebSocket com fallback SockJS
registry.addEndpoint("/ws")
.setAllowedOrigins("http://localhost:3000", "http://localhost:8080")
.withSockJS();
}
}

3. Controller WebSocket​

3.1 DTOs de Mensagem​

package com.example.websocket.dto;

public class ChatMessage {

private String sender;
private String content;
private String timestamp;
private MessageType type;

public enum MessageType {
CHAT, JOIN, LEAVE
}

public ChatMessage() {}

public ChatMessage(String sender, String content, MessageType type) {
this.sender = sender;
this.content = content;
this.type = type;
this.timestamp = java.time.LocalDateTime.now().toString();
}

// Getters e Setters
public String getSender() { return sender; }
public void setSender(String sender) { this.sender = sender; }

public String getContent() { return content; }
public void setContent(String content) { this.content = content; }

public String getTimestamp() { return timestamp; }
public void setTimestamp(String timestamp) { this.timestamp = timestamp; }

public MessageType getType() { return type; }
public void setType(MessageType type) { this.type = type; }
}

3.2 Controller​

package com.example.websocket.controller;

import com.example.websocket.dto.ChatMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.stereotype.Controller;

@Controller
public class ChatController {

private static final Logger log = LoggerFactory.getLogger(ChatController.class);

// Cliente envia para /app/chat.sendMessage
// Servidor faz broadcast para /topic/public
@MessageMapping("/chat.sendMessage")
@SendTo("/topic/public")
public ChatMessage sendMessage(@Payload ChatMessage message) {
log.info("Mensagem recebida: {} de {}", message.getContent(), message.getSender());
return message;
}

// Gerenciamento da entrada de um usuário
@MessageMapping("/chat.addUser")
@SendTo("/topic/public")
public ChatMessage addUser(@Payload ChatMessage message,
SimpMessageHeaderAccessor headerAccessor) {

// Armazenar o username na sessão WebSocket
headerAccessor.getSessionAttributes().put("username", message.getSender());

log.info("Usuário entrou: {}", message.getSender());

message.setType(ChatMessage.MessageType.JOIN);
return message;
}
}

3.3 Envio de Mensagens a partir de um Service​

package com.example.websocket.service;

import com.example.websocket.dto.ChatMessage;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;

@Service
public class NotificationService {

private final SimpMessagingTemplate messagingTemplate;

public NotificationService(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}

// Broadcast para todos os assinantes de /topic/notifications
public void sendBroadcastNotification(String message) {
ChatMessage notification = new ChatMessage(
"System",
message,
ChatMessage.MessageType.CHAT
);

messagingTemplate.convertAndSend("/topic/notifications", notification);
}

// Mensagem privada para um usuário específico
public void sendPrivateMessage(String username, String message) {
ChatMessage privateMessage = new ChatMessage(
"System",
message,
ChatMessage.MessageType.CHAT
);

// Envia para /user/{username}/queue/private
messagingTemplate.convertAndSendToUser(
username,
"/queue/private",
privateMessage
);
}

// Enviar para um destino específico
public void sendToRoom(String roomId, ChatMessage message) {
messagingTemplate.convertAndSend("/topic/room/" + roomId, message);
}
}

4. Event Listeners​

4.1 Gerenciamento de Conexões/Desconexões​

package com.example.websocket.listener;

import com.example.websocket.dto.ChatMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectedEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;

@Component
public class WebSocketEventListener {

private static final Logger log = LoggerFactory.getLogger(WebSocketEventListener.class);

private final SimpMessageSendingOperations messagingTemplate;

public WebSocketEventListener(SimpMessageSendingOperations messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}

@EventListener
public void handleWebSocketConnectListener(SessionConnectedEvent event) {
log.info("Nova conexão WebSocket estabelecida");
}

@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());

String username = (String) headerAccessor.getSessionAttributes().get("username");

if (username != null) {
log.info("Usuário desconectado: {}", username);

ChatMessage leaveMessage = new ChatMessage(
username,
username + " saiu do chat",
ChatMessage.MessageType.LEAVE
);

messagingTemplate.convertAndSend("/topic/public", leaveMessage);
}
}
}

5. Cliente JavaScript​

5.1 Template HTML​

<!DOCTYPE html>
<html>
<head>
<title>WebSocket Chat</title>
<script src="/webjars/sockjs-client/sockjs.min.js"></script>
<script src="/webjars/stomp-websocket/stomp.min.js"></script>
<style>
.chat-container { max-width: 600px; margin: 0 auto; }
.messages { height: 400px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; }
.message { padding: 5px; margin: 5px 0; }
.join-message { color: green; }
.leave-message { color: red; }
input[type="text"] { width: 70%; padding: 10px; }
button { padding: 10px 20px; }
</style>
</head>
<body>
<div class="chat-container">
<div id="login-page">
<input type="text" id="username" placeholder="Digite seu nome">
<button onclick="connect()">Entrar no Chat</button>
</div>

<div id="chat-page" style="display: none;">
<div class="messages" id="messages"></div>
<input type="text" id="message" placeholder="Digite uma mensagem...">
<button onclick="sendMessage()">Enviar</button>
</div>
</div>

<script>
let stompClient = null;
let username = null;

function connect() {
username = document.getElementById('username').value.trim();
if (!username) return;

// Conexão SockJS
const socket = new SockJS('/ws');
stompClient = Stomp.over(socket);

stompClient.connect({}, onConnected, onError);
}

function onConnected() {
document.getElementById('login-page').style.display = 'none';
document.getElementById('chat-page').style.display = 'block';

// Inscrever-se no tópico público
stompClient.subscribe('/topic/public', onMessageReceived);

// Inscrever-se em mensagens privadas
stompClient.subscribe('/user/queue/private', onPrivateMessage);

// Anunciar entrada
stompClient.send('/app/chat.addUser', {}, JSON.stringify({
sender: username,
type: 'JOIN'
}));
}

function onError(error) {
console.error('Erro WebSocket:', error);
alert('Não foi possível conectar ao servidor WebSocket');
}

function sendMessage() {
const messageInput = document.getElementById('message');
const content = messageInput.value.trim();

if (content && stompClient) {
stompClient.send('/app/chat.sendMessage', {}, JSON.stringify({
sender: username,
content: content,
type: 'CHAT'
}));

messageInput.value = '';
}
}

function onMessageReceived(payload) {
const message = JSON.parse(payload.body);
displayMessage(message);
}

function onPrivateMessage(payload) {
const message = JSON.parse(payload.body);
message.content = '[Privado] ' + message.content;
displayMessage(message);
}

function displayMessage(message) {
const messagesDiv = document.getElementById('messages');
const messageElement = document.createElement('div');
messageElement.className = 'message';

if (message.type === 'JOIN') {
messageElement.className += ' join-message';
messageElement.textContent = message.sender + ' entrou no chat';
} else if (message.type === 'LEAVE') {
messageElement.className += ' leave-message';
messageElement.textContent = message.sender + ' saiu do chat';
} else {
messageElement.textContent = message.sender + ': ' + message.content;
}

messagesDiv.appendChild(messageElement);
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}

// Enviar com Enter
document.getElementById('message').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});
</script>
</body>
</html>

6. Autenticação WebSocket​

6.1 Channel Interceptor​

package com.example.websocket.config;

import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

@Component
public class AuthChannelInterceptor implements ChannelInterceptor {

private final JwtService jwtService;

public AuthChannelInterceptor(JwtService jwtService) {
this.jwtService = jwtService;
}

@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor
.getAccessor(message, StompHeaderAccessor.class);

if (StompCommand.CONNECT.equals(accessor.getCommand())) {
// Extrair o token do header
String token = accessor.getFirstNativeHeader("Authorization");

if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);

if (jwtService.validateToken(token)) {
String username = jwtService.extractUsername(token);

UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
username, null, Collections.emptyList()
);

accessor.setUser(auth);
}
}
}

return message;
}
}

6.2 Registrar o Interceptor​

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

private final AuthChannelInterceptor authInterceptor;

@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(authInterceptor);
}

// ... outras configurações
}

7. Rooms e Canais​

7.1 Controller com Rooms​

@Controller
public class RoomController {

@MessageMapping("/room/{roomId}/send")
@SendTo("/topic/room/{roomId}")
public ChatMessage sendToRoom(
@DestinationVariable String roomId,
@Payload ChatMessage message) {

log.info("Mensagem para sala {}: {}", roomId, message.getContent());
return message;
}

@MessageMapping("/room/{roomId}/join")
@SendTo("/topic/room/{roomId}")
public ChatMessage joinRoom(
@DestinationVariable String roomId,
@Payload ChatMessage message,
SimpMessageHeaderAccessor headerAccessor) {

// Armazenar a sala na sessão
headerAccessor.getSessionAttributes().put("currentRoom", roomId);

message.setType(ChatMessage.MessageType.JOIN);
message.setContent(message.getSender() + " entrou na sala " + roomId);

return message;
}
}

7.2 Cliente JavaScript - Rooms​

function joinRoom(roomId) {
// Cancelar inscrição da sala anterior
if (currentRoomSubscription) {
currentRoomSubscription.unsubscribe();
}

// Inscrever-se na nova sala
currentRoomSubscription = stompClient.subscribe(
'/topic/room/' + roomId,
onRoomMessage
);

// Anunciar entrada
stompClient.send('/app/room/' + roomId + '/join', {}, JSON.stringify({
sender: username,
type: 'JOIN'
}));

currentRoom = roomId;
}

function sendToRoom(content) {
stompClient.send('/app/room/' + currentRoom + '/send', {}, JSON.stringify({
sender: username,
content: content,
type: 'CHAT'
}));
}

8. Resumo​

Arquitetura​

Client (Browser)
|
| SockJS / WebSocket
|
Spring WebSocket
|
├── STOMP Protocol
|
├── Message Broker (/topic, /queue)
|
└── @MessageMapping Controllers

Prefixos de Destino​

PrefixoUso
/appCliente para servidor
/topicBroadcast (1-para-muitos)
/queuePonto-a-ponto (1-para-1)
/userMensagens privadas

Boas Práticas​

  1. Usar SockJS para compatibilidade
  2. Implementar autenticação
  3. Gerenciar desconexões corretamente
  4. Limitar o tamanho das mensagens
  5. Monitorar conexões ativas
  6. Utilizar rooms para escalabilidade

Ir para os Exercícios Práticos

Voltar ao Módulo 14