Trabajo en red. Parte 3
Comunicación mediante el protocolo TCP
La clase Socket
Un objeto java.net.Socket es un "conector" a través del cual enviamos y recibimos datos mediante el protocolo TCP. A diferencia de los "conectores" java.net.DatagramSocket, que eran usados para enviar paquetes sueltos, estos "conectores" TCP sirven para enviar o recibir datos de forma continua, como si trabajáramos con un flujo InputStream o OutputStream.
El hecho de que el protocolo subyacente sea TCP nos permite olvidarnos de detalles relacionados con la pérdida de datos, ya que es el propio protocolo el
encargado de hacerlo por nosotros. A todos los efectos, podemos tratar un Socket TCP como un canal carente de errores.
¿Cómo se usa un objeto Socket? La inicialización de estos objetos es más compleja que en el caso de DatagramSocket, ya que es necesario que
previamente haya alguien "escuchando" en el extremo receptor.
Suponiendo que, de alguna forma, hay un programa "escuchando" en el puerto 1234 de la máquina con dirección IP 209.41.57.70, la inicialización de nuestro Socket sería:
InetAddress d = InetAddress.getByName("209.41.57.70");
Socket s = new Socket(d,1234);
/* Utilizacion del socket */
...
/* Cerramos el socket */
s.close();
Una vez tenemos un Socket abierto con otra máquina, podemos obtener un flujo de entrada o de salida para poder recibir o transmitir datos. Esto se hace
con los métodos Socket.getInputStream() y Socket.getOutputStream():
Veamos un ejemplo donde abrimos un socket, leemos los bytes que nos transmitan desde el otro extremo y los imprimimos en pantalla:
InetAddress d = InetAddress.getByName("209.41.57.70");
Socket s = new Socket(d, 1234);
Inputstream is = s.getInputStream();
while((int dato=is.read())!=-1){
System.out.println("Recibido " + dato);
}
is.close();
s.close();
La clase ServerSocket
La clase java.net.ServerSocket es el mecanismo mediante el cual nuestros programas pueden quedarse "escuchando" en un puerto, esperando conexiones entrantes. La forma general de trabajar con sockets será entonces: Un programa (lo llamaremos "servidor") crea un ServerSocket en un determinado puerto conocido por el resto de programas. El servidor queda esperando a que algún cliente intente conectar con él. En el momento en que se establece la conexión, ambos programas (el cliente y el servidor) obtienen un objeto Socket. Mediante objetos InputStream y OutputStream obtenidos a través de los objetos Socket, el cliente y el servidor intercambian datos. Uno de los dos programas cierra la conexión.
La parte del cliente ya la hemos visto en los apartados anteriores. Veamos como se realiza la parte del servidor.
La forma más sencilla de crear un objeto ServerSocket es indicando el número de puerto al constructor:
ServerSocket ss = new ServerSocket(1234);
/* Utilizamos el objeto ServerSocket */
Una vez creado, tenemos que quedarnos esperando a que alguien intente realizar la conexión. Esto se consigue mediante la función ServerSocket.accept(). Esta función espera una conexión entrante, y devuelve un objeto de tipo Socket.
ServerSocket ss = new ServerSocket(1234);
Socket s = ss.accept();
/* Utilizamos el objeto Socket */
s.close();
Una vez el servidor tiene el objeto Socket, puede realizar las mismas acciones que el cliente (extraer los flujos de entrada/salida, cerrar la conexión, etc.)
En el siguiente ejemplo, nuestro servidor espera la conexión entrante y responde con un mensaje de bienvenida:
ServerSocket ss = new ServerSocket(1234);
Socket s = ss.accept();
OutputStream os = s.getOutputStream();
String mensaje = "¡Conéctate con otro sitio y déjame en paz, pesado!";
byte[] matriz = mensaje.getBytes();
os.write(matriz);
os.close();
s.close();
De un objeto ServerSocket se pueden obtener muchos objetos Socket diferentes, cada uno independiente de los demás. Por ejemplo, podemos tener un programa que trabaje en el puerto 80 y que asigne cada nueva conexión a un hilo de ejecución distinto. No es necesario que se cierren los objetos Socket previos antes de poder aceptar una nueva conexión. Esto es lo que hace que los servidores Web puedan atender a varias personas al mismo tiempo, sin tener que esperar a terminar con cada cliente antes de atender al siguiente.
Por ejemplo, supongamos que tenemos una clase de objetos "MiniServidor", que implementan la interfaz Runnable y que están programados para responder a las peticiones que les llegan a través de un Socket. Una posible implementación para el servidor sería:
Serversocket ss = new ServerSocket(1234);
while(true){
Socket s = ss.accept();
MiniServidor m = new MiniServidor(s);
Thread t = new Thread(m);
t.start();
}
Gestión de excepciones
Como en todos los casos en que tengamos que trabajar con protocolos de red o sistemas de entrada/salida, tenemos que encargarnos de gestionar las posibles excepciones. Las más comunes son java.io.IOException (para los casos en los que haya problemas con la conexión) y java.net.UnknownHostException (cuando especificamos una dirección IP desconocida o incorrecta).
Un ejemplo cliente/servidor completo usando TCP
Con el fin de comparar ambos métodos, vamos a realizar un par de programas que realicen la misma función que el ejemplo anterior, pero usando el protocolo TCP en vez de UDP.
Veremos una implementación en la que para cada nuevo número transmitido el cliente abre una conexión TCP distinta.Aunque este sistema es perfectamente válido, no es muy eficiente, ya que pierde mucho tiempo en el proceso de establecimiento de las conexiones. Sería más deseable una implementación en la que todas las peticiones fueran a través del mismo socket TCP.
El funcionamiento del programa es muy similar al del que usaba Datagramas. Se realiza un completo control de errores.
import java.io.*;
import java.net.*;
class servidor{
public static void main(String args[]){
// Primero indicamos la dirección IP local
try{
System.out.println("LocalHost = " + InetAddress.getLocalHost().toString());
} catch (UnknownHostException uhe){
System.err.println("No puedo saber la dirección IP local : " + uhe);
}
// Abrimos un "Socket de Servidor" TCP en el puerto 1234.
ServerSocket ss = null;
try{
ss = new ServerSocket(1234);
} catch (IOException ioe){
System.err.println("Error al abrir el socket de servidor : " + ioe);
System.exit(-1);
}
int entrada;
long salida;
// Bucle infinito
while(true){
try{
// Esperamos a que alguien se conecte a nuestro Socket
Socket sckt = ss.accept();
// Extraemos los Streams de entrada y de salida
DataInputStream dis = new DataInputStream(sckt.getInputStream());
DataOutputStream dos = new DataOutputStream(sckt.getOutputStream());
// Podemos extraer información del socket
// Nº de puerto remoto
int puerto = sckt.getPort();
// Dirección de Internet remota
InetAddress direcc = sckt.getInetAddress();
// Leemos datos de la peticion
entrada = dis.readInt();
// Calculamos resultado
salida = (long)entrada*(long)entrada;
// Escribimos el resultado
dos.writeLong(salida);
// Cerramos los streams
dis.close();
dos.close();
sckt.close();
// Registramos en salida estandard
System.out.println( "Cliente = " + direcc + ":" + puerto +
"\tEntrada = " + entrada +
"\tSalida = " + salida );
} catch(Exception e){
System.err.println("Se ha producido la excepción : " +e);
}
}
}
}
class cliente {
public static void main(String args[]){
// Leemos el primer parámetro, donde debe ir la dirección
// IP del servidor
InetAddress direcc = null;
try{
direcc = InetAddress.getByName(args[0]);
} catch(UnknownHostException uhe){
System.err.println("Host no encontrado : " + uhe);
System.exit(-1);
}
// Puerto que hemos usado para el servidor
int puerto = 1234;
// Para cada uno de los argumentos...
for (int n=1;n<args.length;n++){
Socket sckt = null;
DataInputStream dis = null;
DataOutputStream dos = null;
try{
// Convertimos el texto en número
int numero = Integer.parseInt(args[n]);
// Creamos el Socket
sckt = new Socket(direcc,puerto);
// Extraemos los streams de entrada y salida
dis = new DataInputStream(sckt.getInputStream());
dos = new DataOutputStream(sckt.getOutputStream());
// Lo escribimos
dos.writeInt(numero);
// Leemos el resultado final
long resultado = dis.readLong();
// Indicamos en pantalla
System.out.println( "Solicitud = " + numero +
"\tResultado = " +resultado );
// y cerramos los streams y el socket
dis.close();
dos.close();
} catch(Exception e){
System.err.println("Se ha producido la excepción : " +e);
}
try{
if (sckt!=null) sckt.close();
} catch(IOException ioe){
System.err.println("Error al cerrar el socket : " + ioe);
}
}
}
}
Aparte de la introducción de los métodos Socket.getPort() y Socket.getInetAddress(), que nos devuelven, respectivamente, el puerto y la dirección IP de la máquina remota, el código anterior únicamente resume las ideas presentadas con anterioridad.
Se deja como "ejercicio para el lector" el cambiar el código anterior para que todas las peticiones que el cliente transmite al servidor vayan por el mismo socket TCP.
Trabajo con URLs
Una URL (Uniform Resource Locator) es, a grandes rasgos, el nombre de un determinado recurso (archivos, bases de datos, ordenadores, impresoras, etc)
en Internet. Por ejemplo, http://www.akal.com/index2.htm es una URL que "apunta" a una página dentro de un servidor Web.
El formato de una URL está definido en el standard RFC 1738, y suele seguir el esquema:
PROTOCOLO://MAQUINA/DIRECTORIO/SUBDIRECTORIO/ARCHIVO
Por ejemplo:
ftp://ftp.microsoft.com/public/file.txt
http://www.etsit.upv.es/iaeste/index.html
Una forma más general, que nos permite especificar el puerto de conexión, así como el login y el password es la siguiente:
http://login:password@maquina:puerto/dir/subdir/archivo
En Java, las URLs se representan mediante la clase java.net.URL. Esta clase solo representa la dirección, no su contenido. Para acceder al contenido de un objeto URL necesitamos obtener un objeto java.net.URLConnection, extraído a partir del propio URL.
Por ejemplo, el siguiente código crea una URL que apunta a http://www.javasoft.com, posteriormente obtiene un objeto URLConnection y por último hace algo realmente útil: leer el contenido de la URL:
URL direccion = new URL("http://www.javasoft.com");
URLConnection conex = direccion.openConnection();
InputStream entrada = conex.getInputStream();
while((int dato=entrada.read())!=-1){
/* Hacemos algo interesante con lo que leemos */
}
El ejemplo anterior es una muestra muy simple de como utilizar las clases URL y URLConnection. En una aplicación real probablemente habría que usar el resto de métodos que nos proporciona la clase URLConnection (para conocer datos del recurso remoto, leer información sobre la comunicación y el tipo de
protocolo usado, etc.)
Veamos un ejemplo completo. El siguiente programa simplemente se conecta con una URL especificada en la línea de comandos y guarda el contenido en un archivo.
/* Este programa muestra como trabajar con URL's a través de Java */
/* El programa se limita a acceder a una URL, extraer su stream de salida,
y leer los datos byte a byte, guardándolos en el archivo especificado */
import java.net.*;
import java.io.*;
class getURL{
public static void main(String params[]){
/* El programa debe recibir dos parámetros:
1.- la URL
2.- el archivo local donde guardar el resultado */
if (params.length<2){
System.err.println("Necesito una URL y un nombre de archivo local válidos");
System.exit(-1);
}
/* Intentamos acceder a la URL */
try{
/* Si la URL fuera incorrecta saltaría al catch de MalformedURLException */
URL miUrl = new URL(params[0]);
/* Obtenemos una URLConnection, mediante openConnection(), y sacamos
un InputStream mediante getInputStream() */
/* Si se produce algún error salta al catch de IOException */
InputStream is = miUrl.openConnection().getInputStream();
/* Abrimos el archivo para escritura */
FileOutputStream fos = new FileOutputStream(params[1]);
int dato;
/* Vamos leyendo bytes hasta que read() nos devuelva -1 */
while ((dato=is.read()) != -1)
fos.write(dato);
/* Cerramos todos los streams */
fos.close();
is.close();
}
catch(MalformedURLException errorURL){
System.err.println("La URL " + params[0] + " es incorrecta");
}
catch(IOException errorIO){
System.err.println("Error de entrada/salida : " + errorIO);
}
} /* fin main */
} /*fin clase */
La forma de invocar el programa sería como sigue:
java getURL http://www.akal.com/index2.htm mifichero.htm
En el código anterior además aparecen las excepciones típicas que aparecen en estos casos (java.net.MalformedURLException, java.io.IOException, etc.)