Download Introducción a sockets en Java

Document related concepts
no text concepts found
Transcript
INVESTIGACIÓN D COMO CREAR UN
SOCKET.
NORMA SANCHEZ YAÑEZ.
Introducción a 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.
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
UnknownHostException”
IOException,
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
P3-2
Prácticas de Redes de Computadores
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
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 UnknownHostException. Ahora la consulta
Introducción a los sockets en Java
P3-3
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 nombredirección IP. La clase InetAddress posee varios métodos estáticos que
permiten crear objetos InetAddress inicializados de forma adecuada. El
más popular es:
public
static
HostName)
InetAddress
InetAddress.getByName(String
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
funciona 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.
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étodos
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
•
public int getLocalPort(): devuelve el puerto local al que el
InetAddress
getInetAddress():
dirección IP remota a la que el socket está conectado.
socket está ligado.
devuelve
la
P3-4
•
Prácticas de Redes de Computadores
public
InetAddress
getLocalAddress(): Devuelve la
direcció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 continuo 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 proporcione 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 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 lecturas se lleven
a cabo más rápidamente.
Introducción a los sockets en Java
P3-5
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.
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:
“ClienteSMTP.java” y renómbralo como
“ClienteSMTPej5.java”. Añade, al final de tu programa, el código
Copia
el
programa
siguiente:
PrintWriter esc = new PrintWriter
(s.getOutputStream());
salida.print("EHLO redesXX.redes.upv.es\r\n");
P3-6
Prácticas de Redes de Computadores
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.
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 funcionamiento.
Introducción a los sockets en Java
P3-7
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.
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
P3-8
Prácticas de Redes de Computadores
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
ordenador 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 conexión
por cualquier otro motivo, como por ejemplo que no haya un 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:
Introducción a los sockets en Java
P3-9
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
COMENTARIO:
ESTE TIPO DE INVESTIGACION NOS FORTALECE NUESTROS
CONOCIMIENTOS.
PARA QUE SAMOS MEJORES CADA DIA.