Home UC3M
Home IT

Trabajando con sockets TCP

 OBJETIVOS

Las clases URL y URLConnector proporcionan mecanismos a muy alto nivel de acceder a recursos de Internet, basicamente creabamos aplicaciones clientes que se conectaban a un servidor Web que nos proporcionaba el recurso requerido.
Ahora vamos a crear nuestros propios servicios y clientes que puedan acceder a esos servicios, es decir, vamos a desarrollar aplicaciones cliente-servidor, para ello necesitamos mecanismos a más bajo nivel de comunicaciones en red.
La comunicación tanto a través de TCP como de UDP utiliza la abstracción de sockets para comunicar procesos entre sí. Esta comunicación consiste en la transmisión de un mensaje entre el socket de un proceso (cliente) y el socket de otro proceso (servidor).
En esta primera práctica veremos como da soporte Java a Socket TCP.

OBLIGATORIO ENTREGAR EL EJERCICIO 3

 ENUNCIADO


En este práctica veremos las clases de Java que nos permite trabajan de forma sencilla con sockets TCP. Antes, vamos a ver una facilidad de Java para manejar direcciones de Internet.

Clase InetAddress

La clase java.net.InetAddress nos permite representar en Java direcciones IP. Con ella nos podemos referir a los computadores por sus nombres de host en el DNS (Domain Name Server), y es el propio Java quien realiza la consulta correspondiente para conocer la dirección IP.
Típicamente para crear instancias de InetAddress, se invoca el método estático getByName(String) pasándole el nombre DNS del host como parámetro. Este objeto representará la dirección de Internet de ese host, y se podrá utilizar para construir sockets. El siguiente código muestra cómo se puede emplear (código fuente: DireccionIP.java):

import java.net.*;

public class DireccionIP {
  public static void main(String[] args) throws Exception {
    if(args.length != 1) {
      System.err.println(
        "Usage: DireccionIP NombreHost");
      System.exit(1);
    }
    InetAddress a = InetAddress.getByName(args[0]);
    System.out.println(a);
  }
} 



Este programa al pasarle un nombre de un host, nos devuelve la dirección IP que tiene asignada, asi:
  java DireccionIP lm000.it.uc3m.es

Obtenemos el siguiente resultado:
  lm000.it.uc3m.es/163.117.139.214

NOTA:Aunque en el ejemplo, no se han tratado excepciones, este método puede devolver una excepción del tipo UnknownHostException (host desconocido). Modificad el programa para capturar esta excepción.

Comunicación cliente/servidor con Socket TCP

El interfaz Java que da soporte a sockets TCP está constituida por las clases ServerSocket y Socket.
  1. ServerSocket: es utilizada por un servidor para crear un socket en el puerto en el que escucha las peticiones de conexión de los clientes. Su método accept toma una petición de conexión de la cola, o si la cola está vacía, se bloquea hasta que llega una petición. El resultado de ejecutar accept es una instancia de Socket, a través del cual el servidor tiene acceso a los datos enviados por el cliente.
  2. Socket: es utilizada tanto por el cliente como por el servidor. El cliente crea un socket especificando el nombre DNS del host y el puerto del servidor, así se crea el socket local y además se conecta con el servicio.
    Esta clase proporciona los métodos getInputStream y getOutputStream para acceder a los dos streams asociados a un socket (recordemos que son bidireccionales), y devuelve tipos de datos InputStream y OutputStream, respectivamente, a partir de los cuales podemos construir BufferedReader y PrintWriter, respectivamente, para poder procesar los datos de forma más sencilla.

Si nos centramos en la parte de comunicaciones, la forma general de implementar un cliente será:
  1. Crear un objeto de la clase Socket, indicando host y puerto donde corre el servicio.
  2. Obtener las referencias al stream de entrada y al de salida al socket.
  3. Leer desde y escribir en el stream de acuerdo al protocolo del servicio. Para ello emplear alguna de las facilidades del paquete java.io.
  4. Cerrar los streams.
  5. Cerrar el socket.
La forma de implementar un servidor será:
  1. Crear un objeto de la clase ServerSocket para escuchar peticiones en el puerto asignado al servicio.
  2. Esperar solicitudes de clientes
  3. Cuando se produce una solicitud:
    • Aceptar la conexión obteniendo un objeto de la clase Socket
    • Obtener las referencias al stream de entrada y al de salida al socket anterior.
    • Leer datos del socket, procesarlos y enviar respuestas al cliente, escribiendo en el stream del socket.Para ello emplear alguna de las facilidades del paquete java.io.
  4. Cerrar los streams.
  5. Cerrar los sockets.


Vamos a ver todo esto con un sencillo ejemplo: una aplicación cliente/servidor de eco, es decir, el servidor repite lo mismo que le envía el cliente, hasta que el cliente quiere finalizar el servicio, para lo cual envía la palabra "Adios" al servidor.
  • Código del Cliente


  • Analizad el siguiente código del cliente EcoCliente.java:

    import java.net.*;
    import java.io.*;
    
    public class EcoCliente {
      public static void main(String[] args)  throws IOException {
        Socket socketCliente = null;
        BufferedReader entrada = null;
        PrintWriter salida = null;
    
        // Creamos un socket en el lado cliente, enlazado con un
        // servidor que está en la misma máquina que el cliente
        // y que escucha en el puerto 4444
        try {
          socketCliente = new Socket("localhost", 4444);
          // Obtenemos el canal de entrada
          entrada = new BufferedReader(new InputStreamReader(socketCliente.getInputStream()));
          // Obtenemos el canal de salida
          salida = new PrintWriter(new BufferedWriter(new 
    	  OutputStreamWriter(socketCliente.getOutputStream())),true);
        } catch (IOException e) {
    	System.err.println("No puede establer canales de E/S para la conexión");
            System.exit(-1);
        }
        BufferedReader stdIn =
    	new BufferedReader(new InputStreamReader(System.in));
    
        String linea;
    
        // El programa cliente no analiza los mensajes enviados por el
        // usario, simplemente los reenvía al servidor hasta que este
        // se despide con "Adios"
        try {
          while (true) {
            // Leo la entrada del usuario
            linea = stdIn.readLine();
            // La envia al servidor
            salida.println(linea);
            // Envía a la salida estándar la respuesta del servidor
            linea = entrada.readLine();
    	System.out.println("Respuesta servidor: " + linea);
            // Si es "Adios" es que finaliza la comunicación
    	if (linea.equals("Adios")) break;
          }
        } catch (IOException e) {
    	System.out.println("IOException: " + e.getMessage());
        }
     
        // Libera recursos
        salida.close();
        entrada.close();
        stdIn.close();
        socketCliente.close();
      }
    }
    


  • Código del Servidor


  • Analizad el siguiente código del servidor EcoServidor.java:

    import java.io.*;
    import java.net.*;
    
    public class EcoServidor {  
      public static final int PORT = 4444;
      public static void main(String[] args) throws IOException {
        // Establece el puerto en el que escucha peticiones
        ServerSocket socketServidor = null;
        try {
          socketServidor = new ServerSocket(PORT);
        } catch (IOException e) {
          System.out.println("No puede escuchar en el puerto: " + PORT);
          System.exit(-1);
        }
    
        Socket socketCliente = null;
        BufferedReader entrada = null;
        PrintWriter salida = null;
    
        System.out.println("Escuchando: " + socketServidor);
        try {
          // Se bloquea hasta que recibe alguna petición de un cliente
          // abriendo un socket para el cliente
          socketCliente = socketServidor.accept();
          System.out.println("Connexión acceptada: "+ socketCliente);
          // Establece canal de entrada
          entrada = new BufferedReader(new InputStreamReader(socketCliente.getInputStream()));
          // Establece canal de salida
          salida = new PrintWriter(new BufferedWriter(new 
    	  OutputStreamWriter(socketCliente.getOutputStream())),true);
          
          // Hace eco de lo que le proporciona el cliente, hasta que recibe "Adios"
          while (true) {  
            String str = entrada.readLine();
    	System.out.println("Cliente: " + str);
    	salida.println(str);
    	if (str.equals("Adios")) break;
          }
    
        } catch (IOException e) {
          System.out.println("IOException: " + e.getMessage());
        }  
        salida.close();
        entrada.close();
        socketCliente.close();
        socketServidor.close();
      }
    }
    

Para probar como funciona este sencillo programa, debereis arrancar primero el servidor, que se quedará a la escucha de peticiones, y despues el cliente. Si lo haceis en orden inverso, al arrancar el cliente se producirá una IOException.

CONSEJO: Probad que ocurre cuando se producen errores: se muere el programa cliente, se muere el programa servidor...y modificad los programas si lo considerais oportuno.

Ahora tendreis que hacer algunas modificaciones en los programas anteriores:
  • Modificación 1:Modificad el código considerando que el servidor se encuentra en un máquina remota, cuyo nombre se pasa como parámetro al programa cliente
  • Modificación 2:El servidor anterior es poco eficiente, porque sólo atiende a un cliente simultaneamente, ¿cómo mejoraríais su eficiencia?. Modificad el código del servidor y verificad su funcionamiento.

 MÁS EJERCICIOS

NOTA: Considerad el caso más sencillo: los ficheros que se transmiten son de texto. Para indicar la finalización de una transferencia utilizad "\0". Para transmitir un error del servidor al cliente, utilizad un código que no se correspondan a un caracter alfanumérico, por ejemplo, ESC(0x1B).
  • Ejercicio 1:Cread un servidor que abra un fichero solicitado por un cliente y se lo envíe a través de la red. El servidor aceptará el siguiente formato de peticiones:


    1. Para proporcionar un fichero al cliente:
    2.   get "nombre_completo_fichero"
      
    3. Para dar por finalizado el servicio:
    4.   bye
      


  • Ejercicio 2:Modificad el programa anterior para aumentar el servicio proporcionado por nuestro servidor, permitiendo modificar el contenido de un fichero remoto, para ello se deberá seguir el siguiente protocolo:

    1. Bloquear en el servidor el fichero que va a modificar el cliente, comando "lock".
    2.   lock "nombre_fichero"
      
    3. Descargar el fichero al sistema de ficheros del cliente, comando "get" ( como el comando anterior especifica el nombre de fichero, en este comando no se incluye el nombre).
    4. Una vez modificado el fichero en el sistema local, enviar el fichero modificado al servidor, comando "put". Se guardará con el mismo nombre que tenía.
    5. Desbloquear el fichero, comando "unlock nombre_fichero".


  • Ejercicio 3:Modificad el programa anterior para que el servidor pueda atender peticiones de varios clientes


  • Ejercicio 4:Implentad una sencilla interfaz gráfica para el programa cliente.

 ENLACES





inicio | tablón| contacta