|
Programacion en red |
Trabajo en red. Parte 2
Comunicación mediante el protocolo UDP
La clase DatagramSocket
Un objeto java.net.DatagramSocket es un "conector" a través del cual enviamos y recibimos paquetes UDP. En la literatura técnica estos paquetes se denominan Datagramas.
La forma usual de crear un DatagramSocket para recibir paquetes es especificando un número de puerto en el constructor. De esta forma, este DatagramSocket estará "escuchando" en el puerto especificado, preparado para recibir cualquier paquete entrante.
Si queremos construir un DatagramSocket para enviar paquetes, no es necesario especificar el número de puerto, ya que nos es indiferente. No ocurre lo mismo en el caso anterior, ya que siempre querremos usar un puerto fijo conocido por el resto de aplicaciones.
DatagramSocket ds1 = new DatagramSocket(123); /* Aquí usamos este DatagramSocket para recibir datos... */ /* ... */ /* Hemos terminado, cerramos el socket */ ds1.close(); DatagramSocket ds2 = new Datagramsocket(); /* Aquí lo usamos para transmitir datos... */ /* ... */ /* Hemos terminado, cerramos el socket */ ds2.close();
La clase DatagramPacket
Esta clase representa a los paquetes de datos que vamos a recibir o transmitir a través de los objetos DatagramSocket. Estos paquetes constan de una cabecera (que incluye la dirección de origen y destino del paquete, el puerto, la longitud del paquete, un checksum, etc.) y un cuerpo (donde se encuentra el contenido real del paquete).
En Java accedemos a las distintas partes de un datagrama mediante los métodos de la clase java.net.DatagramPacket.
La forma de construir datagramas es distinta dependiendo de si queremos enviar o recibir datos. En caso de que únicamente queramos recibir, debemos especificar un array de bytes donde almacenar los datos y un número entero con la longitud máxima que queremos recibir.
Si queremos transmitir, debemos especificar el buffer de datos que queremos enviar, la longitud máxima de datos, la dirección y el puerto de destino del datagrama. La dirección de destino se especifica mediante un objeto de tipo InetAddress, mientras que el puerto se indica mediante un número entero.
Veamos un ejemplo, donde enviamos un datagrama a una determinada dirección, suponiendo que tenemos un objeto InetAddress correctamente creado. Nótese el uso del método send() de la clase DatagramSocket para enviar el datagrama:
int tam = 1024;
InetAddress direcc = ...;
byte[] datos = new byte[tam];
int puerto = 543;
for (int n=0;n<tam;n++){
/* Generamos los datos que vamos a enviar */
datos[n] = ...;
}
DatagramSocket ds = new DatagramSocket();
DatagramPacket dp = new DatagramPacket(datos, tam, direcc, puerto);
ds.send(dp); /* Aquí enviamos el paquete */
Como se ve, la clase DatagramPacket solo nos da herramientas para enviar matrices de bytes a través de UDP. Si queremos transmitir datos más complejos (cadenas de texto, enteros largos, números en coma flotante, objetos Java, etc.) debemos ser nosotros los encargados de codificar esa información dentro de un array de bytes.
Si lo que queremos es recibir datos, solo necesitamos reservar espacio para la información entrante (un array de bytes), poner un objeto DatagramSocket escuchando en un puerto y esperar a recibir un paquete mediante el método receive().
int tam = 1024; byte[] buffer = new byte[tam]; int puerto = 987; DatagramSocket ds = new Datagramsocket(puerto); DatagramPacket dp = new DatagramPacket(buffer,tam); ds.receive(dp); // Ahora tenemos en buffer la información que nos interesa
La clase InetAddress
¿Cómo se usa la clase java.net.InetAddress que aparecía en uno de los ejemplos anteriores?.
La forma de crear un objeto InetAddress es mediante el método estático InetAddress.getByName(String), que recibe un nombre de host en notación alfanumérica (por ejemplo "www.etsit.upv.es" o "209.41.57.70" y devuelve un objeto InetAddress con esa dirección. Si la dirección no existe o no puede ser encontrada, este método lanza una UnknownHostException.
Por ejemplo, si queremos mandar una matriz de bytes al puerto 90 de la dirección "www.upv.es", tenemos que escribir:
int tam = ...; int puerto = 90; String maquina = "www.upv.es"; byte[] buffer = new byte[tam]; // ... // Generamos el contenido del buffer // ... InetAddress direcc = InetAddress.getByName(maquina); DatagramSocket ds = new DatagramSocket(); DatagramPacket dp = new DatagramPacket(buffer, tam, direcc, puerto); ds.send(dp); /* Aquí enviamos el paquete */
Si queremos enviar paquetes a nuestra propia máquina hay que usar como nombre de host la dirección "localhost" o "127.0.0.1". También podemos usar el método InetAddress.getLocalHost(), que devuelve un objeto InetAddress que "apunta" a la máquina local.
Un ejemplo cliente/servidor completo usando UDP
A continuación se incluye una aplicación completa, formada por un cliente y un servidor que se comunican mediante UDP. El cliente envía números enteros (32 bits) al servidor y este se los devuelve después de procesarlos (simplemente los eleva al cuadrado), en forma de enteros largos (64 bits).
En ambos programas (cliente y servidor) se utilizan varias clases explicadas con anterioridad (es fundamental haber leído previamente el capítulo de Entrada/Salida) y se realiza un extensivo control de excepciones.
Los datos que el cliente envía al servidor los obtiene de la línea de comandos.
Veamos un ejemplo en el que el cliente manda los números 43, 56 y 2 al servidor. Si ejecutamos el servidor en una máquina con dirección IP 194.140.47.1 y ejecutamos el cliente en otra máquina con dirección IP distinta, tendríamos que teclear la siguiente en la línea de comandos:
Servidor:
java servidor
Cliente:
java cliente 194.140.47.1 43 56 2
Como cada paquete enviado es independiente de los demás, podríamos tener varios clientes comunicándose con el servidor al mismo tiempo, con lo que los distintos paquetes se intercalarían unos con otros.
Si ejecutamos ambos programas en la misma máquina tendríamos que abrir dos sesiones distintas y teclear las instrucciones anteriores en cada sesión. En el caso del cliente tendríamos que poner:
java cliente localhost 43 56 2
Veamos el código del programa:
import java.net.*;
import java.io.*;
class servidor{
public static void main(String args[]){
// Primero indicamos la dirección IP local
try{
System.out.println("LocalHost = " + InetAddress.getLocalHost().toString());
} catch (UnknownHostException uhe){
System.err.println("No puedo saber la dirección IP local : " + uhe);
}
// Abrimos un Socket UDP en el puerto 1234.
// A través de este Socket enviaremos datagramas del tipo DatagramPacket
DatagramSocket ds = null;
try{
ds = new DatagramSocket(1234);
} catch(SocketException se){
System.err.println("Se ha producido un error al abrir el socket : " + se);
System.exit(-1);
}
// Bucle infinito
while(true){
try{
// Nos preparamos a recibir un número entero (32 bits = 4 bytes)
byte bufferEntrada[] = new byte[4];
// Creamos un "contenedor" de datagrama, cuyo buffer
// será el array creado antes
DatagramPacket dp = new DatagramPacket(bufferEntrada,4);
// Esperamos a recibir un paquete
ds.receive(dp);
// Podemos extraer información del paquete
// Nº de puerto desde donde se envió
int puerto = dp.getPort();
// Dirección de Internet desde donde se envió
InetAddress direcc = dp.getAddress();
// "Envolvemos" el buffer con un ByteArrayInputStream...
ByteArrayInputStream bais = new ByteArrayInputStream(bufferEntrada);
// ... que volvemos a "envolver" con un DataInputStream
DataInputStream dis = new DataInputStream(bais);
// Y leemos un número entero a partir del array de bytes
int entrada = dis.readInt();
long salida = (long)entrada*(long)entrada;
// Creamos un ByteArrayOutputStream sobre el que podamos escribir
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// Lo envolvemos con un DataOutputStream
DataOutputStream dos = new DataOutputStream(baos);
// Escribimos el resultado, que debe ocupar 8 bytes
dos.writeLong(salida);
// Cerramos el buffer de escritura
dos.close();
// Generamos el paquete de vuelta, usando los datos
// del remitente del paquete original
dp = new DatagramPacket(baos.toByteArray(),8,direcc,puerto);
// Enviamos
ds.send(dp);
// Registramos en salida estandard
System.out.println( "Cliente = " + direcc + ":" + puerto +
"\tEntrada = " + entrada +
"\tSalida = " + salida );
} catch(Exception e){
System.err.println("Se ha producido el error " + e);
}
}
}
}
class cliente {
public static void main(String args[]){
// Leemos el primer parámetro, donde debe ir la dirección
// IP del servidor
InetAddress direcc = null;
try{
direcc = InetAddress.getByName(args[0]);
} catch(UnknownHostException uhe){
System.err.println("Host no encontrado : " + uhe);
System.exit(-1);
}
// Puerto que hemos usado para el servidor
int puerto = 1234;
// Creamos el Socket
DatagramSocket ds = null;
try{
ds = new DatagramSocket();
} catch(SocketException se){
System.err.println("Error al abrir el socket : " + se);
System.exit(-1);
}
// Para cada uno de los argumentos...
for (int n=1;n<args.length;n++){
try{
// Creamos un buffer para escribir
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
// Convertimos el texto en número
int numero = Integer.parseInt(args[n]);
// Lo escribimos
dos.writeInt(numero);
// y cerramos el buffer
dos.close();
// Creamos paquete
DatagramPacket dp = new DatagramPacket(baos.toByteArray(),4,direcc,puerto);
// y lo mandamos
ds.send(dp);
// Preparamos buffer para recibir número de 8 bytes
byte bufferEntrada[] = new byte[8];
// Creamos el contenedor del paquete
dp = new DatagramPacket(bufferEntrada,8);
// y lo recibimos
ds.receive(dp);
// Creamos un stream de lectura a partir del buffer
ByteArrayInputStream bais = new ByteArrayInputStream(bufferEntrada);
DataInputStream dis = new DataInputStream(bais);
// Leemos el resultado final
long resultado = dis.readLong();
// Indicamos en pantalla
System.out.println( "Solicitud = " + numero +
"\tResultado = " +resultado );
} catch (Exception e){
System.err.println("Se ha producido un error : " + e);
}
}
}
}
En el código anterior se usan métodos no vistos hasta ahora, como DatagramPacket.getAddress(), que devuelve la dirección IP del remitente del mensaje, y DatagramPacket.getPort(), que devuelve el puerto. Existen otros métodos importantes, como DatagramPacket.getLength(), que indican la longitud de datos recibidos.
Consideraciones sobre UDP
UDP es un protocolo no orientado a la conexión. Esto significa que la comunicación es más rápida (porque no hay que establecer conexiones ni circuitos virtuales entre máquinas) pero también menos segura.
Nadie nos garantiza que un paquete vaya a llegar a su destino. Tampoco está garantizado que dos paquetes lleguen en el mismo orden en que se enviaron. Si nuestros programas usan una red local privada para comunicarse, es muy improbable que estos problemas aparezcan. Pero si deben usar una red pública como Internet, con múltiples pasarelas y enlaces distintos entre los punto de origen y destino, cualquiera de estos errores puede ocurrir (y ocurrirá, seguro).
En estos casos es responsabilidad del programador el comprobar que los paquetes llegan a sus destino y que la información se transmite en orden. Existen multitud de mecanismos para conseguir esto, pero la complejidad de su implementación hace más recomendable la utilización de un protocolo orientado a la conexión como TCP.
















































