WebSockets HTML5 em Java com Jetty: Web em tempo real

Navegadores são bons em fazer requisições para o servidor. Mas e o contrário? Fazer o servidor enviar dados pro navegador em momentos arbitrários sempre foi um trabalho. Ajax reverso, comet, long polling são algumas das gambiarrastécnicas usadas. Mas o HTML5 trouxe uma grande novidade: a API de WebSockets.

WebSockets permitem abrir uma conexão com o servidor remoto e trafegar dados arbitrariamente do servidor para o cliente e vice-versa. Dá pra fazer muita coisa com isso. Um chat em tempo real, um mecanismo de sincronização, e até streaming de dados binários.

No último QCon SP 2012, usei WebSockets na minha palestra de Web Mobile para sincronizar os slides de apresentação com os celulares, tablets e notebooks da platéia. Todo mundo abriu uma página com os slides e, conforme a palestra andava, eu mudava o slide no telão e imediatamente a platéia via o novo slide em seus dispositivos, junto com notas e exemplos adicionais.

Os slides foram feitos em HTML, CSS e JS, e o mecanismo de sincronizar meu slide com os mais de 200 dispositivos conectados ao mesmo tempo foi WebSockets. Minha máquina no telão enviava para o servidor qual era o slide atual e este distribuía a informação pra todo mundo em tempo real.

JavaScript no cliente

O código na página é muito simples. Você abre a conexão com o servidor e pode receber ou enviar mensagens. O envio é uma simples chamada de método e o recebimento, um callback JavaScript assíncrono. Há ainda um callback pra você saber quando a conexão for aberta.

No caso do sincronizador de slides, a versão mobile aberta nos dispositivos dos usuários recebia o ID do slide a ser mostrado:

var ws = new WebSocket('ws://meuservico.com/websockets');

ws.onopen = function() {
  console.log('Conexão aberta com sucesso');
};

ws.onmessage = function(message) {
  var slide = document.getElementById(message);
  mostrarSlide(slide);
};

Já a máquina principal, que faz a sincronização, envia qual é o slide atual pra todo mundo:

function mostrarSlide(slide) {
   // lógica de exibir slide...

   // sincroniza dispositivos
   ws.send(slide.id);
}

Repare como usei um protocolo bem simples, trocando apenas os IDs dos slides. Isso facilitou em ocupar menos banda e evitar deixar a rede pesada no momento da palestra.

Servidor WebSockets com Jetty 8

O cliente JavaScript é bastante simples, mas a complexidade maior acaba ficando no servidor. WebSockets são um novo protocolo de comunicação em cima do HTTP e porta 80, e portanto exigem um servidor compatível. Há uma implementação de WebSockets muito boa no Jetty 8 para usarmos em Java, mas há outras para diversas linguagens – como o socket.io para Node.JS.

Com Jetty 8, é possível usar WebSockets através de uma Servlet especial, a WebSocketServlet. Herdamos dessa classe e sobrescrevemos o método doGet como numa Servlet comum. Mas, além disso, devemos sobrescrever o método doWebSocketConnect que faz a conexão no protocolo de WebSockets em si.

Sua implementação desse método deve devolver um objeto do tipo WebSocket que você vai criar. Para trabalhar com mensagens texto – como no nosso caso – implementamos a interface OnTextMessage. Essa interface nos dá três métodos: onOpen, onMessage e onClose.

Servidor de sincronização com Java

A ideia do sincronizador de slides é simples: guardar todos os usuários conectados numa lista e, quando chegar uma nova mensagem, reenviamos pra todo mundo. Para saber qual usuário é o principal, que comanda a sincronização, vou usar um parâmetro na URL:

@WebServlet(urlPatterns="/sincronizar")
public class SincronizadorServlet extends WebSocketServlet {
	// lista de todos os usuários conectados.
	// cuidado com acesso concorrente, por isso uso aqui a CopyOnWriteArraySet
	private final Set<SyncWebSocket> usuarios = new CopyOnWriteArraySet<SyncWebSocket>();
	
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse res)
			throws ServletException, IOException {

		// implementação do doGet aqui. 
		// pode até deixar em branco, sem resposta.

	}
	
	@Override
	public WebSocket doWebSocketConnect(HttpServletRequest request, String arg1) {

		// faz a conexão via WebSockets e devolve um novo cliente.
		// devolvemos um objeto da nossa classe SyncWebSocket que implementa a lógica.
		// repare que ela terá acesso à lista de usuários e 
		// ao parâmetro para saber se é o usuário principal ou não.
		return new SyncWebSocket(usuarios, request.getParameter("principal") != null);
	}
}

Cada cliente é representado pela minha classe SyncWebSocket que recebe as mensagens do usuário principal e dispara para todos os outros.

public class SyncWebSocket implements OnTextMessage {
	private final Set<SyncWebSocket> usuarios;
	private final boolean principal;
	private Connection connection;
	
	public SyncWebSocket(Set<SyncWebSocket> usuarios, boolean principal) {
		this.usuarios = usuarios;
		this.principal = principal;
	}

	public void onOpen(Connection connection) {
		// novo usuário conectado. adicionar na lista compartilhada
		usuarios.add(this);

		// guarda a Connection pra enviar mensagens depois
		this.connection = connection;
	}
	
	
	public void onClose(int arg0, String arg1) {
		// remove usuário da lista quando sai
		usuarios.remove(this);
	}

	public void onMessage(String message) {
		// só recebe mensagens se esse for o usuário principal.
		if (principal) {

			// envia a mensagem pra todo mundo, sincronizando os clientes
			for (SyncWebSocket usuario: usuarios) {
				try {
					usuario.connection.sendMessage(message);
				} catch (IOException e) {
					usuarios.remove(usuario);
					usuario.connection.close();
				}
			}
		} else {
			throw new RuntimeException("Você não pode mandar mensagens!");
		}
	}
}

Para subir esse código, você vai precisar do Jetty 8. No meu caso, precisei também copiar os JARs de websockets e outras libs do Jetty pra dentro do WEB-INF/lib do projeto.

É um exemplo simples de sincronização, mas muito mais é possível com WebSockets. Dá pra mandar mensagens binárias, e enviar e receber mensagens ao mesmo tempo.

Mais WebSockets e o futuro

WebSockets são suportadas nas últimas versões de todos os navegadores, incluindo os mobile. Há um detalhe apenas com relação à versão do protocolo que é suportada. Tivemos várias revisões do protocolo que foi sendo refinado com o tempo fechando bugs importantes de segurança. A versão final do RFC ainda não é suportada por todo mundo e pode gerar alguns problemas de compatibilidade para coisas mais complicadas.

A transmissão de dados binários foi adicionada recentemente ao protocolo e também não é suportada em todos os navegadores.

24 Comentários

  1. Caio Ribeiro Pereira 23/08/2012 at 10:48 #

    Se existisse um Socket.IO para Java, ai sim a coisa ficaria séria hehehe

    A última versão do Tomcat possui WebSockets também né?

  2. Sérgio Lopes 23/08/2012 at 12:43 #

    Verdade, tem no Tomcat sim. API própria também, mas bem parecida com essa do Jetty:

    http://tomcat.apache.org/tomcat-7.0-doc/web-socket-howto.html

  3. Nícolas Rossett 23/08/2012 at 17:43 #

    Sérgio isso só é possivel com java ou rola com php tb?

  4. Antonio Cesar 28/08/2012 at 21:27 #

    Muito legal o post… Parabéns pelo trabalho.

  5. João Reis 03/09/2012 at 13:35 #

    Interessante a pergunta sobre algo disso com php…alguém conhece?

  6. Diego 04/09/2012 at 17:38 #

    Para php vocês podem utilizar-se do Ratchet – http://socketo.me/

  7. Raphael Lacerda 10/09/2012 at 14:45 #

    Sergio! Post incrível!
    Pequena ressalva

    No comentário do código, ao invés de
    “// só recebe mensagens se esse for o usuário principal.”"
    nao seria
    “// só envia mensagens se esse for o usuário principal.”

  8. Sérgio Lopes 10/09/2012 at 15:25 #

    Valeu, Rafael!

    Na linha lá que você comentou, o correto é “receber” mesmo. O método “onMessage” é um callback que recebe no servidor mensagens enviadas pelos clientes. Ele é chamado quando uma mensagem chega.

  9. Germano 11/09/2012 at 12:44 #

    Html5 ta ficando realmente muito bom, os browsers estão se mexendo e o suporte já é quase total! No caso do websockets, para quem precisa desenvolver uma solução crossbrowser, uma opção seria usar o Cometd. Ele possui um mecanismo de comunicação baseado no protocolo Bayeux, com subscribes, channels, etc. O legal do Cometd é que ele é independente da “camada” de transporte. Pode-se definir Websocket como preferencial, mas se o browser não suportar ele pode usar long-pooling ou callback-pooling. Pode-se até mesmo fazer um fallback em Flash para emular Websocket, pois estes dois métodos sao bem gambiosos, como o Sérgio mesmo falou. Outra coisa legal do Cometd é que funciona bem com proxy

  10. Maria Helena da Silva 12/09/2012 at 10:25 #

    Outro dia solicitaram que eu incluísse na intranet um aviso se alguém registrou uma demanda nova. Eu informei que não era possível porque dependia da conexão que cai a cada 5 min de desuso e porque não tinha a menor idéia de como avisar o usuário conectado. Acho que na intranet (tomcat java) não terei problemas com a segurança, certo?

  11. Sérgio Lopes 13/09/2012 at 00:44 #

    Oi Maria Helena!

    Se esse aviso precisa ser dado em “tempo real”, deve aparecer pro usuário no instante em que acontece a nova demanda, então, sim, WebSockets podem ser uma boa solução.

    O usuário vai ter que estar com o navegador aberto pra receber a mensagem, claro. E cuidado com suporte nos navegadores: no Internet Explorer, só a partir do 10.

    E não há problemas de segurança não, mesmo pra Internet aberta.

    Abraços

  12. ricardo 09/10/2012 at 00:34 #

    o websocket é cross-domain?

  13. Sérgio Lopes 09/10/2012 at 04:33 #

    Ricardo, é cross domain sim. Fica a cargo do seu server autenticar e aceitar ou não clientes.

  14. Neylor Leandro de Sousa 17/10/2012 at 14:12 #

    No exemplo dos slides Sérgio, como você fez com o tempo de sessão? Pois vc e os outros clientes poderiam ficar um tempo imprevisível sem interagir com o browser.

  15. Sérgio Lopes 19/10/2012 at 11:06 #

    @Neylor

    No server, eu implementei um ping de 30 em 30 segundos que tenta enviar uma mensagem pra todos os clientes. Se der exception e o cara não receber, eu derrubo a conexão dele. Isso evita que conexões fiquem abertas sem o cara estar por perto.

    Aí no cliente eu implementei um JS pra reconectar automaticamente se a conexão cair. Assim se o cara voltar pra página ou a rede dele ficar ativa novamente, ele já está conectado.

    É bem agressivo no sentido de tentar manter a conexão aberta o tempo todo. Mas com WebSockets não vejo muito problema. O servidor aguenta esse monte de pessoas penduradas ao mesmo tempo.

    Uma otimização possível que não fiz seria usar a Visibility API no JS pra desligar a conexão caso o cara saia da aba/janela pra ver outra coisa. E depois reconectar quando ele voltar.

  16. Eric Serafim 15/01/2013 at 12:56 #

    Olá Sergio, gostaria de saber se já teve contato com frameworks cross-server ou cross-container , tais como, Atmosphere e JWebSocket, e se você teria outros para indicar?

  17. Sérgio Lopes 15/01/2013 at 19:31 #

    Oi Eric, não mexi não. Vi um pouco de Socket.io mas queria algo “puro Web Sockets”. Abraços

  18. Eliezer 17/01/2013 at 12:34 #

    Existe a possibilidade de configurar o tempo de conexão com o servidor? Ex: Quero que a conexão dos clientes com o servidor dure apenas 5 min.

  19. Lucas 13/05/2013 at 12:03 #

    Sérgio, é possível você disponibilizar o download da parte do servidor que é feito em java?
    Estou aprendendo java, e fica mais fácil se eu pegar algo que esteja pronto pra eu re-fazer, ver como funcionar, add mais coisas, etc..

    Fico agradecido.. E ótimo blog \õ

    flw

  20. Deyvid Franklin 16/06/2013 at 03:52 #

    Interessante, e inovadora sua ideia da apresentação com o slide funcionando dessa forma.
    Fiz um chat a um tempinho atras utilizando a API WebSocket , com a biblioteca SignalR em C#.
    É muito semelhante ao seu exemplo, só tive dificuldades quando comecei a implementar múltiplas janelas para conversas com múltiplos usuários, Enfim, não vem ao caso. #Parabéns jovem!

  21. José Roberto 04/09/2013 at 16:28 #

    Sérgio, boa tarde!

    Posso utilizar o TomCat 7 para webSocket ou o Jetty é o mais recomendado mesmo ?

    Obrigado,

    José Roberto.

Deixe uma resposta