Descarga la aplicación para disfrutar aún más
Vista previa del material en texto
Práctica 3: Introducción a los sockets en Java En esta práctica se va a tener una primera toma de contacto con la interfaz de los sockets en Java. Para ello plantearemos una serie de ejercicios muy sencillos que ilustran algunos conceptos básicos del funcionamiento de los sockets TCP en Java. 1. Entorno de trabajo Java está disponible para diferentes sistemas operativos y con diversos entornos de programación. Nosotros vamos a trabajar en Linux. Para editar el programa os sugerimos el uso del editor kwrite. Por otra parte, recuerda que si el nombre del fichero fuente de nuestro programa es Ejemplo.java lo compilaremos mediante la orden “javac Ejemplo.java ”, y lo ejecutaremos tecleando “java Ejemplo ”. Además, el lenguaje Java distingue entre mayúsculas y minúsculas, y el nombre del fichero debe coincidir con el de la clase principal. Toda la información sobre las clases de Java puede encontrarse en la página Web de Sun: http://java.sun.com/j2se/1.4/docs/api. Por otra parte, en http://www.ulpgc.es/otros/tutoriales/java se puede consultar un curso de Java. P3-2 Prácticas de Redes de Computadores 2. Cómo establecer conexión con un servidor La clase java.net.Socket es la clase Java fundamental para realizar operaciones TCP desde el extremo cliente. A partir de aquí nos referiremos a ella como clase Socket . Esta clase posee varios constructores que permiten especificar el host destino y el número de puerto al que queremos conectarnos. El host puede especificarse como un objeto InetAddress (que corresponde a una dirección IP) o como un String . El número de puerto siempre se indica como un valor int que puede variar desde el 1 al 65.535. Empezaremos probando el costructor más sencillo: ”public Socket (String host, int puerto) throws IOException, UnknownHostException” Este constructor crea un socket TCP e intenta conectarlo al host y puerto remotos especificados como parámetros. Si el servidor DNS no está en funcionamiento o no puede resolver el nombre, el constructor lanzará una excepción UnknownHostException . Si el nombre se resuelve pero el socket no puede conectar por alguna otra razón, se lanzará una excepción IOException . Esto puede deberse, entre otras razones, a que el puerto destino en el servidor no esté abierto, existan problemas de encaminamiento en la red para alcanzar el destino o simplemente el servidor especificado esté apagado. Ejercicio 1: Escribe un programa en java, “ ClienteTCP1.java ” , que sea capaz de aceptar dos parámetros de entrada en línea de órdenes para establecer una conexión: el nombre de un servidor y un número de puerto al que conectar en ese servidor. El programa debe visualizar un mensaje por pantalla indicando si la conexión se ha establecido o no. En caso de éxito debe mostrar también el número de puerto y el nombre del servidor con el que ha conectado, y en caso de fallo indicar el motivo: “Nombre de servidor desconocido” o “No es posible realizar conexión”, dependiendo el tipo de excepción ocurrida (ver notas en página siguiente). Prueba tu programa con los siguientes servidores y puertos, comprobando lo que sucede: 1. mail.redes.upv.es 25 2. herodes.redes.upv.es 25 3. zoltar.redes.upv.es 25 Introducción a los sockets en Java P3-3 Ejercicio 1 (NOTAS): Para facilitar la depuración, el programa debe comprobar si hay parámetros de entrada y, si no se han suministrado, intentar conectarse al puerto 25 del servidor “zoltar.redes.upv.es”. Por otra parte, recuerda que los parámetros de entrada en Java son de tipo “String” y dado que el puerto en el constructor Socket es un “int” , es necesario realizar una conversión de tipo. De este modo, si args[1] es el parámetro correspondiente al puerto, la conversión puede hacerse, por ejemplo, como: int puerto = Integer.parseInt(args[1]) . Otra opción para crear el socket es pasarle como parámetro la dirección IP mediante un objeto InetAddress , en lugar del nombre de dominio del servidor. En este caso se podría generar una excepción IOException si no se puede conectar pero no una UnknownHostExcep- tion . Ahora la consulta al DNS se lleva a cabo al crear el objeto InetAddress y es, en este momento, cuando pueden surgir los problemas relacionados con la traducción nombre-dirección IP. La clase Inet- Address posee varios métodos estáticos que permiten crear objetos Inet- Address inicializados de forma adecuada. El más popular es: public static InetAddress InetAddress.getByName(String HostName) que se utiliza de la forma: InetAddress dirIP = InetAddress.getByName(“www.upv.es”) Si se van a abrir varias conexiones al mismo host (como haremos en el último apartado de la práctica) es más eficiente emplear este constructor, para resolver únicamente una vez la dirección IP a la que se desea conectar e indicarla directamente en el constructor Socket . Ejercicio 2: Copia el “ClienteTCP1.java” a “ClienteTCP2.java” . Modifícalo para que traduzca el nombre del host antes de crear el socket. Comprueba que funcio- na de forma equivalente. 3. Cómo obtener información sobre la conexión establecida La clase Socket dispone de varios métodos que permiten obtener información sobre la conexión establecida entre el cliente y el servidor. P3-4 Prácticas de Redes de Computadores Entre ellos podemos citar getLocalAddress() y getInetAddress() , que devuelven las direcciones IP local y remota, respectivamente, y getLocalPort() y getPort() , que devuelven los puertos local y remoto, respectivamente. A continuación se detallan brevemente los méto- dos mencionados. Puedes consultar más información sobre ellos en la web de Sun, que se cita al inicio de la práctica • public int getPort() : devuelve el puerto remoto al que el socket está conectado. • public InetAddress getInetAddress() : devuelve la direc- ción IP remota a la que el socket está conectado. • public int getLocalPort() : devuelve el puerto local al que el socket está ligado. • public InetAddress getLocalAddress() : Devuelve la di- rección IP local a la que el socket está ligado. Ejercicio 3: Modifica el cliente “ClienteTCP2.java” del ejercicio anterior para que muestre información en la pantalla del cliente sobre la conexión que establece (direcciones IP y números de puerto locales y remotos). Ejecútalo cuatro veces seguidas, conectándote con el servidor del laboratorio “zoltar.redes.upv.es” en el puerto 25 y comprueba qué valores se modifican. Interpreta el resultado obtenido. 4. Cómo leer los datos que se reciben a través de la conexión Para leer los datos que se van recibiendo a través del socket utilizaremos el método getInputStream de la clase Socket . Este método devuelve un objeto del tipo InputStream (flujo de octetos de entrada), lo que se ajusta bien a la filosofía TCP de transmisión orientada a flujo conti- nuo de datos, pero no resulta cómodo para leer los mensajes del servidor. Lo que nos conviene, para facilitar nuestro trabajo, es un método que propor- cione un flujo de caracteres y, a ser posible, en líneas de texto completas. La clase InputStreamReader es un “puente” desde los flujos de bytes a los de caracteres: lee octetos y los codifica en caracteres empleando un código de caracteres determinado. Adicionalmente, para mejorar la eficiencia en la conversión pueden leerse varios octetos en cada operación Introducción a los sockets en Java P3-5 de lectura anidando esta clase dentro de una BufferedReader . Por ejemplo, de la forma siguiente: BufferedReader lee = new BufferedReader(new InputStreamReader(s.getInputStream())); siendo “s” el objeto de la clase Socket definido previamente. Cuando un programa lee de un BufferedReader , el texto se extrae de un buffer en lugar de acceder al flujo de entrada directamente. Cuando el buffer se vacía, vuelve a llenarse con tanto texto como sea posible, incluso aunque no todo sea aún necesario. Esto hará que las futuras lecturasse lleven a cabo más rápidamente. En nuestro caso, empleando el método readLine de la clase BufferedReader podremos leer las respuestas del servidor como líneas completas de texto. Este método lee una línea de texto y la devuelve como un String . public String readLine() throws IOException Ejercicio 4: Renombra el cliente “ClienteTCP2.java” del ejercicio anterior como “ClienteSMTP.java” . Modíficalo para que se conecte siempre al puerto 25 y muestre la primera línea de texto que recibe del servidor. 5. Cómo enviar datos a través de la conexión Para escribir a través de un socket también es más eficiente utilizar una clase que proporcione cierta capacidad de almacenamiento (buffering). Por este motivo, para escribir a través del socket, utilizaremos un objeto de la clase java.io.PrintWriter conectado al flujo de salida del socket. Este objeto proporciona la capacidad de almacenamiento deseada. Además, esta clase permite manejar adecuadamente conjuntos de caracteres y texto internacional. Uno de sus constructores (el que utilizaremos habitualmente) es: public PrintWriter(OutputStream out, boolean autoFlush) que además posee una ventaja importante como comprobaremos a continuación. Para ello el segundo parámetro del constructor debe invocarse con el valor true . P3-6 Prácticas de Redes de Computadores 6. Vaciado del buffer TCP (flush) Aunque las ventajas de emplear para la escritura una clase con almacenamiento son claras, también puede plantear algunos inconvenientes si uno no es cuidadoso, como vamos a ver en el ejercicio siguiente. Ejercicio 5: Copia el programa “ClienteSMTP.java ” y renómbralo como “ClienteSMTPej5.java” . Añade, al final de tu programa, el código siguiente: PrintWriter esc = new PrintWriter (s.getOutputStream()); salida.print("EHLO redesXX.redes.upv.es\r\n"); System.out.println(“lectura 2: “ + lee.readLine()); s.close(); NOTA: Se ha supuesto que el objeto BufferedReader definido en el ejercicio anterior se llama “lee” y el objeto Socket se llama “s” . Ejecútalo para que se conecte al puerto 25 de “zoltar.redes.upv.es”. ¿Qué es lo que ocurre?¿Aparece la segunda respuesta del servidor en tu pantalla? (Lo normal será que no aparezca nada). Este cliente SMTP debería enviar un mensaje correcto al servidor SMTP de zoltar y recibir su respuesta, pero no recibe nada. ¿Por qué? Porque él tampoco le envía nada. Para mejorar la eficiencia, el stream de salida intenta llenar su buffer tanto como sea posible antes de enviar los datos, pero como el cliente no tiene más datos que enviar (de momento) su petición no llega a enviarse nunca. La solución a este problema la da el método flush () de la clase PrintWriter . Este método fuerza a que se envíen los datos aunque el buffer no esté aún lleno. En caso de duda acerca de si resulta o no necesario utilizarlo, es mejor invocarlo, ya que realizar un flush innecesario consume pocos recursos, pero no utilizarlo cuando se necesita puede provocar bloqueos en el programa. Ejercicio 6: Modifica el cliente SMTP para que utilice el método flush y comprueba que ahora funciona correctamente. Introducción a los sockets en Java P3-7 Podemos realizar el vaciado del buffer de forma automática al escribir en éste (sin tener que emplear el método flush explícitamente). Para ello necesitamos dos cosas: • El constructor de la clase PrintWriter debe emplearse tal y como se ha mostrado antes, con un segundo parámetro adicional a true . • La escritura debe realizarse mediante el método println() , en lugar del método print . Además el método println añade el final de línea con lo que no necesitamos escribirlo como parte de la orden que envía el cliente (a diferencia de lo que hemos hecho en el programa anterior). Vamos a modificar nuestro cliente para comprobar este funciona- miento. Ejercicio 7: Modifica el cliente SMTP para que utilice el método println en lugar del print (elimina lo que sobra, como la secuencia \r\n y el flush ). Comprueba que funciona volviendo a establecer conexión con el servidor zoltar en el puerto 25. NOTA: Hay que tener en cuenta que cuando usamos el método println , lo que se envía como final de línea es \n . El estándar especifica que debe enviarse \r\n . No obstante, como podéis haber comprobado al usar println , el programa funciona correctamente. Esto es debido a la generosidad del servidor, que contesta a la petición a pesar de que no se ajusta completamente al estándar. Podemos ajustarnos fácilmente al estándar definiendo los finales de línea como \r\n con la sentencia System.setProperty (“line.separador”, ”\r\n”); 7. Cierre de la conexión Las conexiones se cierran mediante el método close() de la clase Socket . Una vez que un socket se ha cerrado, ya no está disponible para volverlo a utilizar (por ejemplo, no puede ser reconectado). En caso necesario, hay que volver a crear un nuevo socket. Si este socket tenía un flujo asociado, el flujo se cierra también. P3-8 Prácticas de Redes de Computadores 8. Explorador de puertos ¿Está seguro nuestro ordenador? ¿Es fácil entrar en él de forma no autorizada desde otro ordenador? Responder a esta pregunta es complicado. En general, sabemos que para entrar en nuestro ordenador desde otro equipo es necesario que en alguno de los puertos de nuestro ordenador haya un servidor escuchando. En el caso más extremo, si todos los puertos de nuestro ordenador están cerrados, tenemos la seguridad de que no vamos a aceptar datos que provengan de la red, por lo que evitamos la entrada no deseada de intrusos. Cerrar todos los puertos del ordenador puede no ser una buena idea, pues seguramente algunos de los servicios que usamos habitualmente (y de los cuales incluso no somos conscientes) dejarán de funcionar, con la correspondiente molestia. Una política más acertada es mantener abiertos únicamente los puertos que necesitamos y cerrar el resto. No obstante, a menudo el sistema operativo se configura con determinadas opciones por defecto, dejando abiertos algunos puertos que seguramente no deseamos que estén abiertos. En otros casos, es posible que un intruso haya abierto un puerto en nuestro ordenador y haya dejado en él un servidor, del cual no tenemos conocimiento. La mejor manera de saber qué puertos están abiertos en nuestro ordenador es usar un explorador de puertos. Realizar esta tarea en TCP es muy sencillo. Basta con recorrer los puertos de nuestro ordenador intentando conectarnos a ellos. Si un puerto nos permite conectarnos, significa que hay un servidor escuchando en él. Si rechaza la conexión, entonces el puerto está cerrado. En el caso de UDP, hacer la exploración no es tan sencilla, pues al ser un servicio sin conexión, si no recibimos contestación, nunca vamos a tener la seguridad de qué es lo que ha pasado con nuestro mensaje. Para crear el explorador de puertos nos vamos a valer de la clase Socket , que ya hemos empleado en los ejercicios anteriores. Como hemos visto, su constructor puede lanzar dos excepciones diferentes: • UnknownHostException , que se genera cuando el nombre del orde- nador con el que queremos crear la conexión no puede ser resuelto a una dirección IP. • IOException , que se genera cuando no se puede establecer la co- nexión por cualquier otro motivo, como por ejemplo que no haya un Introducción a los sockets en Java P3-9 servidor escuchando en el puerto especificado en los parámetros de Socket . Utilizaremos esta segunda excepción para crear el explorador de puertos, de manera que si mientras barremos los puertos de nuestro equipo se genera esta excepción, sabremos que el puerto está cerrado. Si no se genera, entonces es que hay un servidor en ese puerto. Como lo habitual es que la mayoría de los puertos se encuentren cerrados, nuestro explorador debe mostrar por pantalla un mensaje únicamente si el puerto está abierto. En caso contrario,suelen salir tantos mensajes que resulta difícil averiguar todos los puertos abiertos. Ejercicio 8: Teclea y completa el siguiente explorador de puertos. Ejecútalo. ¿Qué puertos hay abiertos?¿A qué servicios corresponden esos puertos? (en /etc/services hay un listado detallado de los servicios ordenados por número de puerto). import java.net.*; import java.io.*; public class LowPortScanner { public static void main(String[] args) { String host = "localhost"; for (int puerto = 1; puerto < 1024; puerto ++) { try { // COMPLETAR CÓDIGO } catch (UnknownHostException e) { // COMPLETAR CÓDIGO } catch (IOException e) { // COMPLETAR CÓDIGO } } // end for } // end main } // end PortScanner Ejercicio 9: Elimina de tu ordenador todos los ficheros que has creado durante el desarrollo de esta práctica.
Compartir