Parte 1: Servidor com Spring Boot
Para impleemntar WebSocket no Spring Boot, baseado na especificação JSR-356, utilize os componentes centrais: a anotação @ServerEndpoint para definir endpoints, ServerEndpointExporter para registro automático e a classe Session para gerenciar conexões.
1. Adicionar Dependência
Inclua no arquivo pom.xml a dependência do starter WebSocket:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2. Classe de Configuração
Para aplicações com Tomcat embutido, registre um bean para exportar endpoints:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WsConfig {
@Bean
public ServerEndpointExporter wsEndpointExporter() {
return new ServerEndpointExporter();
}
}
3. Implementação do Endpoint
Crie uma classe para lidar com eventos de conexão e mensagens. Este exemplo inclui gerenciamento de usuários e envio de mensagens em grupo:
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
@Slf4j
@Component
@ServerEndpoint("/comms/{uid}")
public class WsEndpoint {
private static final CopyOnWriteArraySet<WsEndpoint> activeEndpoints = new CopyOnWriteArraySet<>();
private static final ConcurrentHashMap<String, Session> uidSessionMap = new ConcurrentHashMap<>();
private Session currentSession;
private String uid;
@OnOpen
public void handleOpen(Session session, @PathParam("uid") String uid) {
this.currentSession = session;
this.uid = uid;
activeEndpoints.add(this);
uidSessionMap.put(uid, session);
log.info("Conexão estabelecida: " + uid + ", total ativo: " + activeEndpoints.size());
try {
transmitMessage("Bem-vindo! Seu ID é: " + uid);
} catch (IOException ex) {
log.error("Falha ao enviar mensagem de boas-vindas", ex);
}
}
@OnClose
public void handleClose() {
activeEndpoints.remove(this);
if (uid != null) {
uidSessionMap.remove(uid);
}
log.info("Conexão encerrada. Usuários online: " + activeEndpoints.size());
}
@OnMessage
public void handleMessage(String message, Session session) {
log.info("Mensagem de " + uid + ": " + message);
try {
broadcast("Usuário " + uid + " disse: " + message);
} catch (IOException ex) {
log.error("Falha no broadcast", ex);
}
}
@OnError
public void handleError(Session session, Throwable error) {
log.error("Erro na sessão", error);
}
private void transmitMessage(String msg) throws IOException {
if (currentSession != null && currentSession.isOpen()) {
currentSession.getBasicRemote().sendText(msg);
}
}
public static void broadcast(String msg) throws IOException {
for (WsEndpoint ep : activeEndpoints) {
synchronized (ep) {
if (ep.currentSession != null && ep.currentSession.isOpen()) {
ep.currentSession.getBasicRemote().sendText(msg);
}
}
}
}
public static void sendToUser(String msg, String targetUid) throws IOException {
Session target = uidSessionMap.get(targetUid);
if (target != null && target.isOpen()) {
target.getBasicRemote().sendText(msg);
} else {
log.warn("Usuário offline ou sessão inválida: " + targetUid);
}
}
}
Pontos importantes: CopyOnWriteArraySet garante segurança em ambiente multi-thread; @PathParam captura parâmetros da URL; a classe Session é essencial para envio de dados via sendText().
Parte 2: Cliente com JavaScript Puro
O cliente utiliza a API WebSocket nativa do navegador, sem dependências externas.
Exemplo HTML/JS Básico
Crie um arquivo index.html com a seguinte estrutura:
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<title>Teste de WebSocket Nativo</title>
<style>
#registro { width: 600px; height: 400px; border: 1px solid #ddd; overflow-y: auto; padding: 10px; font-family: monospace; }
.msg-recebida { color: green; }
.msg-enviada { color: blue; }
.info { color: #666; font-style: italic; }
</style>
</head>
<body>
<h2>Demo de Chat com WebSocket</h2>
<div>
ID do Usuário: <input type="text" id="campoUid" value="usuario_001">
<button onclick="iniciarConexao()">Conectar</button>
<button onclick="encerrarConexao()" disabled id="btnDesconectar">Desconectar</button>
</div>
<br>
<div>
<input type="text" id="campoMsg" placeholder="Digite sua mensagem..." style="width: 400px;">
<button onclick="enviarMensagem()" id="btnEnviar" disabled>Enviar</button>
<button onclick="enviarPrivado()">Enviar Privado</button>
</div>
<br>
<div id="registro"></div>
<script>
let socket = null;
const registroDiv = document.getElementById('registro');
function registrar(msg, classe = 'info') {
const p = document.createElement('div');
p.className = classe;
const horario = new Date().toLocaleTimeString();
p.innerText = `[${horario}] ${msg}`;
registroDiv.appendChild(p);
registroDiv.scrollTop = registroDiv.scrollHeight;
}
function iniciarConexao() {
const uid = document.getElementById('campoUid').value;
if (!uid) { alert("Insira um ID de usuário"); return; }
const urlWs = `ws://localhost:8080/comms/${uid}`;
registrar(`Conectando a ${urlWs}...`);
socket = new WebSocket(urlWs);
socket.onopen = function() {
registrar('Conexão bem-sucedida!', 'info');
document.getElementById('btnDesconectar').disabled = false;
document.getElementById('btnEnviar').disabled = false;
};
socket.onmessage = function(evento) {
registrar('Recebido: ' + evento.data, 'msg-recebida');
try {
const dados = JSON.parse(evento.data);
console.log('Dados parseados:', dados);
} catch (e) { /* Tratar se não for JSON */ }
};
socket.onclose = function() {
registrar('Conexão fechada', 'info');
document.getElementById('btnDesconectar').disabled = true;
document.getElementById('btnEnviar').disabled = true;
socket = null;
};
socket.onerror = function(erro) {
registrar('Erro: ' + erro, 'info');
};
}
function encerrarConexao() {
if (socket) socket.close();
}
function enviarMensagem() {
if (socket && socket.readyState === WebSocket.OPEN) {
const campo = document.getElementById('campoMsg');
const texto = campo.value;
if (!texto) return;
socket.send(texto);
registrar('Enviado: ' + texto, 'msg-enviada');
campo.value = '';
} else {
registrar('Conexão não ativa', 'info');
}
}
function enviarPrivado() {
if (socket && socket.readyState === WebSocket.OPEN) {
const alvo = prompt("ID do destinatário:");
const conteudo = prompt("Mensagem:");
if (alvo && conteudo) {
const payload = JSON.stringify({
tipo: 'PRIVADO',
destinatario: alvo,
mensagem: conteudo
});
socket.send(payload);
registrar(`Privado para ${alvo}: ${conteudo}`, 'msg-enviada');
}
}
}
window.onbeforeunload = function() {
if (socket) socket.close();
};
</script>
</body>
</html>
Parte 3: Integração e Soluções Comuns
Teste e Depuração
Para testar: inicie a aplicação Spring Boot, abra o index.html em dois navegadores, insira IDs diferentes e envie mensagens. Observe os logs no servidor para verificar a comunicação.
Problemas de CORS (Cross-Origin)
Se o frontend e backend estiverem em domínios diferentes, o hansdhake do WebSocket pode falhar. Soluções incluem configurar CORS no Spring Boot via ServerEndpointConfig ou usar um proxy reverso no Nginx. Exemplo de configuração no Nginx:
location /comms/ {
proxy_pass http://backend_server;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400;
}
Considerações para Produção
- WSS Criptografado: Em produção, utilize
wss://com SSL configurado no Nginx. - Ambientes em Cluster: A implementação acima é para único servidor. Para múltiplos nós, integre um sistema de mensagens como Redis Pub/Sub para sincronizar mensagens entre instâncias.
- Keep-Alive: Implemente lógica de ping/pong na aplicação para detectar desconexões inativas.
Reconexão Automática no Cliente
Adicione uma estratégia de reconexão com backoff exponencial no JavaScript:
let tentativasReconexao = 0;
function tentarReconectar() {
tentativasReconexao++;
const delay = Math.min(1000 * tentativasReconexao, 30000);
setTimeout(() => {
registrar('Tentando reconexão...');
iniciarConexao();
}, delay);
}
// Modificar o handler onclose
socket.onclose = function() {
registrar('Conexão perdida, reconectando...', 'info');
tentarReconectar();
};