WebSockets HTML5 em Java com Jetty: Web em tempo real

Postado em 23. ago, 2012 por Sérgio Lopes em Java, Web Design

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.

Sérgio Lopes ()

Mais sobre o autor

Tags: , , , , , ,

21 Respostas para “WebSockets HTML5 em Java com Jetty: Web em tempo real”

  1. Caio Ribeiro Pereira

    23. ago, 2012

    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. ago, 2012

    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. ago, 2012

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

  4. Antonio Cesar

    28. ago, 2012

    Muito legal o post… Parabéns pelo trabalho.

  5. João Reis

    03. set, 2012

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

  6. Diego

    04. set, 2012

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

  7. Raphael Lacerda

    10. set, 2012

    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. set, 2012

    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. set, 2012

    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. set, 2012

    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. set, 2012

    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. out, 2012

    o websocket é cross-domain?

  13. Sérgio Lopes

    09. out, 2012

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

  14. Neylor Leandro de Sousa

    17. out, 2012

    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. out, 2012

    @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. jan, 2013

    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. jan, 2013

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

  18. Eliezer

    17. jan, 2013

    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. mai, 2013

    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

Trackbacks/Pingbacks

  1. WebSockets HTML5 em Java com Jetty: Web em tempo real « thoughts… - agosto 23, 2012

    [...] http://blog.caelum.com.br/websockets-html5-em-java-com-jetty-web-em-tempo-real/ Share this:TwitterFacebookLike this:LikeBe the first to like this. [...]

  2. 10 razões para migrar sua aplicação para JSF 2 | blog.caelum.com.br - setembro 19, 2012

    [...] muitas funcionalidades interessantes na especificação e ver o JSF tentar se alinhar mostra cada vez mais a preocupação da tecnologia em melhorar a User [...]

Deixar uma Resposta