Usando Spring Boot para implementação de WebSocket com STOMP
Publicados: 2022-03-11O protocolo WebSocket é uma das maneiras de fazer com que seu aplicativo manipule mensagens em tempo real. As alternativas mais comuns são sondagens longas e eventos enviados pelo servidor. Cada uma dessas soluções tem suas vantagens e desvantagens. Neste artigo, mostrarei como implementar WebSockets com o Spring Boot Framework. Abordarei a configuração do lado do servidor e do lado do cliente e usaremos o protocolo STOMP sobre WebSocket para nos comunicarmos.
O lado do servidor será codificado puramente em Java. Mas, no caso do cliente, mostrarei trechos escritos tanto em Java quanto em JavaScript (SockJS), já que, normalmente, os clientes WebSockets são embutidos em aplicativos front-end. Os exemplos de código demonstrarão como transmitir mensagens para vários usuários usando o modelo pub-sub, bem como enviar mensagens apenas para um único usuário. Em outra parte do artigo, discutirei brevemente a segurança de WebSockets e como podemos garantir que nossa solução baseada em WebSocket permaneça operacional mesmo quando o ambiente não oferece suporte ao protocolo WebSocket.
Observe que o tópico de segurança de WebSockets será abordado brevemente aqui, pois é um tópico complexo o suficiente para um artigo separado. Devido a isso, e vários outros fatores que abordo no WebSocket em Produção? seção no final, recomendo fazer modificações antes de usar esta configuração em produção , leia até o final para uma configuração pronta para produção com medidas de segurança em vigor.
Protocolos WebSocket e STOMP
O protocolo WebSocket permite implementar comunicação bidirecional entre aplicativos. É importante saber que o HTTP é usado apenas para o handshake inicial. Depois que isso acontece, a conexão HTTP é atualizada para uma conexão TCP/IP recém-aberta que é usada por um WebSocket.
O protocolo WebSocket é um protocolo de nível bastante baixo. Ele define como um fluxo de bytes é transformado em quadros. Um quadro pode conter um texto ou uma mensagem binária. Como a mensagem em si não fornece informações adicionais sobre como roteá-la ou processá-la, é difícil implementar aplicativos mais complexos sem escrever código adicional. Felizmente, a especificação WebSocket permite o uso de sub-protocolos que operam em um nível de aplicação superior. Um deles, suportado pelo Spring Framework, é o STOMP.
O STOMP é um protocolo de mensagens simples baseado em texto que foi criado inicialmente para linguagens de script, como Ruby, Python e Perl, para se conectar a agentes de mensagens corporativos. Graças ao STOMP, clientes e corretores desenvolvidos em diferentes idiomas podem enviar e receber mensagens entre si. O protocolo WebSocket às vezes é chamado de TCP para Web. Analogicamente, STOMP é chamado de HTTP para Web. Ele define vários tipos de quadros que são mapeados em quadros WebSockets, por exemplo, CONNECT
, SUBSCRIBE
, UNSUBSCRIBE
, ACK
ou SEND
. Por um lado, esses comandos são muito úteis para gerenciar a comunicação e, por outro, permitem implementar soluções com recursos mais sofisticados, como reconhecimento de mensagens.
O lado do servidor: Spring Boot e WebSockets
Para construir o lado do servidor WebSocket, utilizaremos a estrutura Spring Boot, que acelera significativamente o desenvolvimento de aplicativos autônomos e da Web em Java. Spring Boot inclui o módulo spring-WebSocket
, que é compatível com o padrão Java WebSocket API (JSR-356).
A implementação do lado do servidor WebSocket com o Spring Boot não é uma tarefa muito complexa e inclui apenas algumas etapas, que percorreremos uma a uma.
Etapa 1. Primeiro, precisamos adicionar a dependência da biblioteca WebSocket.
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
Se você planeja usar o formato JSON para mensagens transmitidas, convém incluir também a dependência GSON ou Jackson. Muito provavelmente, você também pode precisar de uma estrutura de segurança, por exemplo, Spring Security.
Etapa 2. Em seguida, podemos configurar o Spring para ativar as mensagens WebSocket e STOMP.
Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/mywebsockets") .setAllowedOrigins("mydomain.com").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry config){ config.enableSimpleBroker("/topic/", "/queue/"); config.setApplicationDestinationPrefixes("/app"); } }
O método configureMessageBroker
faz duas coisas:
- Cria o agente de mensagens na memória com um ou mais destinos para enviar e receber mensagens. No exemplo acima, há dois prefixos de destino definidos:
topic
equeue
. Eles seguem a convenção de que os destinos das mensagens a serem transportadas para todos os clientes inscritos por meio do modelo pub-sub devem ser prefixados comtopic
. Por outro lado, os destinos para mensagens privadas geralmente são prefixados porqueue
. - Define o
app
de prefixo usado para filtrar destinos manipulados por métodos anotados com@MessageMapping
que você implementará em um controlador. O controlador, após processar a mensagem, a enviará ao broker.
Voltando ao trecho acima—provavelmente você notou uma chamada para o método withSockJS()
—ele habilita as opções de fallback do SockJS. Para manter as coisas curtas, ele permitirá que nossos WebSockets funcionem mesmo que o protocolo WebSocket não seja suportado por um navegador da Internet. Falarei um pouco mais sobre esse assunto com mais detalhes.
Há mais uma coisa que precisa ser esclarecida - por que chamamos o método setAllowedOrigins()
no endpoint. Geralmente é necessário porque o comportamento padrão do WebSocket e do SockJS é aceitar apenas solicitações de mesma origem. Portanto, se seu cliente e o lado do servidor usam domínios diferentes, esse método precisa ser chamado para permitir a comunicação entre eles.
Etapa 3 . Implemente um controlador que manipulará as solicitações do usuário. Ele transmitirá a mensagem recebida para todos os usuários inscritos em um determinado tópico.
Aqui está um método de exemplo que envia mensagens para o destino /topic/news
.
@MessageMapping("/news") @SendTo("/topic/news") public void broadcastNews(@Payload String message) { return message; }
Em vez da anotação @SendTo
, você também pode usar SimpMessagingTemplate
que você pode conectar automaticamente dentro do seu controlador.
@MessageMapping("/news") public void broadcastNews(@Payload String message) { this.simpMessagingTemplate.convertAndSend("/topic/news", message) }
Em etapas posteriores, você pode querer adicionar algumas classes adicionais para proteger seus terminais, como ResourceServerConfigurerAdapter
ou WebSecurityConfigurerAdapter
da estrutura Spring Security. Além disso, geralmente é benéfico implementar o modelo de mensagem para que o JSON transmitido possa ser mapeado para objetos.

Construindo o cliente WebSocket
Implementar um cliente é uma tarefa ainda mais simples.
Etapa 1. Cliente Autowire Spring STOMP.
@Autowired private WebSocketStompClient stompClient;
Etapa 2. Abra uma conexão.
StompSessionHandler sessionHandler = new CustmStompSessionHandler(); StompSession stompSession = stompClient.connect(loggerServerQueueUrl, sessionHandler).get();
Feito isso, é possível enviar uma mensagem para um destino. A mensagem será enviada a todos os usuários inscritos em um tópico.
stompSession.send("topic/greetings", "Hello new user");
Também é possível assinar mensagens.
session.subscribe("topic/greetings", this); @Override public void handleFrame(StompHeaders headers, Object payload) { Message msg = (Message) payload; logger.info("Received : " + msg.getText()+ " from : " + msg.getFrom()); }
Às vezes é necessário enviar uma mensagem apenas para um usuário dedicado (por exemplo, ao implementar um chat). Em seguida, o cliente e o lado do servidor devem usar um destino separado dedicado a essa conversa privada. O nome do destino pode ser criado anexando um identificador exclusivo a um nome de destino geral, por exemplo, /queue/chat-user123
. Os identificadores de sessão HTTP ou STOMP podem ser utilizados para esta finalidade.
O Spring torna o envio de mensagens privadas muito mais fácil. Só precisamos anotar o método de um Controller com @SendToUser
. Em seguida, esse destino será tratado por UserDestinationMessageHandler
, que depende de um identificador de sessão. No lado do cliente, quando um cliente se inscreve em um destino prefixado com /user
, esse destino é transformado em um destino exclusivo para esse usuário. No lado do servidor, um destino de usuário é resolvido com base no Principal
de um usuário.
Exemplo de código do lado do servidor com anotação @SendToUser
:
@MessageMapping("/greetings") @SendToUser("/queue/greetings") public String reply(@Payload String message, Principal user) { return "Hello " + message; }
Ou você pode usar SimpMessagingTemplate
:
String username = ... this.simpMessagingTemplate.convertAndSendToUser(); username, "/queue/greetings", "Hello " + username);
Vejamos agora como implementar um cliente JavaScript (SockJS) capaz de receber mensagens privadas que podem ser enviadas pelo código Java no exemplo acima. Vale saber que os WebSockets fazem parte da especificação HTML5 e são suportados pela maioria dos navegadores modernos (o Internet Explorer os suporta desde a versão 10).
function connect() { var socket = new SockJS('/greetings'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { stompClient.subscribe('/user/queue/greetings', function (greeting) { showGreeting(JSON.parse(greeting.body).name); }); }); } function sendName() { stompClient.send("/app/greetings", {}, $("#name").val()); }
Como você provavelmente notou, para receber mensagens privadas, o cliente precisa se inscrever em um destino geral /queue/greetings
prefixado com /user
. Ele não precisa se preocupar com nenhum identificador exclusivo. No entanto, o cliente precisa fazer login no aplicativo antes, para que o objeto Principal
no lado do servidor seja inicializado.
Protegendo WebSockets
Muitos aplicativos da Web usam autenticação baseada em cookies. Por exemplo, podemos usar o Spring Security para restringir o acesso a determinadas páginas ou controladores apenas para usuários logados. O contexto de segurança do usuário é então mantido por meio de uma sessão HTTP baseada em cookie que é posteriormente associada a sessões WebSocket ou SockJS criadas para esse usuário. Os terminais WebSockets podem ser protegidos como qualquer outra solicitação, por exemplo, no WebSecurityConfigurerAdapter
do Spring.
Atualmente, os aplicativos da Web geralmente usam APIs REST como back-end e tokens OAuth/JWT para autenticação e autorização de usuários. O protocolo WebSocket não descreve como os servidores devem autenticar clientes durante o handshake HTTP. Na prática, os cabeçalhos HTTP padrão (por exemplo, Autorização) são usados para essa finalidade. Infelizmente, não é suportado por todos os clientes STOMP. O cliente STOMP do Spring Java permite definir cabeçalhos para o handshake:
WebSocketHttpHeaders handshakeHeaders = new WebSocketHttpHeaders(); handshakeHeaders.add(principalRequestHeader, principalRequestValue);
Mas o cliente JavaScript SockJS não suporta o envio de cabeçalho de autorização com uma solicitação SockJS. No entanto, permite o envio de parâmetros de consulta que podem ser usados para passar um token. Essa abordagem requer a gravação de código personalizado no lado do servidor que lerá o token dos parâmetros de consulta e o validará. Também é importante certificar-se de que os tokens não sejam registrados junto com as solicitações (ou os logs estejam bem protegidos), pois isso pode introduzir uma grave violação de segurança.
Opções de fallback do SockJS
A integração com o WebSocket nem sempre pode ocorrer sem problemas. Alguns navegadores (por exemplo, IE 9) não suportam WebSockets. Além disso, os proxies restritivos podem impossibilitar a atualização do HTTP ou cortar as conexões abertas por muito tempo. Nesses casos, o SockJS vem em socorro.
Os transportes SockJS se enquadram em três categorias gerais: WebSockets, HTTP Streaming e HTTP Long Polling. A comunicação começa com o SockJS enviando GET /info
para obter informações básicas do servidor. Com base na resposta, o SockJS decide o transporte a ser usado. A primeira escolha são WebSockets. Se eles não forem suportados, então, se possível, o Streaming será usado. Se esta opção também não for possível, o Polling é escolhido como método de transporte.
WebSocket em produção?
Embora essa configuração funcione, não é a “melhor”. O Spring Boot permite que você use qualquer sistema de mensagens completo com o protocolo STOMP (por exemplo, ActiveMQ, RabbitMQ), e um corretor externo pode suportar mais operações STOMP (por exemplo, confirmações, recebimentos) do que o corretor simples que usamos. STOMP Over WebSocket fornece informações interessantes sobre WebSockets e protocolo STOMP. Ele lista os sistemas de mensagens que lidam com o protocolo STOMP e podem ser uma solução melhor para uso em produção. Especialmente se, devido ao alto número de solicitações, o agente de mensagens precisar ser agrupado. (O agente de mensagens simples do Spring não é adequado para clustering.) Então, em vez de ativar o agente simples em WebSocketConfig
, é necessário ativar a retransmissão do agente Stomp que encaminha mensagens para e de um agente de mensagens externo. Para resumir, um agente de mensagens externo pode ajudá-lo a construir uma solução mais escalável e robusta.
Se você estiver pronto para continuar sua jornada de desenvolvedor Java explorando o Spring Boot, tente ler Using Spring Boot for OAuth2 and JWT REST Protection a seguir.