7. PROGRAMACION EN RED CON JAVA :

La programación en red siempre ha sido dificultosa , el programador debía de conocer la mayoría de los detalles de la red, incluyendo el hardware utilizado, los distintos niveles en que se divide la capa de red, las librerias necesarias para programar en cada capa, etc...

Pero , la idea simplemente consiste en obtener información desde otra maquina , aportada por otra aplicación software. Por lo tanto , de cierto modo se puede reducir al mero hecho de leer y escribir archivos , con ciertas salvedades.

El sistema de Entrada/Salida de Unix sigue el paradigma que normalmente se designa como Abrir-Leer-Escribir-Cerrar. Antes de que un proceso de usuario pueda realizar operaciones de entrada/salida, debe hacer una llamada a Abrir (open) para indicar, y obtener los permisos del fichero o dispositivo que se desea utilizar.

Una vez que el fichero o dispositivo se encuentra abierto, el proceso de usuario realiza una o varias llamadas a Leer (read) y Escribir (write), para la lectura y escritura de los datos.

El proceso de lectura toma los datos desde el objeto y los transfiere al proceso de usuario, mientras que el de escritura los transfiere desde el proceso de usuario al objeto. Una vez concluido el intercambio de información, el proceso de usuario llamará a Cerrar (close) para informar al sistema operativo que ha finalizado la utilización del fichero o dispositivo.

En Unix, un proceso tiene un conjunto de descriptores de entrada/salida desde donde leer y por donde escribir. Estos descriptores pueden estar referidos a ficheros, dispositivos, o canales de comunicaciones sockets.

El ciclo de vida de un descriptor, aplicado a un canal de comunicación (socket), está determinado por tres fases :

 

- Creación, apertura del socket

- Lectura y Escritura, recepción y envío de datos por el socket

- Destrucción, cierre del socket

 

La interface IPC en Unix-BSD está implementada sobre los protocolos de red TCP y UDP. Los destinatarios de los mensajes se especifican como direcciones de socket; cada dirección de socket es un identificador de comunicación que consiste en una dirección Internet y un número de puerto.

Las operaciones IPC se basan en pares de sockets. Se intercambian información transmitiendo datos a través de mensajes que circulan entre un socket en un proceso y otro socket en otro proceso. Cuando los mensajes son enviados, se encolan en el socket hasta que el protocolo de red los haya transmitido. Cuando llegan, los mensajes son encolados en el socket de recepción hasta que el proceso que tiene que recibirlos haga las llamadas necesarias para recoger esos datos.

 

 

El lenguaje Java fue desarrollado por la empresa Sun MicroSystems hacia el año 1990, mediante la creación de un grupo de trabajo en cuya cabeza estaba James Gosling. Este grupo de trabajo fue ideado para desarrollar un sistema de control de electrodomésticos y de PDAs o asistentes personales (pequeños ordenadores) y que además tuviese la posibilidad de interconexión a redes de ordenadores. Todo ello implicaba la creación de un hardware polivalente, un sistema operativo eficiente (SunOS) y un lenguaje de desarrollo (Oak). El proyecto concluyó dos años más tarde con un completo fracaso que condujo a la disolución del grupo.

 

Pero el desarrollo del proyecto relativo al lenguaje oak siguió adelante gracias entre otras cosas a la distribución libre del lenguaje por Internet mediante la incipiente por aquellos años World Wide Web. De esta forma el lenguaje alcanzó cierto auge y un gran número de programadores se encargaron de su depuración así como de perfilar la forma y usos del mismo.

 

El nombre de Java , surgió durante una de las sesiones de brainstorming que se celebraban por el equipo de desarrollo del lenguaje. Hubo que cambiar el nombre debido a que ya existía otro lenguaje con el nombre de oak.

Sun MicroSystems lanzó las primeras versiones de Java a principios de 1995 , y se han ido sucediendo las nuevas versiones durante estos últimos años , fomentando su uso y extendiendo las especificaciones y su funcionalidad.

 

Una de las características más importantes de Java es su capacidad y a la vez facilidad para realizar aplicaciones que funcionen en red. La mayoría de los detalles de implementación a bajo nivel están ocultos y son tratados de forma transparente por la J.V.M (Maquina Virtual de Java). Los programas son independientes de la arquitectura y se ejecutan indistintamente en una gran variedad de equipos con diferentes tipos de microprocesadores y sistemas operativos.

  

 

7.1 CLASES PARA LAS COMUNICACIONES DE RED EN JAVA : java.net

En las aplicaciones en red, como ya hemos visto siempre tenemos un cliente y un servidor. El servidor es el que espera las conexiones del cliente en un lugar claramente definido y el cliente es el que lanza las peticiones a la maquina donde se está ejecutando el servidor, y al lugar donde está esperando el servidor , los puertos. Una vez establecida la conexión , esta es tratada como un stream (flujo) típico de entrada/salida.

Cuando se escriben programas Java que se comunican a través de la red, se está programando en la capa de aplicación. Típicamente, no se necesita trabajar con las capas TCP y UDP , en su lugar se puede utilizar las clases del paquete java.net. Estas clases porporcionan comunicación de red independiente del sistema.

 

A través de las clases del paquete java.net, los programas Java pueden utilizar TCP o UDP para comunicarse a través de Internet. Las clases URL, URLConnection, Socket, y SocketServer utilizan TCP para comunicarse a través de la Red. Las clases DatagramPacket y DatagramServer utilizan UDP.

 

TCP proporciona un canal de comunicación fiable punto a punto, lo que utilizan para comunicarse las aplicaciones cliente-servidor en Internet. Las clases Socket y ServerSocket del paquete java.net proporcionan un canal de comunicación independiente del sistema utilizando TCP, cada una de las cuales implementa el lado del cliente y el servidor respectivamente.

 

Así el paquete java.net proporciona, entre otras, las siguientes clases , que son las que veremos con detalle:

 

- Socket : Implementa un extremo de la conexión TCP.

 

- ServerSocket : Se encarga de implementar el extremo Servidor de la conexión en la que se esperarán las conexiones de los clientes.

 

- DatagramSocket : Implementa tanto el servidor como el cliente cuando se utiliza UDP.

 

- DatagramPacket : Implementa un datagram packet, que se utiliza para la creación de servicios de reparto de paquetes sin conexión.

 

- InetAddress : Se encarga de implementar la dirección IP.

 

 

La clase Socket del paquete java.net es una implementación independiente de la plataforma de un cliente para un enlace de comunicación de dos vías entre un cliente y un servidor. Utilizando la clase java.net.Socket en lugar de tratar con código nativo, los programas Java pueden comunicarse a través de la red de una forma independiente de la plataforma.

El entorno de desarrollo de Java incluye un paquete, java.io, que contiene un juego de canales de entrada y salida que los programas pueden utilizar para leer y escribir datos. Las clases InputStream y OutputStream del paquete java.io son superclases abstractas que definen el comportamiento de los canales de I/O secuenciales de Java. java.io también incluye muchas subclases de InputStream y OtputStream que implementan tipos especificos de canales de I/O.

 

 

7.1.1 DATAGRAM SOCKET (Servicio sin Conexión) :

Es el más simple , lo único que hacemos es enviar los datos , mediante la creación de un socket y utilizando los métodos de envio y recepción apropiados.

Se trata de un servicio de transporte sin conexión. Son más eficientes que TCP, pero no está garantizada la fiabilidad. Los datos se envían y reciben en paquetes, cuya entrega no está garantizada. Los paquetes pueden ser duplicados, perdidos o llegar en un orden diferente al que se envió.

El protocolo de comunicaciones con datagramas UDP, es un protocolo sin conexión, es decir, cada vez que se envíen datagramas es necesario enviar el descriptor del socket local y la dirección del socket que debe recibir el datagrama. Como se puede ver, hay que enviar datos adicionales cada vez que se realice una comunicación.

 

public class java.net.DatagramSocket extends java.lang.Object

 

 

 

A) Constructores :

- public DatagramSocket ( ) throws SocketException : Se encarga de construir un Socket para datagramas y de conectarlo al primer puerto disponible.

- public DatagramSocket (int port) throws SocketException : Idem , pero con la salvedad de que nos permite especificar el número de puerto asociado.

- public DatagramSocket (int, InetAddress) throws SocketException : Nos permite especificar , además del puerto , la dirección a la que se va a conectar.

 

 

 

B) Métodos :

 

 

7.1.2 DATAGRAM PACKET :

Un DatagramSocket envía y recibe los paquetes y un DatagramPacket contiene la información relevante. Cuando quisieramos recibir un datagrama, tendremos que situarlo en algún sitio, que suele ser un bufer o array de bytes. Y cuando preparamos un datagrama para ser enviado, el DatagramPacket no sólo debe tener la información, sino que además debe tener la dirección IP y el puerto de destino, que puede coincidir con un puerto TCP.

 

public final class java.net.DatagramPacket extends java.lang.Object

 

  1. Constructores :

 

B) Métodos :

 

 

7.1.3 STREAM SOCKET (Servicio Orientado a Conexión) :

Es un servicio orientado a conexión donde los datos se transfieren sin encuadrarlos en registros o bloques. Si se rompe la conexión entre los procesos, éstos serán informados. El protocolo de comunicaciones con streams es un protocolo orientado a conexión, ya que para establecer una comunicación utilizando el protocolo TCP, hay que establecer en primer lugar una conexión entre un par de sockets. Mientras uno de los sockets atiende peticiones de conexión (servidor), el otro solicita una conexión (cliente). Una vez que los dos sockets estén conectados, se pueden utilizar para transmitir datos en ambas direcciones.

Permite a las aplicaciones Cliente y Servidor , disponer de un stream que facilita la comunicación entre ambos , obteniendose una mayor fiabilidad.

El funcionamiento es diferente al anterior ya que cada extremo se comportará de forma diferente , el servidor adopta un papel pasivo y espera conexiones de los clientes. Mientras que el cliente adoptará un papel activo , solicitando conexiones al servidor.

 

 

En la parte del servidor tendremos :

 

 

public final class java.net.ServerSocket extends java.lang.Object

 

 

  1. Constructores :

 

 

 

Hay que recordar , que es fundamental que el puerto escogido sea conocido por el cliente, en caso contrario , no se podría establecer la conexión.

 

 

 

 

  1. MÉTODOS :

 

 

 

 

 

 

 

En la parte del cliente :

 

public final class java.net.Socket extends java.lang.Object

 

  1. Constructores :

 

 

 

 

  1. MÉTODOS :

 

 

 

 

 

 

 

 

 

 

 

 

7.1.4 La Clase InetAddress : Esta clase implementa la dirección IP.

 

public final class java.net.InetAddress extends java.lang.Object

 

  1. Constructores :
  2. Para crear una nueva instancia de esta clase se debe de llamar a los métodos getLocalHost( ) , getByname ( ) o getAllByName ( ) .

     

  3. Métodos :

 

Hay que tener en cuenta que el byte de mayor orden de la dirección estará en getAddress ( ) [0] .

 

throws UnknownHostException : Retorna un vector con todas las direcciones IP del host especificado en el parámetro.

 

throws UnknownHostException : Retorna la dirección IP del nombre del host que se le pasa como parámetro, aunque también se le puede pasar un string representando su dirección IP.

 

 

 

 

Un ejemplo de utilización muy sencillo de esta clase es el siguiente , que se encarga de devolvernos la direccion IP de la máquina.

 

Public class QuienSoy {

 

Public static void main (String[] args) throws Exception {

If (args.length !=1) {

System.err.println(" Uso : QuienSoy NombreMaquina ");

System.exit(1);

}

InetAddress direccion = InetAddress.getByName(args[0]);

System.out.println(direccion);

}

}

 

7.2 ENVIO Y RECEPCIÓN A TRAVÉS DE SOCKETS :

El servidor creará un socket , utilizando ServerSocket, le asignará un puerto y una dirección, una vez haga el accept para esperar llamadas ,se quedará bloqueado a la espera de las mismas. Una vez llegue una llamada el accept creará un Socket para procesarla.

A su vez , cuando un cliente desee establecer una conexión , creará un socket y establecerá una conexión al puerto establecido. Es y sólo en este momento cuando se da una conexión real y se mantendrá hasta su liberación mediante close( ).

 

Para poder leer y escribir datos , los sockets disponen de unos stream asociados , uno de entrada (InputStream) y otro de salida (OutputStream) respectivamente.

 

Para obtener estos streams a partir del socket utilizaremos :

 

ObjetoDeTipoSocket.getInputStream ( ) : Nos devolverá un objeto de tipo InputStream.

 

ObjetoDeTipoSocket.getOutputStream ( ) : Nos devolverá un objeto de tipo OutputStream.

Para el envío de datos , podemos utilizar OutputStream directamente en el caso de que queramos enviar un flujo de bytes sin buffer o también podemos crear un objeto de tipo stream basado en el OutputStream que proporciona el socket.

 

 

En Java, crear una conexión socket TCP/IP se realiza directamente con el paquete java.net. A continuación mostramos un diagrama de lo que ocurre en el lado del cliente y del servidor:

 

El servidor establece un puerto y espera durante un cierto tiempo (timeout segundos), a que el cliente establezca la conexión. Cuando el cliente solicite una conexión, el servidor abrirá la conexión socket con el método accept().

El cliente establece una conexión con la máquina host a través del puerto que se designe en port#. El cliente y el servidor se comunican con manejadores InputStream y OutputStream .

 

Si estamos programando un cliente, el socket se abre de la forma:

Socket miSocket;

miSocket = new Socket( host , Puerto );

Donde host es el nombre de la máquina en donde estamos intentando abrir la conexión y Puerto es el puerto (un número) del servidor que está corriendo sobre el cual nos queremos conectar.

Cuando se selecciona un número de puerto, se debe tener en cuenta que los puertos en el rango 0-1023 están reservados. Estos puertos son los que utilizan los servicios estándar del sistema como email, ftp, http, etc.... Por lo que para nuestras aplicaciones debemos asegurarnos de seleccionar un puerto por encima del 1023.

Hasta ahora no hemos utilizado excepciones; pero debemos tener en cuenta la captura de excepciones cuando se está trabajando con sockets. Así :

Socket miSocket;

try {

miSocket = new Socket( host , Puerto );

} catch( IOException e ) {

System.out.println( e );

}

} catch (UnknownHostException uhe) {

System.out.println(uhe);

}

En el caso de que estemos implementando un servidor, la forma de apertura del socket sería como sigue :

Socket SocketServidor;

try {

SocketServidor = new ServerSocket( Puerto );

} catch( IOException e ) {

System.out.println( e );

}

Cuando implementamos un servidor necesitamos crear un objeto socket desde el ServerSocket para que esté atento a las conexiones que le puedan realizar clientes potenciales y poder aceptar esas conexiones:

Socket SocketServicio = null;

try {

SocketServicio = SocketServidor.accept();

} catch( IOException e ) {

System.out.println( e );

}

7.3 CREACIÓN DE STREAMS :

 

7.3.1 Creación de Streams de Entrada :

En la parte cliente de la aplicación, se puede utilizar la clase DataInputStream para crear un stream de entrada que esté listo a recibir todas las respuestas que el servidor le envíe.

 

DataInputStream teclado;

try {

teclado = new DataInputStream( miSocket.getInputStream() );

} catch( IOException e ) {

System.out.println( e );

}

 

La clase DataInputStream permite la lectura de líneas de texto y tipos de datos primitivos de Java de un modo altamente portable; dispone de métodos para leer todos esos tipos como: read(), readChar(), readInt(), readDouble() y readLine().

Deberemos utilizar la función que creamos necesaria dependiendo del tipo de dato que esperemos recibir del servidor. En el lado del servidor, también usaremos DataInputStream, pero en este caso para recibir las entradas que se produzcan de los clientes que se hayan conectado:

 

DataInputStream entrada;

try {

entrada = new DataInputStream( SocketServicio.getInputStream() );

} catch( IOException e ) {

System.out.println( e );

}

 

7.3.2 Creación de Streams de Salida :

En el lado del cliente, podemos crear un stream de salida para enviar información al socket del servidor utilizando las clases PrintStream o DataOutputStream:

 

PrintStream salida;

try {

salida = new PrintStream( miSocket.getOutputStream() );

} catch( IOException e ) {

System.out.println( e );

}

 

La clase PrintStream tiene métodos para la representación textual de todos los datos primitivos de Java. Sus métodos write y println() tienen una especial importancia en este aspecto. No obstante, para el envío de información al servidor también podemos utilizar DataOutputStream:

 

DataOutputStream salida;

try {

salida = new DataOutputStream( miSocket.getOutputStream() );

} catch( IOException e ) {

System.out.println( e );

}

 

La clase DataOutputStream permite escribir cualquiera de los tipos primitivos de Java, muchos de sus métodos escriben un tipo de dato primitivo en el stream de salida. De todos esos métodos, el más útil quizás sea writeBytes(). En el lado del servidor, podemos utilizar la clase PrintStream para enviar información al cliente:

 

 

PrintStream salida;

try {

salida = new PrintStream( SocketServicio.getOutputStream() );

} catch( IOException e ) {

System.out.println( e );

}

Pero también podemos utilizar la clase DataOutputStream como en el caso de envío de información desde el cliente.

 

7.4 CIERRE DE SOCKETS :

Siempre debemos cerrar los canales de entrada y salida que se hayan abierto durante la ejecución de la aplicación.

 

En la parte del cliente:

try {

salida.close(); teclado.close(); miSocket.close();

} catch( IOException e ) {

System.out.println( e );

}

 

Y en la parte del servidor:

try {

salida.close(); entrada.close(); SocketServicio.close(); SocketServidor.close();

} catch( IOException e ) {

System.out.println( e );

}

 

7.5 DIFERENCIAS ENTRE SOCKETS STREAM Y DATAGRAMA :

Ahora se nos presenta un problema, ¿qué protocolo, o tipo de sockets, debemos usar - UDP o TCP? La decisión depende de la aplicación cliente/servidor que estemos escribiendo. Vamos a ver algunas diferencias entre los protocolos para ayudar en la decisión.

 

En UDP, cada vez que se envía un datagrama, hay que enviar también el descriptor del socket local y la dirección del socket que va a recibir el datagrama, luego éstos son más grandes que los TCP. Como el protocolo TCP está orientado a conexión, tenemos que establecer esta conexión entre los dos sockets antes de nada, lo que implica un cierto tiempo empleado en el establecimiento de la conexión, que no existe en UDP.

 

En UDP hay un límite de tamaño de los datagramas, establecido en 64 kilobytes, que se pueden enviar a una localización determinada, mientras que TCP no tiene límite; una vez que se ha establecido la conexión, el par de sockets funciona como los streams: todos los datos se leen inmediatamente, en el mismo orden en que se van recibiendo.

 

UDP es un protocolo desordenado, no garantiza que los datagramas que se hayan enviado sean recibidos en el mismo orden por el socket de recepción. Al contrario, TCP es un protocolo ordenado, garantiza que todos los paquetes que se envíen serán recibidos en el socket destino en el mismo orden en que se han enviado.

 

Los datagramas son bloques de información del tipo lanzar y olvidar. Para la mayoría de los programas que utilicen la red, el usar un flujo TCP en vez de un datagrama UDP es más sencillo y hay menos posibilidades de tener problemas. Sin embargo, cuando se requiere un rendimiento óptimo, y está justificado el tiempo adicional que supone realizar la verificación de los datos, los datagramas son un mecanismo realmente útil.

 

En resumen, TCP parece más indicado para la implementación de servicios de red como un control remoto (rlogin, telnet) y transmisión de ficheros (ftp), que necesitan transmitir datos de longitud indefinida. UDP es menos complejo y tiene una menor sobrecarga sobre la conexión, esto hace que sea el indicado en la implementación de aplicaciones cliente/servidor en sistemas distribuidos montados sobre redes de área local.

 

 

7.6 CLASES UTILES EN COMUNICACIONES :

Vamos a exponer en resumen otras clases que resultan útiles cuando estamos desarrollando programas de comunicaciones, aparte de las que ya se han visto. El problema es que la mayoría de estas clases se prestan a discusión, porque se encuentran bajo el directorio sun. Esto quiere decir que son implementaciones Solaris y, por tanto, específicas del Unix Solaris. Además su API no está garantizada, pudiendo cambiar. Pero, a pesar de todo, resultan muy interesantes y vamos a comentar un grupo de ellas solamente que se encuentran en el paquete sun.net

 

 

 

 

 

 

 

7.7 EJEMPLO CLIENTE DE ECO :

Sea el programa cliente, PruebaEco, conecta con el Echo del servidor (en el puerto 7) mediante un socket. El cliente lee y escribe a través del socket. PruebaEco envía todo el texto tecleado en su entrada estandar al Echo del servidor, escribiéndole el texto al socket. El servidor repite todos los caracteres recibidos en su entrada desde el cliente de vuelta a través del socket al cliente. El programa cliente lee y muestra los datos pasados de vuelta desde el servidor.

 

import java.io.*;

import java.net.*;

 

public class PruebaEco {

 

public static void main(String[] args) {

Socket ecoSocket = null;

DataOutputStream salida = null;

DataInputStream entrada = null;

DataInputStream stdIn = new DataInputStream(System.in);

 

/* Establecimiento de la conexión del Socket entre el cliente y el servidor y

apertura del canal E/S sobre el socket : */

 

try {

ecoSocket = new Socket("bacterio", 7);

salida = new DataOutputStream(ecoSocket.getOutputStream());

entrada = new DataInputStream(ecoSocket.getInputStream());

} catch (UnknownHostException e) {

System.err.println("No conozco al host : bacterio");

} catch (IOException e) {

System.err.println("Error de E/S para la conexión con: bacterio");

}

 

 

 

 

/* Ahora lee desde el stream de entranda estandar una línea cada vez. El programa escribe inmediatamente la entrada seguida por un carácter de nueva línea en el stream de salida conectado al socket. */

 

if (ecoSocket != null && salida != null && entrada != null) {

try {

String userInput;

while ((userInput = stdIn.readLine()) != null) {

salida.writeBytes(userInput);

salida.writeByte('\n');

System.out.println("eco: " + entrada.readLine());

}

 

/* Cuando el usuario teclea un carácter de fin de entrada, el bucle while termina.

Cierre de los streams de entrada y salida conectados al socket, y cierre de la

conexión del socket con el servidor. */

 

salida.close();

entrada.close();

ecoSocket.close();

} catch (IOException e) {

System.err.println("E/S fallo en la conexión a : bacterio");

}

}

}

}