Procesamiento de Telemetría en Java con Sockets UDP y TCP: Implementación Paso a Paso
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 respondeERROR;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 generaALERT;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.