A API Java para WebSocket está definida na JSR 356 sendo parte integrante do Java EE 7. Através do site oficial da Oracle podemos também baixar a especificação completa ou acessa-la na Web.
A API WebSocket fornece um protocolo de comunicação full-duplex e bidirecional através um uma única conexão TCP. Full-duplex significa que um cliente e um servidor podem enviar mensagens independentes um dos outros. O conceito de bidirecional significa que um cliente pode enviar uma mensagem para um servidor e vice versa.
O WebSocket é uma combinação do protocolo IETF RFC 6455 e a API JavaScript.
O protocolo define um handshake de abertura e enquadramento básico de mensagens, em camadas sobre TCP. Um handshake ou “aperto de mão” é o processo pelo qual duas máquinas afirmam uma à outra que a reconheceu e está pronta para iniciar a comunicação. O handshake é utilizado em protocolos de comunicação, tais como: FTP, TCP, HTTP, SMB, SMTP, POP3 etc.
A API permite que páginas web usem o protocolo WebSocket para a comunicação em dois sentidos com o host remoto. Ao contrário do protocolo HTTP, não há necessidade de criar uma nova conexão TCP e enviar uma mensagem cheia de cabeçalhos para cada troca de mensagens entre cliente e servidor.
Uma vez que o reconhecimento inicial acontece através do campo Upgrade do HTTP (definido no RFC 2616, secção 14.42), o cliente e o servidor podem enviar mensagens um para o outro, independentes entre si. Não existem padrões de troca de mensagens pré-definidas de solicitação/resposta ou um caminho unidirecional entre o cliente e o servidor.
Assim sendo, o protocolo WebSocket muda a forma como os servidores web lidam com as requisições de seus clientes, pois ao invés de encerrar a conexão, o servidor devolve um status 101 e mantém a conexão aberta para que ele ou o cliente possam enviar novas mensagens no fluxo de comunicação.
Segue abaixo um exemplo de uma requisição de handshake enviada pelo cliente:
GET /minhaApp HTTP/1.1
Host: servidor.exemplo.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Version: 13
Origin: http://exemplo.com
Abaixo a resposta do servidor:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
A comunicação entre cliente e servidor é bastante simétrica, mas há duas diferenças. A primeira é que um cliente inicia a conexão com um servidor que esta esperando por uma requisição do tipo WebSocket. A segunda é que um cliente se conecta a um servidor usando uma URI. Um servidor pode aguardar por requisições de múltiplos clientes no mesmo URI.
Além destas duas diferenças, o cliente e o servidor se comportam simetricamente após o handshake. Nesse sentido, eles são considerados pares. Após um handshake com sucesso, os clientes e servidores transferem dados entre si, esses dados são chamados de mensagens. No cabo, uma mensagem é composta de um ou mais quadros. Os quadros de aplicação transportam uma carga útil (ou payload) destinada à aplicação e esta carga útil que está sendo transportada pode estar em formato de texto ou dados binários. Os quadros de controle transportam dados destinados a sinalização em nível de protocolo.
A API Java para WebSocket define uma API padrão para a construção de aplicações WebSocket e fornece suporte para criar um cliente WebSocket e um servidor Endpoint usando anotações e uma interface. As aplicações WebSocket em Java consistem de Endpoints WebSocket, que são objetos que representam uma das pontas ou términos da conexão WebSocket.A API também nos permite criar e consumir WebSocket em formato de texto, binário e mensagens de controle. Além disso, também podemos inicializar e interceptar eventos do ciclo de vida do WebSocket, configurar e gerenciar sessões do WebSocket como timeouts da sessão, quantidade de tentativas, cookies e pool de conexão e, por fim, a API WebSocket também especifica como a aplicação WebSocket funcionará dentro do modelo de segurança do Java EE.
No restante do artigo veremos como criar um Servidor Endpoint com anotações e um Servidor Endpoint programático. Também veremos a criação de um Cliente Endpoint utilizando anotações e programático. Em geral esses dois modelos de programação são suportados pela API Websockets, usando anotações ou interfaces. Por fim, criaremos clientes WebSocket utilizando JavaScript e entenderemos melhor os codificadores, decodificadores e como funciona a integração do WebSocket com o Java EE Security.
Criando um Servidor Endpoint Anotado
Podemos converter um Plain Old Java Object (POJO) em um Servidor Endpoint WebSocket simplesmente usando a anotação @ServerEndpoint. Para executar os exemplos abaixo deveremos baixar a API do WebSocket. A implementação de referencia para a API WebSocket para Java é o projeto Tyrus (http://tyrus.java.net/) que já está integrado no GlassFish 4.0. Para fazer o download direto vá na URL http://search.maven.org/remotecontent?filepath=org/glassfish/tyrus/bundles/websocket-ri-archive/1.5/websocket-ri-archive-1.5.zip lembrando que este link pode estar desatualizado daqui alguns anos. Por isso, se não conseguir por esta URL basta visitar o site do projeto. Também devemos ter a dependência javaee-api-7.0.jar disponível em http://repo1.maven.org/maven2/javax/javaee-api/7.0/javaee-api-7.0.jar.
Segue na Listagem 1 um exemplo de um Servidor Endpoint.
Listagem 1. Exemplo de um Servidor Endpoint.
package com.exemplo;
import javax.websocket.OnMessage;
import javax.websocket.server.ServerEndpoint;
@ServerEndpoint("/teste")
public class ExemploServidor {
@OnMessage
public String recebeMensagem(String mensagem) {
return "teste de mensagem";
}
}
No código acima a anotação @ServerEndpoint decora a classe como um WebSocket Endpoint disponibilizado no URI mencionado no valor da anotação, ou seja, em “/teste”. A classe anotada deve possuir um construtor sem argumentos e deve ser público. A anotação @ServerEndpoint ainda pode ter os atributos especificados na Tabela 1.
Atributo |
Obrigatório/Opcional |
Valor |
value |
Obrigatório |
URI onde o endpoint será publicado. |
encoders |
Opcional |
Conjunto ordenado de encorders usado por este Endpoint. |
decoders |
Opcional |
Conjunto ordenado de decoders usado por este Endpoint. |
subprotocols |
Opcional |
Conjunto ordenado de protocolos WebSocket suportado por este Endpoint. |
configurator |
Opcional |
Classe configuradora customizada usada para também configurar novas instancias deste Endpoint. Esta será uma implementação de ServerEndpointConfig.Configurator. |
Tabela 1. Atributos da anotação @ServerEndpoint.
Ainda no código anterior a anotação @OnMessage decora um método Java que recebe uma mensagem de entrada do WebSocket, neste caso o método recebeMensagem(). Esta mensagem pode ser do tipo texto, binário, ou uma mensagem do tipo "pong" (mensagens recebidas de volta). As mensagens de texto e binárias contêm carga útil (payload) gerada pela aplicação. Uma mensagem pong é uma mensagem de controle do WebSocket e geralmente não é tratada na camada de aplicação.
O método decorado com @OnMessage pode ter diferentes parâmetros dependendo do tipo de mensagem que se quer processar.
Se o método está gerenciando mensagens de texto podemos usar uma String para receber toda a mensagem de texto. Segue na Listagem 2 um exemplo.
Listagem 2. Exemplo de método para gerenciar mensagens de texto.
public void recebeMensagem(String s) {
//. . .
}
Também podemos usar um primitivo Java ou uma Classe equivalente para receber toda a mensagem convertida para esse tipo. Segue na Listagem 3 um exemplo.
Listagem 3. Exemplo de método para converter a mensagem para um tipo.
public void recebeMensagem(int i) {
//. . .
}
Outro tipo possível é utilizarmos uma String e um boolean para receber a mensagem em partes. Segue na Listagem 4 um exemplo.
Listagem 4. Exemplo de método para receber mensagens em partes.
public void recebeMensagemGrande(String mensagem, boolean ultimaMsg) {
//. . .
}
O parâmetro boolean é "true" se a parte recebida é a última parte da mensagem, e false caso contrário.
Utilizamos um Reader para receber todo o texto da mensagem como um “blocking stream”, conforme exemplo da Listagem 5.
Listagem 5. Exemplo de método para receber mensagens em um Reader.
@OnMessage
public void processaReader(Reader reader) {
//. . .
}
Por fim, usamos qualquer parâmetro de objeto para o qual o Endpoint tem um decodificador de texto (Decoder.Text ou Decoder.TextStream).
Se o método está gerenciando mensagens binárias utilizamos byte[] ou ByteBuffer para receber toda a mensagem binária. Segue na Listagem 6 um exemplo.
Listagem 6. Exemplo de método para receber mensagens binárias.
public void recebeMensagem(ByteBuffer b) {
//. . .
}
Utilizamos um par byte[] e boolean, ou um par ByteBuffer e boolean para receber a mensagem em partes, conforme a Listagem 7.
Listagem 7. Exemplo de método para receber mensagens binárias em partes.
public void recebeMensagemBinariaGrande(ByteBuffer buf, boolean ultimaParte) {
//. . .
}
O parâmetro boolean é true se a parte recebida é a última parte, e false caso contrário.
Também podemos utilizar um InputStream para receber toda a mensagem binária como um "blocking stream". Segue exemplo na Listagem 8.
Listagem 8. Exemplo de método para receber mensagens binárias num InputStream.
public void processaStream(InputStream stream) {
//. . .
}
Por fim, podemos utilizar qualquer parâmetro de objeto para o qual o Endpoint tem um decodificador binário.
Se o método está gerenciando mensagens pong usamos um PongMessage para receber a mensagem pong. Segue abaixo um exemplo:
Listagem 9. Exemplo de método para receber mensagens do tipo PongMessage.
public void processaPong(PongMessage pong) {
//. . .
}
Também podemos utilizar um parâmetro String 0..n ou um primitivo Java anotado com @PathParam para servidores Endpoint. Segue abaixo um exemplo:
Listagem 10. Exemplo utilizando parâmetro String e @PathParam.
@ServerEndpoint("/chat/{room}")
public class MeuEndpoint {
@OnMessage
public void recebeMensagem(String message, @PathParam("room")String room) {
//. . .
}
}
No código acima a anotação @PathParam é usada para anotar os parâmetros do método no servidor Endpoint onde o modelo da URI é utilizado no mapeamento do caminho da anotação @ServerEndpoint. O parâmetro do método pode ser do tipo String, qualquer tipo primitivo Java, ou qualquer versão encaixotada dos primitivos (Integer, Long, Float, etc). Se um URI do cliente corresponder ao URI do modelo, mas o parâmetro do caminho não puder ser decodificado, então o gerenciador de erros do WebSocket será chamado.
Outra possibilidade é utilizarmos um parâmetro Session opcional. Segue abaixo um exemplo:
Listagem 11. Exemplo utilizando um Session.
public void recebeMensagem(String message, Session session) {
//. . .
}
Neste código Session indica uma conversação entre dois Endpoints WebSocket e representa o outro fim da conexão. Neste caso, uma resposta para o cliente pode ser retornada da seguinte forma:
Listagem 12. Exemplo de retorno de resposta para um cliente.
public void recebeMesagem(String mensagem, Session session) {
session.getBasicRemote().sendText(...);
}
Os parâmetros podem ser listados em qualquer ordem.
O método pode ter um retorno do tipo void. Tal mensagem é consumida no Endpoint sem retornar uma resposta.
O método pode ter String, ByteBuffer, byte[], qualquer primitivo Java ou classe equivalente, e qualquer outra classe na qual exista um codificador como valor de retorno. Se um tipo de retorno é especificado, então uma resposta é retornada para o cliente.
O atributo maxMessageSize pode ser usado para definir o tamanho máximo da mensagem em bytes que este método será capaz de processar. Abaixo um exemplo de como podemos utilizar essa anotação:
Listagem 13. Exemplo utilizando maxMessageSize para definir o tamanho máximo de mensagens.
@Message(maxMessageSize=6)
public void receiveMessage(String s) {
//. . .
}
Neste código, se uma mensagem de mais de 6 bytes for recebida, então é comunicado um erro e a conexão é fechada. Podemos receber o código de erro exato e uma mensagem interceptando o método do ciclo de vida usando @OnClose. O valor default é -1 para indicar que não existe um máximo. O atributo maxMessageSize apenas se aplica quando a anotação é usada para processar toda a mensagem, não se aplica aos métodos que processam mensagens em partes ou usam um parâmetro de fluxo (stream) ou de leitura (reader) para gerenciar mensagens de entrada.
Outro conceito importante para Web Sockets são os codificadores e os decodificadores. Os Codificadores fornecem uma forma de converter objetos Java customizados em mensagens WebSocket e pode ser especificado através do atributo “encoders”. Os Decodificadores fornecem uma forma de converter mensagens WebSocket para objetos Java customizados e pode ser especificado via atributo “decoders”.
Um atributo opcional configurator pode ser usado para especificar uma classe de configuração customizada para configurarmos novas instâncias deste Endpoint. Segue abaixo um exemplo:
Listagem 14. Exemplo utilizando classes com configurações personalizadas.
public class MeuConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
//. . .
}
}
@ServerEndpoint(value="/websocket", configurator = MeuConfigurator.class)
public class MeuEndpoint {
@OnMessage
public void recebeMensagem(String name) {
//. . .
}
}
No código acima a classe MeuConfigurator fornece uma implementação de ServerEndpointConfig.Configurator. Esta classe abstrata oferece vários métodos para configurar o Endpoint, tais como: oferecer algoritmos de configuração customizados e interceptar a abertura do handshake. O método modifyHandshake é chamado quando uma resposta de um handshake resultante de um pedido handshake é preparado. ServerEndpointConfig é o objeto de configuração do Endpoint usado para configurar este Endpoint. O objeto HandshakeRequest oferece informações sobre a solicitação HTTP do tipo GET definido para o handshake de abertura. Esta classe fornece acesso a informações tais como a lista de cabeçalhos do HTTP que vem com a solicitação ou o HttpSession que a solicitação de handshake era parte. O objeto HandshakeResponse identifica a resposta do handshake HTTP preparado pelo container. O atributo configurator é usado para especificar a classe configurator customizada como parte do @ServerEndpoint.
A anotação @OnOpen pode ser utilizada para decorar um método para ser chamado quando uma nova conexão de um par for recebida. Isto é similar ao @OnClose que pode ser usado para decorar um método para ser chamado quando uma conexão é fechada por um par. @OnError pode ser usada para decorar um método para ser chamado quando um erro é recebido.
Esses métodos podem ter um parâmetro Session opcional, um EndpointConfig opcional para @OnOpen, um CloseReason para @OnClose, ou um Throwable para o parâmetro @OnError, e por fim, pode ter 0..n parâmetros String anotados com @PathParam.
Segue abaixo um exemplo:
Listagem 15. Exemplo utilizando as anotações @OnOpen, @OnClose e @OnError.
@OnOpen
public void abrir(Session s) {
//. . .
}
@OnClose
public void fechar(CloseReason c) {
//. . .
}
@OnError
public void erro(Throwable t) {
//. . .
}
No código acima temos o método “abrir”. Este método é chamado quando uma nova conexão é estabelecida com este Endpoint. O parâmetro “s” fornece mais detalhes sobre outra extremidade da conexão. O método “fechar” é chamado quando a conexão é finalizada. O parâmetro “c” fornece mais detalhes sobre o porquê de uma conexão WebSocket ter sido fechada. Por fim, o método “erro” é chamado quando existe um erro na conexão. O parâmetro “t” fornece mais detalhes sobre o erro.
Para Endpoints publicados na plataforma Java EE, temos suporte completo a injeção de dependência. Injeção de campo, método e construtor estão disponíveis em todas as classes Endpoint do WebSocket. Interceptores podem ser habilitados para essas classes usando o mecanismo padrão. Segue abaixo um exemplo de injeção num Endpoint:
Listagem 16. Exemplo utilizando injeção de dependência.
@ServerEndpoint("/chat")
public class ChatServer {
@Inject Usuario usuario;
//. . .
}
Neste código o bean Usuario é injetado usando o mecanismo padrão de injeção.
Uma observação importante é que as anotações do WebSocket não são passados para baixo na hierarquia de herança das classe Java. Eles aplicam-se apenas para a classe Java em que são marcados. Por exemplo, uma classe Java que herda de uma classe Java anotada com uma anotação @ServerEndpoint a nível de classe não se tornará um Endpoint anotado, a menos que ele próprio esteja anotado com uma anotação @ServerEndpoint. Segue abaixo um exemplo:
Listagem 17. Exemplo demonstrando a herança de um Endpoint.
@ServerEndpoint("/chat")
public class ChatServer {
}
public class MeuChatServer extends ChatServer {
//. . .
}
No código acima a classe ChatServer é identificado como um Endpoint WebSocket. No entanto, MeuChatServer não é, mesmo herdando de um Endpoin WebSocket. Se ele quer ser um Endpoint WebSocket, então ele deve ser explicitamente marcado com uma anotação de nível de classe @ServerEndpoint. Subclasses de um Endpoint anotado não podem usar anotações do WebSocket a nível de método, a menos que eles mesmos usem uma anotação WebSocket a nível de classe. Subclasses que sobrescrevem métodos anotados com anotações de método do WebSocket não obtém métodos de callback do WebSocket a menos que esses métodos da subclasse sejam marcados com uma anotação do WebSocket a nível de método.
Criando um Servidor Endpoint Programático
Podemos criar um Servidor Endpoint estendendo a classe Endpoint. Esse Endpoint também é chamado de um Endpoint Programático. Segue abaixo um exemplo:
Listagem 18. Exemplo de um servidor Endpoint programático.
public class MeuEndpoint extends Endpoint {
@Override
public void onOpen(final Session session, EndpointConfig config) {
//. . .
}
}
No código acima o método onOpen é chamado quando uma nova conexão é iniciada. O atributo EndpointConfig identifica o objeto de configuração usado para configurar este Endpoint.
Múltiplos MessageHandlers podem ser registrados neste método para processar textos de entrada, binários e mensagens pong. Entretanto, apenas um MessageHandler por texto, binário ou mensagem pong pode ser registrado por Endpoint. Segue abaixo um exemplo:
Listagem 19. Exemplo utilizando múltiplos MessageHandlers.
session.addMessageHandler(new MessageHandler.Whole<String>() {
@Override
public void onMessage(String s) {
//. . .
}
});
session.addMessageHandler(new MessageHandler.Whole<ByteBuffer>() {
@Override
public void onMessage(ByteBuffer b) {
//. . .
}
});
session.addMessageHandler(new MessageHandler.Whole<PongMessage>() {
@Override
public void onMessage(PongMessage p) {
//. . .
}
});
No código acima o processador MessageHandler.Whole<String> é registrado para manipular as mensagens de texto recebidas. O método onMessage do processador é invocado quando a mensagem é recebida. O parâmetro “s” possui a carga útil da mensagem.
O processador MessageHandler.Whole<ByteBuffer> é registrado para manipular as mensagens binárias recebidas. O método onMessage do processador é invocado quando a mensagem é recebida. O parâmetro “b” possui a carga útil da mensagem.
Por fim, o processador MessageHandler.Whole<PongMessage> é registrado para manipular PongMessage recebidas. O método onMessage do processador é invocado quando a mensagem é recebida. O parâmetro “p” possui a carga útil da mensagem.
Embora não seja necessário, uma resposta pode ser enviada para a outra extremidade da conexão de forma síncrona. Segue abaixo um exemplo:
Listagem 20. Exemplo de como enviar uma mensagem de forma síncrona.
session.addMessageHandler(new MessageHandler.Whole<String>() {
@Override
public void onMessage(String s) {
try {
session.getBasicRemote().sendText(s);
} catch (IOException ex) {
//. . .
}
}
});
Uma mensagem pode ser retornada de forma assíncrona também, isto é feito utilizando o método Session.getAsyncRemote que retorna uma instância de RemoteEndpoint.Async. Duas variações são possíveis. Segue abaixo um exemplo do primeiro tipo de variação que podemos ter:
Listagem 21. Exemplo de como enviar uma mensagem de forma assíncrona.
@Override
public void onMessage(String data) {
session.getAsyncRemote().sendText(data, new SendHandler() {
@Override
public void onResult(SendResult sr) {
//. . .
}
});
}
Na primeira variação, um processador de callback chamado SendHadler é registrado. O método onResult é chamado uma vez que a mensagem tenha sido transmitida. O parâmetro “sr” indica se a mensagem foi enviada com sucesso, caso não seja enviada com sucesso ele carrega uma exceção para indicar qual era o problema.
Na segunda variação, uma instância de Future é retornada. Segue um exemplo da segunda variação:
Listagem 22. Exemplo de código retornando uma instancia de Future.
@Override
public void onMessage(String data) {
Future f = session.getAsyncRemote().sendText(data);
//. . .
if (f.isDone()) {
Object o = f.get();
}
}
O método sendXXX retorna antes da mensagem ser transmitida. O objeto Future retornado é usado para monitorar o progresso da transmissão. O método get do Future retorna nulo após a conclusão bem-sucedida. Erros na transmissão são empacotadas na exceção ExecutionException lançada quando o objeto Future é consultado. O Endpoint.onClose e os métodos onError podem ser sobrescritos para invocar outros retornos de chamada do ciclo de vida do Endpoint. Segue abaixo um exemplo:
Listagem 23. Exemplo sobrescrevendo onClose e onError.
public class MeuEndpoint extends Endpoint {
//. . .
@Override
public void onClose(Session session, CloseReason c) {
//. . .
}
@Override
public void onError(Session session, Throwable t) {
//. . .
}
}
No método onClose, o parâmetro “c” fornece mais detalhes sobre porque a conexão do WebSocket foi fechada. Da mesma forma, o parâmetro “t” fornece mais detalhes sobre o erro recebido.
Recebemos uma mensagem multiparte sobrescrevendo MessageHandler.Partial<T>, onde T é uma String para mensagens de texto, e ByteBuffer ou byte[] é utilizado para mensagens binárias. Segue abaixo um exemplo:
Listagem 24. Exemplo de como receber uma mensagem multiparte.
session.addMessageHandler(new MessageHandler.Partial<String>() {
@Override
public void onMessage(String name, boolean ultimaParte) {
//. . .
}
});
O parâmetro boolean é verdadeiro se a parte recebida é a última parte, e falso caso contrário.
Podemos configurar Endpoints programaticamente implementando a interface ServerApplicationConfig. Esta interface fornece métodos para especificar os Endpoints dentro de um arquivo que deve ser publicado. Segue um exemplo abaixo:
Listagem 25. Exemplo de como configurar Endpoints programaticamente.
public class MinhaAppConfig implements ServerApplicationConfig {
@Override
public Set<ServerEndpointConfig> getEndpointConfigs(
Set<Class<? extends Endpoint>> set) {
return new HashSet<ServerEndpointConfig>() {{
add(ServerEndpointConfig
.Builder
.create(MeuEndpoint.class, "/chat")
.build());
}};
}
//. . .
}
No código acima a classe MinhaAppConfig implementa a interface ServerApplicationConfig. O método getEndpointConfig fornece uma lista de ServerEndpointConfig que é usado para publicar os Endpoints programáticos. A URI do Endpoint é especificada aqui também.
Podemos configurar o Endpoint com algoritmos de configuração personalizado fornecendo uma instância de ServerEndpointConfig.Configurator. Segue um exemplo abaixo:
Listagem 26. Exemplo de como configurar Endpoints com algoritmos de configuração personalizados.
@Override
public Set<ServerEndpointConfig> getEndpointConfigs(Set<Class<? extends Endpoint>> set) {
return new HashSet<ServerEndpointConfig>() {{
add(ServerEndpointConfig.Builder
.create(MeuEndpoint.class, "/websocket")
.configurator(new ServerEndpointConfig.Configurator() {
@Override
public void modifyHandshake(ServerEndpointConfig sec,
HandshakeRequest request,
HandshakeResponse response) {
//. . .
}
})
.build());
}};
}
No código acima a classe abstrata ServerEndpointConfig.Configurator oferece vários método para configurar o Endpoint tais como: o fornecimento de algoritmos de configuração personalizada, interceptação do handshake de abertura, ou o fornecimento de métodos e algoritmos arbitrários que podem ser acessados a partir de cada instância de Endpoint configurado com este configurador.
Ainda no código acima o método modifyHandshake é usado para interceptar o handshake de abertura. ServerEndpointConfig é o objeto de configuração Endpoint usado para configurar este Endpoint. HandshakeRequest fornece informações sobre a requisição HTTP do tipo GET do WebSocket definido para a abertura de handshake. Esta classe fornece acesso a informações tais como a lista de cabeçalhos do HTTP que vem com a requisição ou o HttpSession que o handshake solicitado era parte. O objeto HandshakeResponse identifica a resposta HTTP do handshake preparado pelo container.
Podemos sobrescrever outros métodos de ServerEndpointConfig.Configurator para customizar o comportamento do Endpoint. Por exemplo, podemos especificar a lista de extensões suportadas pelo Endpoint sobrescrevendo o método getNegotiatedExtensions, e especificar a lista de subprotocolos suportados pelo Endpoint sobrescrevendo o método getNegotiatedSubprotocol.
Para Endpoints publicados na plataforma Java EE temos suporte completo a injeção de dependência que esta disponível na especificação do CDI. Injeção em campos, métodos e construtor estão disponíveis em todas as classes Endpoint. Interceptores podem ser habilitados para essas classes através do mecanismo padrão. Segue abaixo um exemplo de injeção de dependência:
Listagem 27. Exemplo de como injetar uma dependência no nosso Endpoint.
public class MeuEndpoint extends Endpoint {
@Inject MeuBean bean;
//. . .
}
Neste código, o bean MeuBean é injetado através do mecanismo de injeção padrão.
Criando um Cliente Endpoint Anotado
Podemos converte um POJO em um Cliente Endpoint usando a anotação @ClientEndpoint. Segue abaixo um exemplo:
Listagem 28. Exemplo de como tornar um POJO em um cliente Endpoint.
@ClientEndpoint
public class MeuClienteEndpoint {
//. . .
}
A anotação @ClientEndpoint decora a classe como um Cliente Endpoint. A anotação pode incluir os atributos descritos na Tabela 2.
Atributo |
Opcional/Obrigatório |
Valor |
configurator |
Opcional |
O valor é uma classe do tipo configurador personalizada que é utilizada para fornecer uma configuração personalizada de novas instâncias desse Endpoint. Esta será uma implementação de ClientEndpointConfig.Configurator. |
encoders |
Opcional |
Conjunto ordenado de codificadores utilizados por este Endpoint. |
decoders |
Opcional |
Conjunto ordenado de decodificadores utilizados por este Endpoint. |
subprotocols |
Opcional |
Conjunto ordenado de protocolos WebSocket utilizados por este Endpoint. |
Tabela 2. Atributos da anotação @ClientEndpoint.
Podemos interceptar os eventos do ciclo de vida especificando as anotações @OnOpen, @OnClose, e @OnError nos métodos. Segue abaixo um exemplo:
Listagem 29. Exemplo de como interceptar os eventos do ciclo de vida através de anotações.
@ClientEndpoint
public class MeuClienteEndpoint {
@OnOpen
public void abrir(Session s) {
//. . .
}
@OnClose
public void fechar(CloseReason c) {
//. . .
}
@OnError
public void erro(Throwable t) {
//. . .
}
}
No código acima o método “abrir” é chamado quando uma nova conexão é estabelecida com este Endpoint. O parâmetro “s” fornece mais detalhes sobre o outro extremo da conexão. O método “fechar” é chamado quando a conexão é finalizada. O parâmetro “c” fornece mais detalhes sobre porque uma conexão com um WebSocket foi encerrada. O método “erro” é chamado quando existe um erro na conexão. O parâmetro “t” fornece mais detalhes sobre o erro.
Uma nova mensagem de saída a partir do cliente para o Endpoint pode ser enviada durante a iniciação da conexão, por exemplo, no método “abrir”:
Listagem 30. Exemplo de como enviar uma mensagem durante a iniciação da conexão.
@OnOpen
public void abrir(Session session) {
try {
session.getBasicRemote().sendText("Texto de exemplo!");
} catch (IOException ex) {
//. . .
}
}
Uma mensagem de entrada a partir do Endpoint pode ser recebida em qualquer método desde que ele esteja decorado com a anotação @OnMessage. Segue um exemplo abaixo:
Listagem 31. Exemplo de como receber uma mensagem no Endpoint.
@OnMessage
public void processaMensagem(String mensagem, Session session) {
//. . .
}
No código acima o método “processaMensagem” é invocado quando uma mensagem é recebida de um Endpoint. O parâmetro “mensagem” é a carga útil da mensagem. O parâmetro session fornece mais detalhes sobre a outra extremidade da conexão.
O cliente pode conectar-se ao Endpoint via ContainerProvider. Segue abaixo um exemplo de como isso poderia ser realizado:
Listagem 32. Exemplo de como conectar-se ao Endpoint através do ContainerProvider.
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
String uri = "ws://localhost:8080/minhaApp/websocket";
container.connectToServer(MeuCliente.class, URI.create(uri));
No código acima ContainerProvider usa o ServiceLoader para carregar uma implementação de ContainerProvider e fornece uma nova instância de WebSocketContainer. WebSocketContainer nos permite inicializar um handshake com o Endpoint. O servidor Endpoint é publicado na URI ws://localhost:8080/minhaApp/websocket. O cliente se conecta ao Endpoint invocando o método connectToServer e fornece a classe do cliente decorada e a URI do Endpoint. Este método bloqueia até que a conexão seja estabelecida, ou lança um erro se a conexão não puder ser estabelecida ou se existe um problema com a classe Endpoint que foi fornecida.
Podemos usar um atributo “configurator” opcional para especificar uma classe de configuração personalizada para configurar novas instâncias deste Endpoint:
Listagem 33. Exemplo utilizando um atributo configurator para especificar uma classe de configuração personalizada.
public class MeuConfigurator extends ClientEndpointConfig.Configurator {
@Override
public void beforeRequest(Map<String, List<String>> headers) {
//. . .
}
//. . .
}
@ClientEndpoint(configurator = MeuConfigurator.class)
public class MeuClienteEndpoint {
//. . .
}
No código acima a classe MeuConfigurator fornece uma implementação de ClientEndpointConfig.Configurator. Esta classe abstrata fornece dois métodos para configurar o Endpoint Cliente: beforeRequest e afterResponse. O método beforeRequest é chamado após o pedido de handshake que será usado para iniciar a conexão com o servidor, mas antes de qualquer parte da solicitação ser enviada. O método afterResponse é chamado após uma resposta de handshake ser recebida do servidor como um resultado de uma interação, realizada através de um handshake, que o próprio servidor iniciou. O parâmetro headers é um mapa mutável de cabeçalhos de solicitação do handshake. O atributo configurator é usado para especificar a classe configuradora personalizada.
Quanto aos Servidores Endpoint baseados em anotação, as anotações dos métodos não são passados para baixo na hierarquia de herança de classe do Java. Eles se aplicam apenas para as classes que estão anotadas. Por exemplo, uma classe Java que herda de uma classe Java anotada com uma anotação @ClientEndpoint a nível de classe não se torna um Endpoint anotado, a menos que ele próprio esteja anotado com uma anotação @ClientEndpoint. Subclasses que sobrescrevem métodos anotados com anotações de métodos do WebSocket não obtém os callbacks do WebSocket a menos que esses métodos das subclasses sejam marcados com uma anotação do WebSocket a nível de método.
Criando um Cliente Endpoint Programático
Também podemos criar um Cliente Endpoint apenas estendo a classe Endpoint. Este Endpoint também é chamado de um Endpoint programático.
Listagem 34. Exemplo de como criar um cliente Endpoint estendendo a classe Endpoint.
public class MeuClienteEndpoint extends Endpoint {
@Override
public void onOpen(final Session session, EndpointConfig ec) {
//. . .
}
}
No código acima temos o método onOpen que é chamado quando uma nova conexão é iniciada. EndpointConfig identifica o objeto de configuração usado para configurar este Endpoint.
Este Endpoint é configurado através de múltiplos MessageHandlers.
Podemos iniciar uma comunicação síncrona ou assíncrona com a outra extremidade da comunicação usando session.getBasicRemote ou session.getAsyncRemote, respectivamente.
Também podemos receber toda a mensagem registrando o gerenciador Messagehandler.Whole<T>, onde T é uma String para mensagens de texto, e ByteBuffer ou byte[] é para mensagens binárias. Recebemos uma mensagem multiparte sobrescrevendo MessageHandler.Partial<T>.
O Cliente Endpoint programático pode conectar-se ao Endpoint através de ContainerProvider. Segue abaixo um exemplo:
Listagem 35. Exemplo de como conectar um cliente Endpoint utilizando um ContainerProvider.
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
String uri = "ws://localhost:8080/minhaApp/websocket";
container.connectToServer(MeuClientEndpoint.class, null, URI.create(uri));
No código acima ContainerProvider usa o ServiceLoader para carregar uma implementação de ContainerProvider e fornece uma nova instancia de WebSocketContainer. WebSocketContainer nos permite iniciar um handshake WebSocket com o Endpoint. O Servidor Endpoint é publicado na URI ws://localhost:8080/minhaApp/websocket. O cliente se conecta ao Endpoint invocando o método connectToServer e fornece como parâmetro o Cliente Endpoint programático e o URI do Endpoint como parâmetros. Este método bloqueia até que a conexão seja estabelecida, ou lança um erro se ou a conexão não puder ser realizada ou existiu um problema com a classe Endpoint fornecida. Podemos usar as configurações default do Cliente Endpoint passando null como o segundo parâmetro.
Podemos configurar um Cliente Endpoint programaticamente fornecendo uma instância de ClientEndpointConfig.Configurator:
Listagem 36. Exemplo de como configurar um cliente Endpoint através de uma instância de ClientEndpointConfig.Configurator.
public class MeuConfigurator extends ClientEndpointConfig.Configurator {
@Override
public void beforeRequest(Map<String, List<String>> headers) {
//. . .
}
@Override
public void afterResponse(HandshakeResponse response) {
//. . .
}
}
No código acima a classe MeuConfigurator fornece uma implementação de ClientEndpointConfig.Configurator. Esta classe abstrata fornece dois métodos para configurar o Cliente Endpoint: beforeRequest e afterResponse. O método beforeRequest é chamado após a solicitação de handshake que será usada para iniciar a conexão com o servidor, mas antes que qualquer parte da requisição seja enviada. O método afterResponse é chamado após a resposta do handshake ser recebida do servidor como um resultado de uma interação de handshake que iniciou.
Um elemento de configuração também pode ser especificado no connectToServer. Segue abaixo um exemplo:
Listagem 37. Exemplo de como configurar um elemento de configuração no método connectToServer.
container.connectToServer(MeuClienteEndpoint.class,
ClientEndpointConfig
.Builder
.create()
.configurator(new MeuConfigurator()).build(), URI.create(uri));
Para Endpoints publicados na plataforma Java EE, está disponível suporte completo a injeção de dependência como descrito na especificação CDI. Injeção em campos, métodos e construtores estão disponíveis em todas as classes Endpoint do WebSocket. Interceptadores podem estar habilitados para essas classes através de mecanismos padrões. Segue abaixo um exemplo de como utilizar a injeção de dependência:
Listagem 38. Exemplo de como utilizar injeção de dependência no Endpoint.
public class MeuClienteEndpoint extends Endpoint {
@Inject MeuBean bean;
//. . .
}
No código acima o bean MeuBean é injetado usando o mecanismo padrão de injeção.
Criando um Cliente WebSocket com JavaScript
Podemos invocar um Endpoint WebSocket usando a API JavaScript definida pelo W3C. A API nos permite conectar a um Endpoint WebSocket através de uma URL e uma lista opcional de subprotocolos. Segue um exemplo abaixo:
var websocket = new WebSocket("ws://localhost:8080/minhaapp/chat");
No código acima invocamos o construtor da classe WebSocket através da especificação de uma URI onde o Endpoint está publicado. O esquema de protocolo “ws://” define a URL para ser um Endpoint WebSocket. O esquema “wss://” pode ser usado para iniciar uma conexão segura. Ainda neste código o Endpoint WebSocket é hospedado no host “localhost” e na porta 8080. A aplicação é publicada na raiz da aplicação. O Endpoint é publicado na URI /chat. Podemos ainda especificar um array opcional de subprotocolos no construtor; o valor default é um array vazio. Uma conexão WebSocket estabelecida está disponível na variável websocket deste JavaScript.
A API define manipuladores de eventos que são invocados para diferentes métodos do ciclo de vida, são eles:
- O manipulador de eventos “onopen” é chamado quando uma nova conexão é iniciada.
- O manipulador de eventos “onerror” é chamado quando um erro é recebido durante a comunicação.
- O manipulador de eventos “onclose” é chamado quando a conexão é finalizada.
Segue abaixo como podemos utilizar estes manipuladores de eventos:
Listagem 39. Exemplo de como utilizar manipuladores de eventos.
websocket.onopen = function() {
//. . .
}
websocket.onerror = function(evt) {
//. . .
}
websocket.onclose = function() {
//. . .
}
Os Dados em formato de texto ou binário podem ser enviados através de qualquer um dos métodos “send” conforme definido abaixo:
websocket.send(meuCampo.value);
Este código lê os valores de entrada em um campo de texto, meuCampo, e envia isto como uma mensagem de texto. Segue abaixo um exemplo de um dado binário:
Listagem 40. Exemplo de como enviar dados binários.
websocket.binaryType = "arraybuffer";
var buffer = new ArrayBuffer(meuCampo.value);
var bytes = new Uint8Array(buffer);
for (var i=0; i<bytes.length; i++) {
bytes[i] = i;
}
websocket.send(buffer);
Este código lê o valor de entrada em um campo de texto chamado meuCampo, cria um arraybuffer do tamanho especificado no campo, e envia isto como uma mensagem binária para o Endpoint WebSocket. O atributo binaryType pode ser setado para blob ou arraybuffer para enviar diferentes tipos de informações binárias.
Uma mensagem pode ser recebida através do manipulador de eventos onmessage:
Listagem 41. Exemplo receber mensagens através do manipulador de eventos onmessage.
websocket.onmessage = function(evt) {
console.log("mensagem recebida: " + evt.data);
}
O WebSocket define um protocolo de mensagem de baixo nível. Quaisquer padrões de troca de mensagens tais como request-response, precisam ser explicitamente construídos em um nível de aplicação.
Codificadores e Decodificadores
A Aplicação pode receber e enviar uma carga útil em texto simples e em formato binário. Podemos converter a carga útil do texto para uma classe específica da aplicação implementando Decoder.Text<T> e Encoder.Text<T>. Também podemos converter a carga útil binária para uma classe específica de aplicação implementando as interfaces Decoder.Binary<T> e Encoder.Binary<T>.
O JSON é um formato típico para uma carga útil em texto. Ao invés de receber a carga útil como texto e então convertê-lo num JsonObject (por exemplo, utilizando as APIs definidas no javax.json package), podemos definir uma classe específica de aplicação para capturar o JsonObject:
Listagem 42. Exemplo de uma classe que captura um objeto Json.
public class MinhaMensagem {
private JsonObject jsonObject;
//. . .
}
MinhaMensagem é uma classe específica de uma aplicação e contém um JsonObject para capturar a carga útil da mensagem.
A interface Decoder.Text<T> pode ser implementada para decodificar uma sequencia de carga útil da entrada para uma classe específica de aplicação:
Listagem 43. Exemplo de uma classe para decodificar mensagens.
public class MeuDecodificadorDeMensagem implements Decoder.Text<MinhaMensagem> {
@Override
public MinhaMensagem decode(String string) throws DecodeException {
MinhaMensagem minhaMensagem = new MinhaMensagem(
Json.createReader(new StringReader(string)).readObject()
);
return minhaMensagem;
}
@Override
public boolean willDecode(String string) {
return true;
}
//. . .
}
Este código mostra como uma carga útil do tipo String é decodificada para o tipo MinhaMensagem. O método decode decodifica o parâmetro String em um objeto de tipo MinhaMensagem, e o método decode retornará “true” se a string puder ser decodificada em um objeto de tipo MinhaMensagem. APIs do padrão javax.json.* são usadas para gerar a representação JSON de uma string. Segue abaixo um exemplo:
Listagem 44. Exemplo de uma classe para codificar mensagens.
public class MeuCodificadorDeMensagem implements Encoder.Text<MinhaMensagem> {
@Override
public String encode(MinhaMensagem minhaMensagem) throws EncodeException {
return minhaMensagem.getJsonObject().toString();
}
//. . .
}
Este código define como um tipo MinhaMensagem é codificado para uma String, e o método encode codifica o parâmetro da mensagem em uma String.
Podemos especificar os codificadores e decodificadores em um Endpoint anotado usando os atributos encoders e decoders da anotação @ServerEndpoint. Segue abaixo um exemplo:
Listagem 45. Exemplo de como especificar codificadores e decodificadores em um Endpoint.
@ServerEndpoint(value = "/encoder",
encoders = {MeuCodificadorDeMensagem.class},
decoders = {MeuDecodificadorDeMensagem.class})
public class MeuEndpoint {
//. . .
}
Múltiplos codificadores e decodificadores também podem ser especificados. Segue abaixo um exemplo:
Listagem 46. Exemplo de como especificar codificadores e decodificadores.
@ServerEndpoint(value = "/encoder",
encoders = {MeuCodificadorDeMensagem1.class, MeuCodificadorDeMensagem 2.class},
decoders = {MeuDecodificadorDeMensagem1.class, MeuDecodificadorDeMensagem2.class})
public class MeuEndpoint {
//. . .
}
O primeiro codificador equivalente com o tipo dado será usado. O primeiro decodificador onde o método “decode” retorna “true” será utilizado.
Os codificadores e decodificadores podem ser especificados em um servidor Endpoint programático durante a configuração do Endpoint em ServerEndpointConfig.Builder. Segue abaixo um exemplo:
Listagem 47. Exemplo de como especificar um codificador e decodificador em um servidor Endpoint através de um ServerEndpointConfig.Builder.
public class MinhaConfiguracaoDeEndpoint implements ServerApplicationConfig {
List<Class<? extends Encoder>> encoders = new ArrayList<>();
List<Class<? extends Decoder>> decoders = new ArrayList<>();
public MinhaConfiguracaoDeEndpoint() {
encoders.add(MeuCodificadorDeMensagem.class);
decoders.add(MeuDecodificadorDeMensagem.class);
}
@Override
public Set<ServerEndpointConfig> getEndpointConfigs(
Set<Class<? extends Endpoint>> set) {
return new HashSet<ServerEndpointConfig>() {
{
add(ServerEndpointConfig
.Builder
.create(MeuEndpoint.class, "/chat")
.encoders(encoders)
.decoders(decoders)
.build());
}
};
}
}
//. . .
}
No código acima a lista de Encoder e Decoder é inicializada no construtor e configura a implementação do codificador e do decodificador usando os métodos “encoders” e “decoders”.
Podemos especificar o codificador e o decodificador no Cliente Endpoint usando os atributos “encoders” e “decoders” da anotação @ClientEndpoint. Segue abaixo um exemplo:
Listagem 48. Exemplo de como especificar um codificador e um decodificador em um @ClientEndpoint.
@ClientEndpoint(
encoders = {MeuCodificadorDeMensagem.class},
decoders = {MeuDecodificadorDeMensagem.class}
)
public class MeuClienteEndpoint {
@OnOpen
public void onOpen(Session session) {
MinhaMensagem mensagem = new MinhaMensagem("{ \"foo\" : \"bar\"}");
session.getBasicRemote().sendObject(mensagem);
}
}
No código acima MeuCodificadorDeMensagem é especificado através do atributo encoders. MeuDecodificadorDeMensagem é especificado através do atributo decoders. O objeto MinhaMensagem é inicializado com uma carga útil JSON. O Cliente Endpoint envia uma mensagem usando “sendObject” ao invés de “sendString”.
Podemos especificar o codificador e o decodificador no cliente programático durante a configuração do Endpoint usando ClientEndpointConfig.Builder. Segue abaixo um exemplo:
Listagem 49. Exemplo de como especificar um codificador e um decodificador em um cliente Endpoint através de um ClientEndpointConfig.Builder.
List<Class<? extends Encoder>> encoders = new ArrayList<>();
List<Class<? extends Decoder>> decoders = new ArrayList<>();
encoders.add(MeuCodificadorDeMensagem.class);
decoders.add(MeuDecodificadorDeMensagem.class);
WebSocketContainer container = ContainerProvider.getWebSocketContainer();
String uri = "ws://localhost:8080" + request.getContextPath() + "/websocket";
container.connectToServer(MeuCliente.class, ClientEndpointConfig.Builder.create().encoders(encoders).decoders(decoders).build(),URI.create(uri));
Neste código, a lista de Encoder e Decoder é inicializada com as implementações encoder e decoder. Os métodos “encoders” e “decoders” no ClientEndpointConfig.Builder podem ser usados para configurar o “encoders” e o “decoders”.
Integração com Java EE Security
Um WebSocket mapeado para um dado URI do tipo “ws://” é protegido no Deployment Descriptor através de um URI do tipo “http://” com o mesmo hostname, porta, e caminho. A autenticação e autorização do WebSocket para um Endpoint forma um mecanismo de segurança baseado em Servlet.
Um WebSocket que requer autenticação somente recebe um handshake de abertura após a aplicação Web autenticar o usuário. Tipicamente este procedimento será realizado por uma autenticação HTTP (BASIC ou FORM) na aplicação web.
Assim, os desenvolvedores de WebSocket podem atribuir um esquema de autenticação, um acesso baseado em papéis de usuário e uma garantia de transporte para os seus Endpoints WebSocket.
Podemos configurar uma autenticação “BASIC” usando o Deployment Descriptor web.xml conforme exemplificado abaixo:
Listagem 50. Configurando mecanismos de autenticação no arquivo web.xml.
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<security-constraint>
<web-resource-collection>
<web-resource-name>WebSocket Endpoint</web-resource-name>
<url-pattern>/*</url-pattern>
<http-method>GET</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>g1</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>file</realm-name>
</login-config>
<security-role>
<role-name>g1</role-name>
</security-role>
</web-app>
No código acima todas as solicitações HTTP do tipo GET requerem autenticações básicas definidas por BASIC em <authmethod>.
Invocando qualquer página no aplicativo irá solicitar que o usuário digite um nome de usuário e senha. As credenciais inseridas devem corresponder a um dos usuários no grupo “g1”. Todos os pedidos posteriores, incluindo o handshake de abertura, ocorrerão no pedido autenticado.
Se um cliente enviar uma requisição de handshake de abertura não autenticado para um WebSocket que esta protegido pelo mecanismo de segurança, uma resposta 401 (Unauthorized) para requisição de handshake de abertura é retornada e a conexão do WebSocket não é inicializada.
A garantia de transporte do tipo NONE permite conexões ws:// sem criptografia para o WebSocket. Por outro lado, a garantia de transporte do tipo CONFIDENTIAL apenas permite acesso ao WebSocket através de uma conexão criptografada wss://.
Exemplo Utilizando WebSockets
No exemplo abaixo será demonstrado como podemos criar um Chat simples utilizando a tecnologias Web e WebSockets.
Primeiramente o exemplo foi realizado utilizando o Eclipse Juno e o servidor de aplicação Glassfish que fornece a implementação de referencia para a especificação WebSocket para Java. A versão do Glassfish utilizada é a 4.0. Talvez o seu eclipse não tenha o plugin do Glassfish, dessa forma basta ir em “Help” no menu superior do eclipse e ir na opção “Install New Software”. Digite a URL http://download.oracle.com/otn_software/oepe/juno/wtp/ e baixe o Plugin. Quando tentarmos adicionar um servidor já deve aparecer a opção pelo Glassfish. Para isso clique com o botão direito do mouse em “Servers” e selecione “New” e por fim “Server” no Eclipse conforme mostra a Figura 1.
Figura 1. Adicionando um novo servidor no eclipse.
Após isso basta selecionar o servidor de aplicação Glassfish na tela “Define New Server”, conforme ilustra a Figura 2.
Figura 2. Selecionando o servidor Glassfish.
Agora que o Glassfish está instalado basta criar um projeto do tipo “Dynamic Web Project”. Primeiramente criamos um arquivo index.jsp conforme definido abaixo:
Listagem 51. Código do arquivo index.jsp.
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>WebSocket</title>
<script language="javascript" type="text/javascript">
var path = window.location.pathname;
var contextoWeb = path.substring(0, path.indexOf('/', 1));
var endPointURL = "ws://" + window.location.host + contextoWeb + "/chat";
var chatClient = null;
function connect () {
chatClient = new WebSocket(endPointURL);
chatClient.onmessage = function (event) {
var messagesArea = document.getElementById("mensagens");
var jsonObj = JSON.parse(event.data);
var message = jsonObj.user + ": " + jsonObj.message + "\r\n";
messagesArea.value = messagesArea.value + message;
messagesArea.scrollTop = messagesArea.scrollHeight;
};
}
function disconnect () {
chatClient.close();
}
function enviaMensagem() {
var user = document.getElementById("usuario").value.trim();
if (user === "")
alert ("Digite o seu nome!");
var inputElement = document.getElementById("messageInput");
var message = inputElement.value.trim();
if (message !== "") {
var jsonObj = {"user" : user, "message" : message};
chatClient.send(JSON.stringify(jsonObj));
inputElement.value = "";
}
inputElement.focus();
}
</script>
</head>
<body onload="connect();" onunload="disconnect();">
<h1> Exemplo de Chat </h1>
<textarea id="mensagens" readonly></textarea>
<div class="panel input-area">
<input id="usuario" type="text" placeholder="Nome"/>
<input id="messageInput" type="text" placeholder="Digite a mensagem"
onkeydown="if (event.keyCode == 13) enviaMensagem();" />
<input class="button" type="submit" value="Enviar" onclick="enviaMensagem();" />
</div>
</body>
</html>
Por fim, criamos o nosso Endpoint que irá conversar com a nossa página Web. Segue abaixo o código do Endpoint:
package com.exemplo; import java.util.Collections; import java.util.HashSet; import java.util.Set; import javax.inject.Singleton; import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.Session; import javax.websocket.OnOpen; import javax.websocket.server.ServerEndpoint; @ServerEndpoint(value="/chat") @Singleton public class ChatServerEndPoint { Set<Session> userSessions = Collections.synchronizedSet(new HashSet<Session>()); @OnOpen public void abrir(Session userSession) { System.out.println("Nova solicitação recebida. Id: " + userSession.getId()); userSessions.add(userSession); } @OnClose public void fechar(Session userSession) { System.out.println("Conexão encerrada. Id: " + userSession.getId()); userSessions.remove(userSession); } @OnMessage public void recebeMensagem(String mensagem, Session userSession) { System.out.println("Mensagem Recebida: " + mensagem); for (Session session : userSessions) { System.out.println("Enviando para " + session.getId()); session.getAsyncRemote().sendText(mensagem); } } }
Segue na Figura 3 a tela do aplicativo criado.
Figura 3. Exemplo do aplicativo criado.
Qualquer usuário que possua a URL pode se junto ao chat com outras pessoas apenas entrando com seu nome e a mensagem. A mensagem é enviada para o servidor quando o usuário clica em "Enviar". Após isso o servidor enviará um broadcast desta mensagem para todos os usuários.
Além do Endpoint para enviar as mensagens de um usuários para todos os outros participantes e uma página HTML que exibe os campos de entrada, o histórico de mensagens e um botão para enviar mensagens temos um código JavaScript que se conecta com o servidor, envia mensagens para o servidor e gerencia as mensagens que vem do servidor. No código JavaScript temos o método connect() que é invocado quando a página é carregada e se conecta ao servidor Endpoint. O método disconnect() é invocado quando a página é descarregada e desconecta o cliente do servidor Endpoint. Por fim, enviaMensagem() é invocado quando clicarmos em Enviar ou pressionamos enter na caixa de texto.
Bibliografia
[1]Josh Juneau. Java EE 7 Recipes: A Problem-Solution Approach. Apress, 2013.
[2]Josh Juneau. Introducing Java EE 7: A Look at What's New. Apress, 2013.
[3]Arun Gupta. Java EE 7 Essentials. O'Reilly, 2013.