Una guía de la API de Java para WebSocket

1. Información general

WebSocket proporciona una alternativa a la limitación de la comunicación eficiente entre el servidor y el navegador web al proporcionar comunicaciones bidireccionales, full-duplex y en tiempo real entre el cliente y el servidor. El servidor puede enviar datos al cliente en cualquier momento. Debido a que se ejecuta sobre TCP, también proporciona una comunicación de bajo nivel de baja latencia y reduce la sobrecarga de cada mensaje .

En este artículo, veremos la API de Java para WebSockets creando una aplicación similar a un chat.

2. JSR 356

JSR 356 o la API de Java para WebSocket, especifica una API que los desarrolladores de Java pueden utilizar para integrar WebSockets en sus aplicaciones, tanto en el lado del servidor como en el lado del cliente Java.

Esta API de Java proporciona componentes del lado del servidor y del cliente:

  • Servidor : todo el contenido del paquete javax.websocket.server .
  • Cliente : el contenido del paquete javax.websocket , que consta de API del lado del cliente y también bibliotecas comunes tanto para el servidor como para el cliente.

3. Creación de un chat con WebSockets

Construiremos una aplicación similar a un chat muy simple. Cualquier usuario podrá abrir el chat desde cualquier navegador, escribir su nombre, iniciar sesión en el chat y comenzar a comunicarse con todos los que estén conectados al chat.

Comenzaremos agregando la última dependencia al archivo pom.xml :

 javax.websocket javax.websocket-api 1.1 

La última versión se puede encontrar aquí.

Para convertir objetos Java en sus representaciones JSON y viceversa, usaremos Gson:

 com.google.code.gson gson 2.8.0 

La última versión está disponible en el repositorio de Maven Central.

3.1. Configuración de punto final

Hay dos formas de configurar puntos finales: basadas en anotaciones y basadas en extensiones. Puede extender la clase javax.websocket.Endpoint o usar anotaciones de nivel de método dedicadas. Como el modelo de anotación conduce a un código más limpio en comparación con el modelo programático, la anotación se ha convertido en la opción convencional de codificación. En este caso, los eventos del ciclo de vida del punto final de WebSocket se manejan mediante las siguientes anotaciones:

  • @ServerEndpoint: si está decorado con @ServerEndpoint, el contenedor garantiza la disponibilidad de la clase como un servidor WebSocket que escucha un espacio URI específico
  • @ClientEndpoint : una clase decorada con esta anotación se trata como un cliente WebSocket
  • @OnOpen : el contenedor invoca un método Java con @OnOpen cuando se inicia una nueva conexión WebSocket
  • @OnMessage : un método Java, anotado con @OnMessage, recibe la información del contenedor WebSocket cuando se envía un mensaje al punto final.
  • @OnError : se invoca un método con @OnError cuando hay un problema con la comunicación
  • @OnClose : se utiliza para decorar un método Java al que llama el contenedor cuando se cierra la conexión WebSocket

3.2. Escribir el punto final del servidor

Declaramos un extremo del servidor WebSocket de clase Java anotándolo con @ServerEndpoint . También especificamos el URI donde se implementa el punto final. El URI se define en relación con la raíz del contenedor del servidor y debe comenzar con una barra inclinada:

@ServerEndpoint(value = "/chat/{username}") public class ChatEndpoint { @OnOpen public void onOpen(Session session) throws IOException { // Get session and WebSocket connection } @OnMessage public void onMessage(Session session, Message message) throws IOException { // Handle new messages } @OnClose public void onClose(Session session) throws IOException { // WebSocket connection closes } @OnError public void onError(Session session, Throwable throwable) { // Do error handling here } }

El código anterior es el esqueleto del punto final del servidor para nuestra aplicación tipo chat. Como puede ver, tenemos 4 anotaciones asignadas a sus respectivos métodos. A continuación puede ver la implementación de dichos métodos:

@ServerEndpoint(value="/chat/{username}") public class ChatEndpoint { private Session session; private static Set chatEndpoints = new CopyOnWriteArraySet(); private static HashMap users = new HashMap(); @OnOpen public void onOpen( Session session, @PathParam("username") String username) throws IOException { this.session = session; chatEndpoints.add(this); users.put(session.getId(), username); Message message = new Message(); message.setFrom(username); message.setContent("Connected!"); broadcast(message); } @OnMessage public void onMessage(Session session, Message message) throws IOException { message.setFrom(users.get(session.getId())); broadcast(message); } @OnClose public void onClose(Session session) throws IOException { chatEndpoints.remove(this); Message message = new Message(); message.setFrom(users.get(session.getId())); message.setContent("Disconnected!"); broadcast(message); } @OnError public void onError(Session session, Throwable throwable) { // Do error handling here } private static void broadcast(Message message) throws IOException, EncodeException { chatEndpoints.forEach(endpoint -> { synchronized (endpoint) { try { endpoint.session.getBasicRemote(). sendObject(message); } catch (IOException | EncodeException e) { e.printStackTrace(); } } }); } }

Cuando un nuevo usuario inicia sesión ( @OnOpen ) se asigna inmediatamente a una estructura de datos de usuarios activos. Luego, se crea un mensaje y se envía a todos los puntos finales mediante el método de transmisión .

Este método también se utiliza cada vez que se envía un mensaje nuevo ( @OnMessage ) por cualquiera de los usuarios conectados; este es el objetivo principal del chat.

Si en algún momento ocurre un error, el método con la anotación @OnError lo maneja. Puede utilizar este método para registrar la información sobre el error y borrar los puntos finales.

Finalmente, cuando un usuario ya no está conectado al chat, el método @OnClose borra el punto final y transmite a todos los usuarios que un usuario ha sido desconectado.

4. Tipos de mensajes

La especificación WebSocket admite dos formatos de datos en línea: texto y binario. La API admite ambos formatos, agrega capacidades para trabajar con objetos Java y mensajes de verificación de estado (ping-pong) como se define en la especificación:

  • Texto : cualquier dato textual ( java.lang.String , primitivas o sus clases contenedoras equivalentes)
  • Binario : datos binarios (por ejemplo, audio, imagen, etc.) representados por un java.nio.ByteBuffer o un byte [] (matriz de bytes)
  • Objetos Java : la API hace posible trabajar con representaciones nativas (objetos Java) en su código y usar transformadores personalizados (codificadores / decodificadores) para convertirlos en formatos compatibles en línea (texto, binario) permitidos por el protocolo WebSocket.
  • Ping-Pong : Un javax.websocket.PongMessage es un reconocimiento enviado por un par de WebSocket en respuesta a una solicitud de verificación de estado (ping)

Para nuestra aplicación, usaremos objetos Java. Crearemos las clases para codificar y decodificar mensajes.

4.1. Codificador

Un codificador toma un objeto Java y produce una representación típica adecuada para la transmisión como un mensaje como JSON, XML o representación binaria. Los codificadores se pueden utilizar implementando las interfaces Encoder.Text o Encoder.Binary .

En el siguiente código, definimos la clase Message a codificar y en el método encode usamos Gson para codificar el objeto Java en JSON:

public class Message { private String from; private String to; private String content; //standard constructors, getters, setters }
public class MessageEncoder implements Encoder.Text { private static Gson gson = new Gson(); @Override public String encode(Message message) throws EncodeException { return gson.toJson(message); } @Override public void init(EndpointConfig endpointConfig) { // Custom initialization logic } @Override public void destroy() { // Close resources } }

4.2. Descifrador

A decoder is the opposite of an encoder and is used to transform data back into a Java object. Decoders can be implemented using the Decoder.Text or Decoder.Binary interfaces.

As we saw with the encoder, the decode method is where we take the JSON retrieved in the message sent to the endpoint and use Gson to transform it to a Java class called Message:

public class MessageDecoder implements Decoder.Text { private static Gson gson = new Gson(); @Override public Message decode(String s) throws DecodeException { return gson.fromJson(s, Message.class); } @Override public boolean willDecode(String s) { return (s != null); } @Override public void init(EndpointConfig endpointConfig) { // Custom initialization logic } @Override public void destroy() { // Close resources } }

4.3. Setting Encoder and Decoder in Server Endpoint

Let's put everything together by adding the classes created for encoding and decoding the data at the class level annotation @ServerEndpoint:

@ServerEndpoint( value="/chat/{username}", decoders = MessageDecoder.class, encoders = MessageEncoder.class )

Every time messages are sent to the endpoint, they will automatically either be converted to JSON or Java objects.

5. Conclusion

In this article, we looked at what is the Java API for WebSockets and how it can help us building applications such as this real-time chat.

We saw the two programming models for creating an endpoint: annotations and programmatic. We defined an endpoint using the annotation model for our application along with the life cycle methods.

Also, in order to be able to communicate back and forth between the server and client, we saw that we need encoders and decoders to convert Java objects to JSON and vice versa.

The JSR 356 API is very simple and the annotation based programming model that makes it very easy to build WebSocket applications.

Para ejecutar la aplicación que construimos en el ejemplo, todo lo que necesitamos hacer es implementar el archivo war en un servidor web e ir a la URL: // localhost: 8080 / java-websocket /. Puede encontrar el enlace al repositorio aquí.