Procesamiento de Telemetría en Java con Sockets UDP y TCP: Implementación Paso a Paso

Informática y Telecomunicaciones11,83 KB

Procesamiento de Telemetría con Sockets en Java

A continuación se presenta la implementación de un sistema de telemetría basado en mensajes delimitados por punto y coma, con comunicación mediante sockets UDP y TCP, junto con un ejemplo básico de cliente-servidor.

1. Lógica de procesamiento del protocolo (TelemetryProcessor)

Método process(String rawMessage)

El método valida el mensaje recibido, separa sus campos por ";" y responde según el tipo de solicitud.

Paso 1.1: Validación de nulo o vacío

Si el mensaje es null o está vacío tras aplicar trim(), se retorna INVALID_FORMAT.

Paso 1.2: Separación de campos

Se utiliza split(";", -1). El límite -1 conserva los campos vacíos al final del mensaje.

Paso 1.3: Consulta de estado (STATUS)
  • Debe contener exactamente dos partes y el identificador no puede estar en blanco.
  • Si el dispositivo no existe en lastReadings, se responde ERROR;DEVICE_NOT_FOUND.
  • Si existe, se responde STATUS_OK;DEVICE_ID;SENSOR_TYPE;VALUE.
Paso 1.4: Validación del formato de telemetría

Deben existir exactamente tres partes no vacías: deviceId, sensorType y valueStr. El valor debe poder convertirse a double y no puede ser NaN ni infinito.

Paso 1.6: Validación del tipo de sensor y evaluación de rangos
  • TEMP: mayor a 40.0 genera ALERT;HIGH_TEMPERATURE; menor a 0.0 genera ALERT;FREEZING_TEMPERATURE; en otro caso, OK;TEMP_RECORDED.
  • HUMIDITY: mayor a 90.0 genera una alerta de humedad alta.
  • BATTERY: menor a 20.0 genera una alerta de batería baja.
  • Cualquier otro tipo responde ERROR;UNKNOWN_SENSOR_TYPE.
Paso 1.5: Almacenamiento de lecturas válidas

La lectura válida se guarda en lastReadings asociada al identificador del dispositivo.

public String process(String rawMessage) {
    if (rawMessage == null || rawMessage.trim().isEmpty()) {
        return INVALID_FORMAT;
    }

    String[] parts = rawMessage.trim().split(";", -1);
    if (parts.length == 0) {
        return INVALID_FORMAT;
    }

    if (parts[0].trim().equalsIgnoreCase("STATUS")) {
        if (parts.length != 2 || parts[1].trim().isEmpty()) {
            return INVALID_FORMAT;
        }
        String queriedId = parts[1].trim();
        TelemetryData data = lastReadings.get(queriedId);
        if (data == null) {
            return "ERROR;DEVICE_NOT_FOUND";
        }
        return "STATUS_OK;" + data.getDeviceId() + ";" + data.getSensorType() + ";" + data.getValue();
    }

    if (parts.length != 3) {
        return INVALID_FORMAT;
    }
    String deviceId = parts[0].trim();
    String sensorType = parts[1].trim();
    String valueStr = parts[2].trim();
    if (deviceId.isEmpty() || sensorType.isEmpty() || valueStr.isEmpty()) {
        return INVALID_FORMAT;
    }

    double value;
    try {
        value = Double.parseDouble(valueStr);
    } catch (NumberFormatException e) {
        return INVALID_FORMAT;
    }
    if (Double.isNaN(value) || Double.isInfinite(value)) {
        return INVALID_FORMAT;
    }

    String type = sensorType.toUpperCase();
    String response;
    switch (type) {
        case "TEMP":
            if (value > 40.0) {
                response = "ALERT;HIGH_TEMPERATURE;" + value;
            } else if (value < 0.0) {
                response = "ALERT;FREEZING_TEMPERATURE;" + value;
            } else {
                response = "OK;TEMP_RECORDED;" + value;
            }
            break;
        case "HUMIDITY":
            if (value > 90.0) {
                // ...
            }
            break;
        case "BATTERY":
            if (value < 20.0) {
                // ...
            }
            break;
        default:
            return "ERROR;UNKNOWN_SENSOR_TYPE";
    }

    lastReadings.put(deviceId, new TelemetryData(deviceId, type, value));
    return response;
}

public Map<String, TelemetryData> getLastReadings() {
    return lastReadings;
}

public void clear() {
    lastReadings.clear();
}

2. Servidor UDP

Método handlePacket(DatagramSocket socket, DatagramPacket packet)

Paso 2.1: Conversión de bytes a String

Se decodifican únicamente los bytes válidos del datagrama usando packet.getOffset() y packet.getLength().

Paso 2.2: Procesamiento del mensaje

El mensaje se envía al TelemetryProcessor para obtener la respuesta del protocolo.

Paso 2.3: Construcción de la respuesta

La respuesta se convierte a bytes UTF-8 y se construye un DatagramPacket dirigido al remitente.

Paso 2.4: Envío
public void handlePacket(DatagramSocket socket, DatagramPacket packet) throws IOException {
    String message = new String(packet.getData(), packet.getOffset(),
            packet.getLength(), StandardCharsets.UTF_8);

    String response = this.processor.process(message);

    byte[] responseBytes = response.getBytes(StandardCharsets.UTF_8);
    DatagramPacket responsePacket = new DatagramPacket(
            responseBytes, responseBytes.length, packet.getAddress(), packet.getPort());

    socket.send(responsePacket);
}

/**
 * Detiene el servidor y libera el socket.
 */
public synchronized void stop() {
    this.running = false;
    if (this.socket != null && !this.socket.isClosed()) {
        this.socket.close();
    }
    if (this.listenerThread != null) {
        try {
            this.listenerThread.join(1000);
        } catch (InterruptedException ignored) {
            Thread.currentThread().interrupt();
        }
    }
}

3. Cliente UDP

Método sendAndReceive(String message)

Paso 3.1: Socket con try-with-resources

El socket se cierra automáticamente al finalizar.

Paso 3.2 y 3.3: Timeout de recepción

Se configura setSoTimeout para limitar la espera de la respuesta.

Paso 3.4 a 3.7: Envío y recepción
public String sendAndReceive(String message) throws IOException {
    try (DatagramSocket socket = new DatagramSocket()) {
        socket.setSoTimeout(this.timeoutMs);

        byte[] data = message.getBytes(StandardCharsets.UTF_8);
        DatagramPacket packet = new DatagramPacket(
                data, data.length, InetAddress.getByName(this.serverHost), this.serverPort);
        socket.send(packet);

        byte[] buffer = new byte[1024];
        DatagramPacket responsePacket = new DatagramPacket(buffer, buffer.length);
        socket.receive(responsePacket);

        return new String(responsePacket.getData(), responsePacket.getOffset(),
                responsePacket.getLength(), StandardCharsets.UTF_8).trim();
    }
}

4. Servidor TCP

Método handleClient(Socket clientSocket)

Se abren los flujos de lectura y escritura con codificación UTF-8 dentro de un bloque try-with-resources. Mientras el cliente envíe mensajes, cada uno se procesa y su respuesta se envía seguida de un salto de línea y un flush() inmediato. El ciclo termina cuando readLine() devuelve null.

public void handleClient(Socket clientSocket) throws IOException {
    try (Socket socket = clientSocket;
         BufferedReader in = new BufferedReader(
                 new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
         BufferedWriter out = new BufferedWriter(
                 new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8))) {

        String message;
        while ((message = in.readLine()) != null) {
            String response = this.processor.process(message);
            out.write(response);
            out.write("\n");
            out.flush();
        }
    }
}

5. Cliente TCP

Método sendAndReceive(String message)

Se crea un socket sin conectar, se establece la conexión con timeout, se envía el mensaje con salto de línea y se lee la respuesta. Si el servidor cierra la conexión sin responder, se lanza una IOException.

public String sendAndReceive(String message) throws IOException {
    try (Socket socket = new Socket()) {
        socket.connect(new InetSocketAddress(this.serverHost, this.serverPort), this.timeoutMs);
        socket.setSoTimeout(this.timeoutMs);

        BufferedWriter out = new BufferedWriter(
                new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8));
        BufferedReader in = new BufferedReader(
                new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));

        out.write(message);
        out.write("\n");
        out.flush();

        String response = in.readLine();
        if (response == null) {
            throw new IOException("El servidor cerró la conexión sin enviar respuesta.");
        }
        return response.trim();
    }
}

6. Ejemplo básico cliente-servidor

Servidor

El servidor escucha en el puerto 5000, acepta conexiones de forma continua, recibe un mensaje con readUTF() y responde con un saludo.

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

public class Server {

    public static void main(String[] args) {
        ServerSocket servidor = null;
        Socket sc = null;
        DataInputStream in;
        DataOutputStream out;

        try {
            servidor = new ServerSocket(5000);
            System.out.println("Servidor iniciado");

            while (true) {
                // Socket del cliente
                sc = servidor.accept();
                System.out.println("Cliente conectado");

                // Del cliente al servidor
                in = new DataInputStream(sc.getInputStream());
                out = new DataOutputStream(sc.getOutputStream());

                // Espera el mensaje del cliente
                String mensaje = in.readUTF();
                System.out.println("Mensaje: " + mensaje);
                out.writeUTF("Hola mundo desde el servidor");

                sc.close();
                System.out.println("Cliente desconectado");
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Cliente

El cliente se conecta a 127.0.0.1:5000, envía un mensaje y muestra la respuesta del servidor.

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;

public class Client {

    public static void main(String[] args) {
        final String HOST = "127.0.0.1";
        final int PORT = 5000;
        DataInputStream in;
        DataOutputStream out;

        try {
            Socket sc = new Socket(HOST, PORT);

            // Del cliente al servidor
            in = new DataInputStream(sc.getInputStream());
            out = new DataOutputStream(sc.getOutputStream());

            out.writeUTF("Hola mundo desde el cliente");
            String msg = in.readUTF();
            System.out.println(msg);

            sc.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Conclusión

La implementación combina validación estricta de mensajes, almacenamiento del último estado por dispositivo y dos mecanismos de transporte: UDP, orientado a datagramas sin conexión, y TCP, orientado a flujos con conexión persistente. Ambos enfoques comparten la misma lógica de protocolo, lo que facilita su mantenimiento y prueba.

Descargar

PDF Word HTML