LECCIÓN 4
COMUNICACIONES BASADAS EN EL PROTOCOLO TCP 4.1 INTRODUCCIÓN El protocolo TCP (Transmisión Control Protocol) funciona en el nivel de transporte, basándose en el protocolo de red IP (Internet Protocol). IP proporciona comunicaciones no fiables y no basadas en conexión, muy dependientes de saturaciones en la red, caídas de nodos, etc. Por el contrario, TCP está orientado a conexión y proporciona comunicaciones fiables basadas en mecanismos de red que gestionan el control de flujo de paquetes y de congestión en los nodos. El control de flujo aludido es una característica técnica importante en el funcionamiento del protocolo TCP: evita que los nodos que envían información puedan saturar a los que la reciben; para lograr este objetivo, el protocolo TCP utiliza de manera interna un mecanismo de sincronización basado en tres comunicaciones realizadas entre cliente y servidor. Un aspecto que hay que tener en cuenta cuando se programan comunicaciones con TCP es la eficiencia que conseguimos. Cuando vamos a traspasar una gran cantidad de información (por ejemplo un fichero de vídeo), si no necesitamos tiempo real, TCP es un protocolo adecuado, puesto que el tiempo necesario para establecer la comunicación es despreciable respecto al utilizado para transmitir los datos. En el otro extremo, si necesitamos una gran cantidad de comunicaciones cortas en las que la fiabilidad no es muy importante, TCP no es un protocolo adecuado; sería mucho mejor UDP, que se explicará más adelante. Un ejemplo de esta situación es el caso de una serie de sensores de temperatura y presión que mandan sus medidas por la red cada segundo y un aparato que recoge las medidas y las visualiza en una central de control.
42 © JESÚS BOBADILLA SANCHO
En definitiva, el protocolo TCP posibilita la comunicación fiable de datos entre nodos cliente y nodos servidores; resulta especialmente adecuado cuando el tamaño de los datos que se transmiten no es pequeño. En Java, las comunicaciones TCP se realizan utilizando la clásica abstracción de socket. Los sockets nos permiten establecer y programar comunicaciones sin tener que conocer los niveles inferiores sobre los que se asientan. Para identificar el destino de los paquetes de datos, los sockets utilizan los conceptos de dirección y puerto. La dirección se refiere a la máquina a la que se dirigen los datos; se determina gracias a la resolución de nombres que proporcionan los DNS o simplemente aportando al socket, de manera directa, la dirección IP del nodo destino. Puesto que una misma máquina (nodo) puede hacerse cargo de recoger varias comunicaciones diferentes de datos (habitualmente ligadas a distintos servicios), existe la necesidad de proporcionar un mecanismo que nos permita distinguir los paquetes que llegan relacionados con los distintos servicios ofrecidos (correo, news, web, etc.): este mecanismo son los puertos. Los puertos se representan con valores enteros, que no deben coincidir para diferentes servicios. Los datos que enviemos a un nodo con dirección IP 138.100.57.45 y puerto 6000 acabarán siendo tratados por programas diferentes (servidores) que los enviados al mismo nodo (138.100.57.45) y puerto 7000. Este mecanismo no es tan diferente al que usamos en el correo ordinario: además de la dirección de la casa a la que enviamos una carta, indicamos la persona destinataria (que es el equivalente al puerto). Los valores numéricos de puertos 1-1023 se reservan a servicios de interés general, montados a menudo sobre protocolos de uso extendido: el 80 para web con HTTP, el 25 para correo saliente con SMTP, el 110 para correo entrante con POP3, el 119 para el servicio de noticias con NNTP, etc. Los valores de puertos entre 1024 y 49151 se usan para servicios específicos de uso no general, el resto (a partir de 49152) se emplean para designar servicios de uso esporádic o. En los siguientes apartados se explicará el mecanismo general con el que se utilizan los sockets TCP, la diferencia entre las clases Socket y ServerSocket, un primer ejemplo de comunicaciones (Hola mundo), una aplicación “teletipo”, otra de traspaso del contenido de un fichero, etc.
© JESÚS BOBADILLA SANCHO
43
4.2 ESTABLECIMIENTO DE COMUNICACIONES Java proporciona dos clases de abstracción de comunicaciones TCP: una para los procesos cliente (Socket) y otra para los procesos servidor (ServerSocket). Antes de entrar en los detalles de programación vamos a mostrar gráficamente el esquema básico de establecimiento de comunicaciones TCP.
1 El programa que proporciona el servicio (programa servidor) crea una instancia de la clase ServerSocket, indicando el puerto asociado al servicio: ServerSocket SocketServidor = new ServerSocket(Puerto);
2 El programa que proporciona el servicio invoca el método accept sobre el socket de tipo ServerSocket. Este método bloquea el programa hasta que se produce una conexión por parte de un cliente: ...SocketServidor.accept(); 3 El método accept devuelve un socket de tipo Socket, con el que se realiza la comunicación de datos del cliente al servidor: Socket ComunicaConCliente = SocketServidor.accept();
4 El programa cliente crea una instancia de tipo Socket, a la que proporciona la dirección del nodo destino y el puerto del servicio: Socket SocketCliente = new Socket(Destino, Puerto);
5 Internamente, el socket del cliente trata de establecer comunicación con el socket de tipo ServerSocket existente en el servidor; cuando la comunicación se establece es cuando realmente (físicamente) se produce el paso 3 del diagrama. 6 Con los pasos anteriores completados se puede empezar a comunicar datos entre el cliente (o clientes) y el servidor.
44 © JESÚS BOBADILLA SANCHO
4.3 TRANSMISIÓN DE DATOS TCP es un protocolo especialmente útil cuando se desea transmitir un flujo de datos en lugar de pequeñas cantidades aisladas de información. Debido a esta característica, los sockets de Java están diseñados para transmitir y recibir datos a través de los Streams definidos en el paquete java.io. La clase Socket contiene dos métodos importantes que se emplean en el proceso de transmisión de flujos de datos: InputStream getInputStream() OutputStream getOutputStream() Empleando el Stream de salida del socket del cliente y el Stream de entrada del socket del servidor podemos establecer un flujo de datos continuo a través de la conexión TCP establecida: Socket cliente
GetOutputStream()
Socket servidor
GetInputStream()
OutputStream FlujoDeSalida = SocketCliente.getOutputStream(); InputStream FlujoDeEntrada = ComunicaConCliente.getInputStream(); Las clases OutputStream e InputStream son abstractas, por lo que no podemos emplear directamente todos sus métodos. En general utilizaremos otras clases más especializadas que nos permiten trabajar con flujos de datos: DataOutputStream, DataInputStream, FileOutputStream, FileInputStream, etc. DataInputStream Flujo = new DataInputStream(FlujoDeEntrada); DataOutputStream Flujo = new DataOutputStream(FlujoDeSalida); Una vez definidas instancias de este tipo de clases, nos basta con emplear los métodos de escritura y lectura de datos que mejor se adapten a nuestras necesidades: Flujo.writeBytes("Línea de texto"); int BytesLeidos = Flujo.read(Mensaje);
© JESÚS BOBADILLA SANCHO
45
4.4 HOLA MUNDO Para consolidar todas las ideas expuestas hasta el momento vamos a realizar la aplicación más sencilla posible: el típico “Hola mundo” en versión TCP. En este caso necesitamos: • Un programa que se ejecute en el equipo cliente y envíe el texto “Hola Mundo”: TCPClienteHolaMundo • Un programa que se ejecute en el equipo servidor y reciba e imprima el mensaje: TCPServidorHolaMundo Los programas TCPClienteHolaMundo y TCPServidorHolaMundo han sido implementados siguiendo los esquemas mostrados en los apartados anteriores, por lo que resultarán muy fáciles de entender. Comencemos con TCPClienteHolaMundo: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
import java.net.Socket; import java.io.*; import java.net.UnknownHostException; public class TCPClienteHolaMundo { public static void main(String[] args) { OutputStream FlujoDeSalida; DataOutputStream Flujo; try { Socket SocketCliente = new Socket("localhost", 8000); FlujoDeSalida = SocketCliente.getOutputStream(); Flujo = new DataOutputStream(FlujoDeSalida); Flujo.writeBytes("Hola Mundo"); SocketCliente.close(); } catch (UnknownHostException e) { System.out.println("Referencia a host no resuelta"); } catch (IOException e) { System.out.println("Error en las comunicaciones"); } catch (SecurityException e) { System.out.println("Comunicacion no permitida por razones de seguridad"); } } }
46 © JESÚS BOBADILLA SANCHO
En la línea 1 se indica que vamos a utilizar la clase Socket del paquete java.net. Como ya sabemos, el cliente utiliza la clase Socket y el servidor utiliza, además, la clase ServerSocket. En la línea 11 se crea la instancia SocketCliente de la clase Socket, indicando que el nodo destino es la máquina local (“localhost”) y el puerto es el 8000 (asignado arbitrariamente). En la línea 14 se obtiene el objeto FlujoDeSalida, de tipo OutputStream (línea 8). Utilizando este objeto creamos una instancia Flujo (línea 15) de tipo DataOutputStream (línea 9). Entre los métodos de la clase DataOutputStream se encuentra writeBytes, que utilizamos para enviar el texto “Hola Mundo” (línea 16) a través de la red. Tras enviar este único mensaje cerramos el socket haciendo uso del método close (línea 18) y de esta manera liberamos los recursos empleados. Cuando utilizamos el método writeBytes debemos ser conscientes de que estamos escribiendo un texto en un objeto de tipo DataOutputStream, que a su vez se sustenta en un objeto de tipo OutputStream, que se encuentra asocia do a un socket de tipo Socket; de manera que, finalmente, como esperamos, los datos escritos acaban saliendo a la red a través del socket. Las líneas 19 a 28 contienen los diferentes tratamientos de excepciones que nos podemos encontrar al instanciar el socket y realizar el traspaso de información. Para completar TCPServidorHolaMundo: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
el
ejemplo
debemos
escribir
la
clase
import java.net.ServerSocket; import java.net.Socket; import java.io.*; public class TCPServidorHolaMundo { public static void main(String[] args) { byte[] Mensaje=new byte[80]; InputStream FlujoDeEntrada; DataInputStream Flujo; try { ServerSocket SocketServidor = new ServerSocket(8000); Socket ComunicaConCliente = SocketServidor.accept(); System.out.println("Comunicacion establecida"); FlujoDeEntrada =
© JESÚS BOBADILLA SANCHO
47
18 ComunicaConCliente.getInputStream(); 19 Flujo = new DataInputStream(FlujoDeEntrada); 20 int BytesLeidos = Flujo.read(Mensaje); 21 System.out.println(new String(Mensaje)); 22 23 ComunicaConCliente.close(); 24 SocketServidor.close(); 25 26 } catch (IOException e) { 27 System.out.println("Error en las comunicaciones"); 28 System.exit(0); 29 } catch (SecurityException e) { 30 System.out.println("Comunicacion no permitida por 31 razones de seguridad"); 32 System.exit(0); 33 } 34 35 } 36 37 }
En las líneas 1 y 2 importamos las clase ServerSocket y Socket, que vamos a utilizar. En la línea 3 nos aseguramos de que vamos a tener accesibles las distintas clases del paquete java.io que usamos en el programa: InputStream, DataInputStream e IOException. En la línea 12 creamos la instancia SocketServidor, utilizando el puerto 8000 (que ha de ser el mismo que el definido en TCPClienteHolaMundo); a esta instancia se le aplica el método accept (línea 14) para establecer la conexión y obtener el objeto ComunicaConCliente, de tipo Socket. En las líneas 17 y 18 se obtiene el Stream de entrada asociado al socket que acabamos de conseguir. En la línea 19 utilizamos el objeto FlujoDeEntrada, de tipo InputStream, para crear una instancia Flujo de tipo DataInputStream. La lectura de los datos que nos llegan por la línea de comunicaciones se hace, en nuestro ejemplo, empleando el método read de la clase DataInputStream (línea 20). Mensaje es un vector de bytes (línea 8) de 80 posiciones (el tamaño del vector ha sido elegido arbitrariamente). Finalmente se imprime por consola (línea 21) el texto que nos llega (esperamos “Hola Mundo”) y cerramos los sockets utilizados (líneas 23 y 24). Tras ejecutar cada programa en una consola diferente, las siguientes figuras muestran el correcto funcionamiento de nuestras clases. Nótese que debemos ejecutar en primer lugar la clase servidor, o nos encontraremos con una excepción de tipo IOException en el cliente, tal y como mostramos a continuación.
48 © JESÚS BOBADILLA SANCHO
4.5 APLICANDO LA PROGRAMACIÓN ORIENTADA A OBJETOS Partiendo del ejemplo “Hola Mundo” y cambiando muy pocas cosas, podemos crear dos clases abstractas: TCPCliente y TCPServidor que nos servirán de base para una gran cantidad de aplicaciones. Las nuevas clases se encargarán de crear los sockets necesarios y gestionar las excepciones que se puedan dar, dejando los detalles de envío y recepción de datos a sendos métodos abstractos Comunicacion, que deberá implementar cada aplicación concreta que deseemos realizar basándonos en las clases TCPCliente y TCPServidor. A continuación se muestra la clase TCPCliente, que consta de un constructor (línea 8) que admite como entrada la dirección del host y el puerto destino de las comunicaciones. La clase se encarga de crear el socket (línea 12), establecer el flujo de salida (líneas 14 y 15) y tratar las excepciones (líneas 20 a 28). El envío de datos de cada aplicación se debe implementar dentro del método abstracto Comunicación (línea 32), que actúa sobre el flujo de salida establecido (línea 17). 1 2 3 4
import java.net.Socket; import java.io.*; import java.net.UnknownHostException;
© JESÚS BOBADILLA SANCHO
49
5 public abstract class TCPCliente { 6 private DataOutputStream Flujo; 7 8 TCPCliente(String Host, int Puerto) { 9 OutputStream FlujoDeSalida; 10 11 try { 12 Socket SocketCliente = new Socket(Host, Puerto); 13 14 FlujoDeSalida = SocketCliente.getOutputStream(); 15 Flujo = new DataOutputStream(FlujoDeSalida); 16 17 Comunicacion(Flujo); 18 19 SocketCliente.close(); 20 } catch (UnknownHostException e) { 21 System.out.println("Referencia a host no 22 resuelta"); 23 } catch (IOException e) { 24 System.out.println("Error en las comunicaciones"); 25 } catch (SecurityException e) { 26 System.out.println("Comunicacion no permitida por 27 razones de seguridad"); 28 } 29 } 30 31 32 public abstract void Comunicacion (DataOutputStream 33 Flujo); 34 35 }
La clase TCPServidor es similar a TCPServidorHolaMundo, pero aplicando cambios similares a los explicados en TCPCliente . 1 2 3 4 5 6 7 8 9 10 11 12 13
import java.net.ServerSocket; import java.net.Socket; import java.io.*; public abstract class TCPServidor { private DataInputStream Flujo; TCPServidor(int Puerto){ byte[] Mensaje=new byte[256]; InputStream FlujoDeEntrada; try { ServerSocket SocketServidor = new ServerSocket(Puerto);
50 © JESÚS BOBADILLA SANCHO
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
Socket ComunicaConCliente = SocketServidor.accept(); System.out.println("Comunicacion establecida"); FlujoDeEntrada = ComunicaConCliente.getInputStream(); Flujo = new DataInputStream(FlujoDeEntrada); Comunicacion (Flujo); ComunicaConCliente.close(); SocketServidor.close(); } catch (IOException e) { System.out.println("Error en las comunicaciones"); System.exit(0); } catch (SecurityException e) { System.out.println("Comunicacion no permitida por razones de seguridad"); System.exit(0); } }
public abstract void Comunicacion (DataInputStream Flujo); }
4.6 APLICACIÓN TELETIPO: TALK Utilizando las clases desarrolladas hasta el momento vamos a crear una aplicación que nos permita visualizar por consola, en un ordenador, las frases que introducimos por teclado en otro (o el mismo) nodo. A esta aplicación se le denomina talk ; es un servicio telemático síncrono bastante utilizado. La clase TCPTeletipoCliente extiende a TCPCliente, por lo que heredamos la capacidad de establecer la conexión TCP que hemos programado en esta última clase. Al heredar la clase abstracta TCPCliente estamos obligados a implementar su método abstracto Comunicacion (línea 9); al menos si deseamos poder crear instancias de TCPTeletipoCliente .
© JESÚS BOBADILLA SANCHO
51
En el método Comunicacion nos encargaremos de programar la lectura de frases por teclado y el envío de estas frases por el flujo de salida Flujo, de tipo DataOutputStream. Para leer frases del teclado creamos un objeto Teclado de tipo InputStream, asignándolo a System.in (entrada estándar del sistema), todo ello en la línea 12. En la línea 16 utilizamos el método read para leer una secuencia de hasta 256 caracteres (línea 10) introducidos por el teclado. La siguiente línea de código (si no se levanta una excepción) es la 23, donde se escribe (write) por el Stream de salida (Flujo) el array de NumBytesLeidos bytes Valor. Finalmente, las líneas 28 y 29 convierten la secuencia de bytes en un String Mensaje, que utilizamos para codificar una condición de finalización del bucle de escritura y envío de datos (escribir el texto “Fin”). Cuando la ejecución del método Comunicación finaliza, se continúa con la secuencia de instrucciones del constructor de la clase TCPCliente , cerrándose el socket empleado en la comunicación. 1 import java.io.*; 2 3 public class TCPTeletipoCliente extends TCPCliente { 4 5 TCPTeletipoCliente(String Host, int Puerto) { 6 super(Host, Puerto); 7 } 8 9 public void Comunicacion (DataOutputStream Flujo) { 10 byte[] Valor = new byte[256]; 11 int NumBytesLeidos = 0; 12 InputStream Teclado = System.in; 13 String Mensaje; 14 do { 15 try { 16 NumBytesLeidos = Teclado.read(Valor); 17 } 18 catch (IOException e){ 19 System.out.println("Error en la entrada de datos 20 por consola"); 21 } 22 try { 23 Flujo.write(Valor,0,NumBytesLeidos); 24 }catch (IOException e) { 25 System.out.println("Error en la escritura de 26 datos a la linea");
52 © JESÚS BOBADILLA SANCHO
27 28 29 30 31 32 33 34
} Mensaje = new String(Valor); Mensaje = Mensaje.substring(0,NumBytesLeidos-2); } while (!Mensaje.equals("Fin")); } }
La clase TCPTeletipoServidor extiende la clase abstracta TCPServidor e implementa su único método Comunicacion. En primer lugar se lee cada frase proveniente del cliente (línea 15), utilizando el método read sobre el flujo de entrada Flujo. En las líneas 20 y 21 se convierte el array de bytes en un String; en la línea 22 se imprime el String por la consola y en la línea 24 se comprueba si llega la condición de finalización del bucle. 1 import java.io.*; 2 3 public class TCPTeletipoServidor extends TCPServidor { 4 5 TCPTeletipoServidor(int Puerto) { 6 super(Puerto); 7 } 8 9 public void Comunicacion (DataInputStream Flujo) { 10 byte[] buffer = new byte[256]; 11 int BytesLeidos=0; 12 String Mensaje; 13 do { 14 try { 15 BytesLeidos = Flujo.read(buffer); 16 } catch (IOException e) { 17 System.out.println("Error en la lectura de 18 datos por linea"); 19 } 20 Mensaje = new String(buffer); 21 Mensaje = Mensaje.substring(0,BytesLeidos-2); 22 System.out.println(Mensaje); 23 24 } while (!Mensaje.equals("Fin")); 25 } 26 27 }
© JESÚS BOBADILLA SANCHO
53
Para poder ejecutar la aplicación “Teletipo (talk)” únicamente nos falta crear las clases, cliente y servidor, que instancien a TCPTeletipoCliente y TCPTeletipoServidor. Los parámetros empleados en cliente: host destino y puerto destino, los obtenemos de los argumentos con los que invocamos a TCPTeletipoClientePrincipal. Lo mismo hacemos con el puerto local que requieren las clases de tipo servidor. 1 public class TCPTeletipoClientePrincipal { 2 3 public static void main(String[] args) { 4 int Puerto = Integer.parseInt(args[1]); 5 String Host = args[0]; 6 TCPTeletipoCliente InstanciaCliente = new 7 TCPTeletipoCliente(Host,Puerto); 8 } 9 10 } 1 2 3 4 5 6 7 8
public class TCPTeletipoServidorPrincipal { public static void main(String[] args) { int Puerto = Integer.parseInt(args[0]); TCPTeletipoServidor InstanciaServidor = new TCPTeletipoServidor(Puerto); } }
En los gráficos anteriores se observa como se comunica el cliente con el servidor. En este caso estamos utilizando un mismo equipo para albergar a ambos programas (cliente y servidor), por eso utilizamos el nombre “localhost”, que se refiere a la dirección de la máquina local.
54 © JESÚS BOBADILLA SANCHO
4.7 UTILIZACIÓN DE HILOS Cuando se programan servicios de comunicaciones normalmente surge la necesidad de utilizar threads para que diversos programas se ejecuten en paralelo, por ejemplo cuando existen distintos servidores y servicios en una misma máquina. Imaginemos que deseamos establecer un servicio de comunicaciones en cadena, de manera que una primera persona (el director) envía un mensaje a la segunda persona (un jefe de proyecto), que a su vez envía sus peticiones a una tercera persona (un analista) y así sucesivamente hasta llegar al programador. De esta manera la petición del director se va traduciendo en las distintas peticiones jerárquicas necesarias. Con las clases que hemos implementado podemos resolver fácilmente el problema: el director necesita una ventana de consola ejecutando la clase TCPTeletipoClientePrincipal y el programador también necesita una sola ventana de consola ejecutando la clase TCPTeletipoServidorPrincipal; todos los demás implicados necesitan dos ventanas: una para recibir mensajes de su superior y otra para enviar a su subordinado. Para evitar el uso de varias consolas simultáneas, podemos utilizar el mecanismo de programación concurrente que nos ofrece Java: los hilos. Veamos como podemos variar nuestras clases para obtener una ventana que atienda concurrentemente la entrada (servidor) y salida (cliente) de mensajes.
La clase cliente se programa en TCPTeletipoClienteConHilos, que implementa a Thread, por lo que es un hilo (línea 6); su constructor admite, además de las referencias Host y Puerto , un campo de texto AreaEntrada donde se escribe el mensaje de salida (líneas 16 y 17). El método run (línea 24) de la clase Thread implementa el establecimiento de comunicaciones (línea 26) y la determinación del flujo de salida (líneas 27 y 28). Este método se ejecuta cuando se arranca (start) cada hilo instanciado de esta clase. En la línea 41 se define la clase que implementa el único método (actionPerformed) del interfaz ActionListener. La línea 45 se encarga de recoger el mensaje introducido en el campo de texto (getText), convertirlo en array de bytes (getBytes) y escribirlo (write) en el Stream de salida (Flujo).
© JESÚS BOBADILLA SANCHO
55
El final de la aplicación (línea 50) se alcanza cuando se envía el mensaje “Fin” (línea 49). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
import import import import
java.io.*; java.awt.TextField; java.awt.event.*; java.net.*;
public class TCPTeletipoClienteConHilos extends Thread{ TextField AreaEntrada; OutputStream FlujoDeSalida; DataOutputStream Flujo; Socket SocketCliente; String Host; int Puerto;
TCPTeletipoClienteConHilos(String Host, int Puerto, TextField AreaEntrada) { this.AreaEntrada = AreaEntrada; this.Host = Host; this.Puerto = Puerto; }
public void run() { try { SocketCliente = new Socket(Host, Puerto); FlujoDeSalida = SocketCliente.getOutputStream(); Flujo = new DataOutputStream(FlujoDeSalida); } catch (UnknownHostException e) { System.out.println("Referencia a host no resuelta"); } catch (IOException e) { System.out.println("Error en las comunicaciones"); } AreaEntrada.addActionListener(new TextoModificado()); }
private class TextoModificado implements ActionListener { public void actionPerformed(ActionEvent e) { try { Flujo.write(AreaEntrada.getText().getBytes()); } catch (IOException IOe) {
56 © JESÚS BOBADILLA SANCHO
47 48 49 50 51 52 53 54 55
System.out.println("Error al enviar los datos"); } if (AreaEntrada.getText().equals("Fin")) System.exit(0); AreaEntrada.setText(""); } } }
La clase TCPTeletipoServidorConHilos se apoya en TCPServidorConHilos, por lo que el establecimiento de comunicaciones lo tenemos resuelto y nos basta con implementar el método abstracto Comunicacion. El método Comunicacion recoge los mensajes que le llegan por la red (línea 20), los convierte en String (línea 25) y los visualiza en un campo de texto (línea 26). El objeto de tipo TextField se obtiene a través del constructor de la clase (línea 9). El método Comunicacion finaliza cuando llega el mensaje “Fin” (línea 28). 1 import java.io.*; 2 import java.awt.TextField; 3 4 public class TCPTeletipoServidorConHilos extends 5 TCPServidorConHilos { 6 TextField AreaSalida; 7 8 TCPTeletipoServidorConHilos(int Puerto, 9 TextField AreaSalida) { 10 super(Puerto); 11 this.AreaSalida = AreaSalida; 12 } 13 14 public void Comunicacion (DataInputStream Flujo) { 15 byte[] buffer = new byte[256]; 16 int BytesLeidos=0; 17 String Mensaje; 18 do { 19 try { 20 BytesLeidos = Flujo.read(buffer); 21 } catch (IOException e) { 22 System.out.println("Error en la lectura de 23 datos por linea"); 24 } 25 Mensaje = new String(buffer,0,BytesLeidos); 26 AreaSalida.setText(Mensaje); 27 28 } while (!Mensaje.equals("Fin"));
© JESÚS BOBADILLA SANCHO
29 30 31 }
57
}
La clase TCPServidorConHilos es idéntica a TCPServidor, salvo en la definición de la clase, donde ahora se extiende Tread: public abstract class TCPServidorConHilos extends Thread{ Finalmente necesitamos una clase, TCPTeletipoConHilos, que defina el interfaz gráfico de usuario, instancie los hilos cliente y servidor y los arranque (start). El GUI de la aplicación se define entre las líneas 6 y 24. En las líneas 12 y 13 se instancian los campos de texto Entrada y Salida, que serán utilizados para introducir mensajes (que recoge el hilo cliente) y visualizar los mensajes provenientes de la red (que recoge el hilo servidor). En las líneas 30 y 31 se intancia el hilo servidor, mientras que en las líneas 32 a 34 se hace lo propio con el hilo cliente. Una vez creados los hilos se arrancan (método start) en las líneas 35 y 36. En las líneas 27 a 29 se recogen los argumentos de llamada a la clase, siguiendo el orden: dirección destino, puerto destino, puerto local. 1 import java.awt.*; 2 3 public class TCPTeletipoConHilos { 4 5 public static void main(String[] args) { 6 Frame MiMarco = new Frame(); 7 Panel Visor = new Panel(new GridLayout(2,1)); 8 Panel AreaEnviar = new Panel(new 9 FlowLayout(FlowLayout.LEFT)); 10 Panel AreaRecibir = new Panel(new 11 FlowLayout(FlowLayout.LEFT)); 12 TextField Entrada = new TextField(30); 13 TextField Salida = new TextField(30); 14 15 AreaEnviar.add(new Label("Enviado :")); 16 AreaEnviar.add(Entrada); 17 AreaRecibir.add(new Label("Recibido:")); 18 AreaRecibir.add(Salida); 19 Visor.add(AreaEnviar); 20 Visor.add(AreaRecibir);
58 © JESÚS BOBADILLA SANCHO
21 MiMarco.add(Visor); 22 MiMarco.setSize(400,90); 23 MiMarco.setTitle("Talk con TCP"); 24 MiMarco.setVisible(true); 25 26 if (args.length==3) { 27 String HostRemoto = args[0]; 28 int PuertoLocal = Integer.parseInt(args[2]); 29 int PuertoRemoto = Integer.parseInt(args[1]); 30 TCPTeletipoServidorConHilos InstanciaServidor = new 31 TCPTeletipoServidorConHilos(PuertoLocal,Salida); 32 TCPTeletipoClienteConHilos InstanciaCliente = new 33 TCPTeletipoClienteConHilos(HostRemoto, 34 PuertoRemoto,Entrada); 35 InstanciaServidor.start(); 36 InstanciaCliente.start(); 37 } 38 else { 39 System.out.println("Es necesario pasar tres 40 argumentos (host remoto, puerto remoto, puerto local)"); 41 System.exit(0); 42 } 43 44 } 45 46 }
Ejecutando en tres máquinas diferentes las líneas de comando: Java TCPTeletipoConHilos Host2, 8002, 8000 Java TCPTeletipoConHilos Host3, 8004, 8002 Java TCPTeletipoConHilos Host4, 8006, 8004 ... Obtenemos
© JESÚS BOBADILLA SANCHO
59
4.8 TRASPASO DE FICHEROS El protocolo TCP resulta especialmente útil para transmitir flujos de datos que requieren una comunicación fiable; un ejemplo de esta situación es el envío de un programa, que podría tener un tamaño considerable y que no admite errores en la recepción. En el siguiente ejemplo se proporcionan dos clases (TCPFicheroCliente y TCPFicheroServidor) que se encargan de traspasar un fichero de un ordenador a otro. TCPFicheroCliente extiende la clase TCPCliente, por lo que únicamente necesitamos implementar el método abstracto Comunicacion (línea 10). Para acceder al fichero secuencial utilizamos la clase FileInputStream (líneas 16 y 17). Si no existen problemas al leer el fichero (líneas 18 a 21) nos metemos en un bucle (línea 24) donde se leen (método read en la línea 25) hasta 256 bytes (líneas 11 y 12). Tras la lectura de datos en el fichero, se realiza la escritura (write) en el Stream de salida a la red (línea 26); el bucle se termina cuando en alguna lectura del fichero no hemos rellenado por completo el buffer de entrada (línea 27). La última acción es cerrar el fichero de entrada (línea 28). 1 import java.io.*; 2 3 public class TCPFicheroCliente extends TCPCliente { 4 private FileInputStream FicheroOrigen; 5 6 TCPFicheroCliente(String Host, int Puerto) { 7 super(Host, Puerto); 8 } 9 10 public void Comunicacion (DataOutputStream Flujo) { 11 final int TAMANIO_BUFFER = 256; 12 byte buffer[] = new byte[TAMANIO_BUFFER]; 13 int NumBytesLeidos = 0; 14 15 try { 16 FicheroOrigen = new 17 FileInputStream("TCPCliente.java"); 18 } catch (FileNotFoundException e) { 19 System.out.println("Fichero no encontrado"); 20 System.exit(0); 21 } 22 23 try { 24 do {
60 © JESÚS BOBADILLA SANCHO
25 26 27 28 29 30 31 32 33
NumBytesLeidos = FicheroOrigen.read(buffer); Flujo.write(buffer,0,NumBytesLeidos); } while (NumBytesLeidos == TAMANIO_BUFFER); FicheroOrigen.close(); } catch (IOException e){ System.out.println(e.getMessage()); } } }
La clase TCPFicheroServidor se encarga de recoger los datos que llegan por la red (línea 20) y de escribirlos en el fichero de salida (línea 21). Los detalles de implementación son muy similares a los que existen en la clase TCPClienteServidor. 1 import java.io.*; 2 3 public class TCPFicheroServidor extends TCPServidor { 4 5 TCPFicheroServidor(int Puerto) { 6 super(Puerto); 7 } 8 9 public void Comunicacion (DataInputStream Flujo) { 10 final int TAMANIO_BUFFER = 256; 11 byte buffer[] = new byte[TAMANIO_BUFFER]; 12 int NumBytes=0; 13 14 try { 15 FileOutputStream FicheroDestino = new 16 FileOutputStream("Salida.txt"); 17 18 try { 19 do { 20 NumBytes = Flujo.read(buffer); 21 FicheroDestino.write(buffer,0,NumBytes); 22 } while (NumBytes==TAMANIO_BUFFER); 23 FicheroDestino.close(); 24 } catch (IOException e){ 25 System.out.println("Error de 26 entrada/salida"); 27 } 28 29 } catch (FileNotFoundException e) { 30 System.out.println("Fichero no encontrado"); 31 } 32 33 } 34 }
© JESÚS BOBADILLA SANCHO
61
Para poder ejecutar estas clases únicamente nos falta instanciarlas desde sendos métodos main. En la línea 5 de la clase TCPFicheroClientePrincipal utilizamos directamente la dirección IP del nodo destino de la comunicación. 1 2 3 4 5 6 7 8
public class TCPFicheroClientePrincipal {
1 2 3 4 5 6 7 8
public class TCPFicheroServidorPrincipal {
public static void main(String[] args) { TCPFicheroCliente InstanciaCliente = new TCPFicheroCliente("138.100.155.17",20000); } }
public static void main(String[] args) { TCPFicheroServidor InstanciaServidor = new TCPFicheroServidor(20000); } }
En los siguientes gráficos aparece el resultado de ejecutar la aplicación y comprobar que el fichero ha sido traspasado correctamente:
62 © JESÚS BOBADILLA SANCHO
4.9 COMUNICACIÓN BIDIRECCIONAL Cuando se establece una conexión TCP, existe la posibilidad de realizar comunicaciones bidireccionales a través de los sockets existentes en el cliente y el servidor. El modelo cliente-servidor se adapta de forma natural al enfoque de comunicación semidúplex: se realiza la petición de servicio en un sentido y, posteriormente, se envía la respuesta en sentido contrario; en este apartado se presenta un ejemplo muy conciso en el que un programa cliente y un programa servidor se envían información en los dos sentidos (de cliente a servidor y de servidor a cliente). El programa cliente es TCPClienteDuplexHolaMundo. En la línea 13 se define el socket a través del cual se establecen las comunicaciones en la parte del cliente. En las líneas 15 a 21 se definen los Streams que canalizarán los datos de entrada y salida. La línea 23 codifica un bucle dentro del cual se realiza, de manera alternativa, la comunicación de salida (línea 25) y la de entrada (líneas 28 a 34). La condición de finalizació n se ha establecido de manera que el bucle itere 20 veces. Finalmente, en la línea 37 se cierra el socket. 1 import java.net.*; 2 import java.io.*; 3 4 public class TCPClienteDuplexHolaMundo { 5 6 public static void main(String[] args) { 7 DataInputStream FlujoEntrada; 8 DataOutputStream FlujoSalida; 9 byte[] Mensaje=new byte[80]; 10 int BytesLeidos=0, Frases=0; 11 12 try { 13 Socket SocketCliente = new Socket("localhost", 8000); 14 15 OutputStream FlujoDeSalida = 16 SocketCliente.getOutputStream(); 17 InputStream FlujoDeEntrada = 18 SocketCliente.getInputStream(); 19 20 FlujoEntrada = new DataInputStream(FlujoDeEntrada); 21 FlujoSalida = new DataOutputStream(FlujoDeSalida); 22 23 do { 24 try { 25 FlujoSalida.writeBytes("Hola terricola\n"); 26 Frases++;
© JESÚS BOBADILLA SANCHO
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
63
BytesLeidos = FlujoEntrada.read(Mensaje); } catch (IOException e) { System.out.println("Error en la lectura de datos"); System.exit(0); } System.out.print(new String(Mensaje,0,BytesLeidos)); } while (Frases!=20); SocketCliente.close(); } catch (UnknownHostException e) { System.out.println("Referencia a host no resuelta"); } catch (IOException e) { System.out.println("Error en las comunicaciones"); } } }
El programa servidor, TCPServidorDuplexHolaMundo, realiza acciones muy similares a TCPClienteDuplexHolaMundo. En este caso se crea una instancia de la clase ServerSocket (línea 14) y, con ella, se crea el socket (línea 15) que se comunicará con el cliente. Las comunicaciones se comienzan con la lectura de datos (líneas 28 a 32) y se terminan con la escritura (línea 34). 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
import java.net.ServerSocket; import java.net.Socket; import java.io.*; public class TCPServidorDuplexHolaMundo { public static void main(String[] args) DataInputStream FlujoEntrada; DataOutputStream FlujoSalida; byte[] Mensaje=new byte[80]; int BytesLeidos=0, Frases=0;
{
try { ServerSocket SocketServidor = new ServerSocket(8000); Socket ComunicaConCliente = SocketServidor.accept(); System.out.println("Comunicacion establecida"); OutputStream FlujoDeSalida =
64 © JESÚS BOBADILLA SANCHO
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 } 48 49 50 }
InputStream
ComunicaConCliente .getOutputStream(); FlujoDeEntrada = ComunicaConCliente .getInputStream();
FlujoEntrada = new DataInputStream(FlujoDeEntrada); FlujoSalida = new DataOutputStream(FlujoDeSalida); do { try { BytesLeidos = FlujoEntrada.read(Mensaje); } catch (IOException e) { System.out.println("Error en la lectura de datos"); System.exit(0); } System.out.print(new String(Mensaje,0,BytesLeidos)); FlujoSalida.writeBytes("Hola humano\n"); Frases++; } while (Frases!=20);
ComunicaConCliente.close(); SocketServidor.close(); } catch (IOException e) { System.out.println("Error en las comunicaciones"); System.exit(0); }
© JESÚS BOBADILLA SANCHO
65
4.10 CONFIGURACIÓN DE LAS COMUNICACIONES Los ejemplos que hemos desarrollado utilizan los valores por defecto en el comportamiento de los sockets, y por tanto en el desarrollo de la s comunicaciones. Es posible variar por programa ciertos comportamientos de las comunicaciones, así como obtener y establecer una gran cantidad de información relativa a las clases Socket y ServerSocket. ServerSocket Métodos principales Socket accept() void bind(SocketAddress a)
void close() InetAddress getInetAddress() int getLocalPort() int getSoTimeout()
void setSoTimeout(int ms)
Acción Espera a que se realice una conexión y devuelve un socket para comunicarse con el cliente Asigna la dirección establecida al socket creado con accept, si no se utiliza este método se asigna automáticamente una dirección temporal Cierra el socket Devuelve la dirección a la que está conectada el socket Devuelve el número de puerto asociado al socket Devuelve el valor en milisegundos que el socket espera al establecimiento de comunicación tras la ejecución de accept Asigna el número de milisegundos que el socket espera al establecimiento de comunicación tras la ejecución de accept
Socket Métodos principales void bind(SocketAddress a)
void close() void connect(SocketAddress a) void connect(SocketAddress a, int ms) InetAddress getInetAddress() InputStream getInputStream() int getLocalPort() OutputStream getOutputStream() int getPort() int getSoLinger() int getSoTimeout()
Acción Asigna la dirección establecida al socket creado con accept, si no se utiliza este método se asigna automáticamente una dirección temporal Cierra el socket Conecta el socket a la dirección de servidor establecida Conecta el socket a la dirección de servidor establecida, esperando un máximo de ms milisegundos Devuelve la dirección a la que está conectada el socket Devuelve el stream de entrada asociado al socket Devuelve el número de puerto asociado al socket Devuelve el stream de salida asociado al socket Devuelve el valor del Puerto remoto al que está conectado Devuelve el número de milisegundos que se espera a los datos después de cerrar (close) el socket Devuelve el valor en milisegundos que el socket espera al establecimiento de comunicación tras la ejecución de accept
66 © JESÚS BOBADILLA SANCHO
boolean isBound() boolean isClosed() boolean isConnected() void setSoLinger(boolean Activo, int ms) void setSoTimeout(int ms)
Indica si el socket está vinculado Indica si el socket está cerrado Indica si el socket está conectado Se establece si se esperan o no los datos después de cerrar el socket (y cuanto tiempo se esperan) Asigna el número de milisegundos que el socket espera al establecimiento de comunicación tras la ejecución de accept