Programación Distribuida y su Aplicación Bajo Internet Módulo 3.- Programación Concurrente y Prog. en red Curso 2001–2002. Juan S. Sendra Índice 1. Programación Concurrente 1.1. Introducción . . . . . . . . . . 1.2. Tareas en Java . . . . . . . . 1.3. Planificación . . . . . . . . . 1.4. Sincronización . . . . . . . . . 1.4.1. Exclusión mútua . . . 1.4.2. Espera y Notificación 1.5. Animación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2. Programación en Red 2.1. Contenido del paquete java.net . . . . . . . . 2.2. Direcciones de Internet . . . . . . . . . . . . . 2.3. Ports . . . . . . . . . . . . . . . . . . . . . . . 2.4. Protocolos . . . . . . . . . . . . . . . . . . . . 2.5. URLs . . . . . . . . . . . . . . . . . . . . . . 2.6. Sockets . . . . . . . . . . . . . . . . . . . . . 2.6.1. Lectura/Escritura de datos desde/a un 1. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 2 3 5 6 7 8 9 . . . . . . . . . . . . . . . . . . . . . . . . socket . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 13 13 15 16 16 19 21 . . . . . . . . . . . . . . . . . . . . . Programación Concurrente Programación Concurrente es la denominación de las técnicas de programación utilizadas para expresar paralelismo (varias actividades simultáneas) y resolver los problemas de comunicación y sincronización que se presentan. Java es uno de los pocos lenguajes de programación que incorpora construcciones para expresar concurrencia1 . 1 La programación concurrente resulta mucho más simple y natural en Java que en otros lenguajes 1 1 PROGRAMACIÓN CONCURRENTE 1.1. 1.1 Introducción Introducción La ejecución secuencial (una actividad tras otra) presenta limitaciones a distintos niveles: flexibilidad .- no podemos alterar la secuencia de ejecución para dar paso a tareas más urgentes, o responder de inmediato a las acciones del usuario interactividad .- la interacción con el usuario debe seguir estrictamente el orden secuencial especificado por el programa eficiencia .- Un sistema informático dispone de diferentes recursos que potencialmente pueden explotarse en paralelo (ej.- podemos servir una petición a disco y simultáneamente utilizar el procesador, la tarjeta de red, etc.). Si imponemos un orden secuencial podemos dilapidar ese potencial (ej.- si la aplicación lanza una operación sobre disco, durante el periodo de lectura/escritura el procesador no puede ejecutar nuevas acciones En la actualidad muchas aplicaciones necesitan concurrencia interna Ej.un navegador web puede visualizar un documento mientras descarga otros ficheros. La concurrencia interna se implementa lanzando varas tareas que cooperan entre sı́. Cada tarea mantiene un estado propio (código a ejecutar, pila, registro, punto de ejecución) y además comparte con el resto los recursos definidos globalmente por la aplicación. Las distintas tareas se ejecutan teóricamente en paralelo. En la realidad, el número de procesadores es inferior al de tareas, por lo que se utilizauna estrategia de tiempo compartido, multiplexando el tiempo de procesador entre las distintas tareas. La programación concurrente plantea ventajas e inconvenientes 2 1 PROGRAMACIÓN CONCURRENTE 1.2 Tareas en Java Ventajas Inconvenientes Facilita la programación reactiva .- algunos programas deben realizar actividades como respuesta (reacción) a estı́mulos externos. Ej.- un sistema de tiempo real que controla un proceso fı́sico debe responder a cambios en temperatura, humedad, iluminación, etc. Seguridad .- Además de comprobar que cada tarea es correcta, debemos garantizar que las tareas no interfieren entre sı́ Disponibilidad .- podemos replicar los servicios o explotar el paralelismo para mejorar la disponibilidad (ej.- la impresora es varios órdenes de magnitud más lenta que un disco; cada escritura en impresora se traduce en volcar los datos a disco –tras lo cual la aplicación puede continuar– y lanzar una tarea que vuelca datos de disco a impresora en cuanto ésta está libre Simplifica el diseño .- Los objetos reales suelen mostrar comportamiento autónomo (cada uno evoluciona de forma independiente). Para representarlos por programa es mucho más fácil lanzar una tarea para cada uno de ellos. Ej.- para simular vehı́culos, que poseen dirección, velocidad, etc. distintos entre sı́ Vivacidad .- Una o más tareas pueden bloquearse indefinidamente por distintas causas (ej.- otras actividades monopolizan el uso de la CPU, o existe un interbloqueo) No determinismo .- La multiplexación del tiempo de procesador no es fija, sino que puede variar en cada ejecución. Un programa que depende del intercalado concreto de instrucciones resulta impredecible (no determinista), complicando su depuración Coste .- Hay un coste extra por la multiplexación del tiempo de CPU (hay que decidir qué tarea procede a continuación, y realizar el cambio de contexto entre tareas), y un coste de sincronización (para evitar interferencias entre tareas) Paralelismo .- en sistemas multiprocesador podemos lanzar tareas independientes en cada procesador, y en sistemas monoprocesador multiplexar el tiempo de procesador para explotar el paralelismo con otros dispositivos 1.2. Tareas en Java Podemos implementar tareas de dos formas: Extender la clase java.lang.Thread. Posee los métodos para controlar una tarea start .- inicia la ejecución de la tarea run .- contiene el código a ejecutar stop .- detiene la tarea 3 1 PROGRAMACIÓN CONCURRENTE 1.2 Tareas en Java otros .- métodos para suspender/relanzar la tarea, retrasar su ejecución durante el periodo indicado, ceder el control a otras tareas, etc. class ImprimeNum extends Thread { public void run() { for (int i=-10; i<10; i++) System.out.print(""+i); } } public class Test1 { public static void main (String[] arg) { ImprimeNum in = new ImprimeNum(); in.start(); } } Utilizar el interfaz java.lang.Runnable. Lanzamos la tarea pasando al constructor un objeto que implementa Runnable. Resulta útil cuando queremos lanzar la tarea para un objeto que ya extiende a otra clase. class ImprimeNum implements Runnable { public void run() { for (int i=-10; i<10; i++) System.out.print(""+i); } } public class Test1 { public static void main (String[] arg) { Thread in = new Thread(new ImprimeNum()); in.start(); } } Una vez iniciada la ejecución el procesador divide su tiempo entre las sentencias tras start() y el código de la tarea (en run). Cuando se alcanza el final del método run (o se invoca stop) la tarea ha finalizado. El siguiente programa lanza tres tareas ImprimeNum. El orden en que se muestra la salida del programa depende del intercalado de instrucciones en la CPU, y por lo tanto es impredecible (puede variar al ejecutarse en sistemas diferentes, o incluso entre ejecuciones en un mismo sistema). class ImprimeNum extends Thread { public void run() { for (int i=-10; i<10; i++) System.out.print(""+i); } } public class Test2 { public static void main (String[] arg) { ImprimeNum in1 = new ImprimeNum(); 4 1 PROGRAMACIÓN CONCURRENTE 1.3 Planificación ImprimeNum in2 = new ImprimeNum(); ImprimeNum in3 = new ImprimeNum(); in1.start(); in2.start(); in3.start(); } } Podemos dar nombre a las distintas tareas (ej.- para identificarlas durante la depuración). El constructor de Thread admite como parámetro una tira de caracteres que corresponde al nombre de la tarea. Ej.- Incorporando nombre a las tareas, podemos distinguir el autor de cada lı́nea en nuestro programa ejemplo class ImprimeNum extends Thread { public ImprimeNum(String nom) {super(nom);} public void run() { for (int i=-10; i<10; i++) System.out.print(getName()+": "+i); } } public class Test3 { public static void main (String[] arg) { ImprimeNum in1 = new ImprimeNum("Pepe"); ImprimeNum in2 = new ImprimeNum("Carlos"); ImprimeNum in3 = new ImprimeNum("Maria"); in1.start(); in2.start(); in3.start(); } } 1.3. Planificación Una tarea es ejecutable (candidata a utilizar el procesador) si ha arrancado (con start), no ha terminado todavı́a (no ha finalizado el código de run ni se ha ejecutado stop), y no está esperando un recurso. Las tareas ejecutables se sitúan en unas colas de planificación organizadas por prioridades y controladas por el soporte en ejecución de Java. Por defecto, cada nueva tarea tiene la misma prioridad que su creador Podemos alterar la prioridad invocando Thread.setPriority con un argumento entero en el rango [1..10] (mayor valor implica mayor prioridad). La clase Thread define las constantes MAX PRIORITY, NORM PRIORITY, MAX PRIORITY Cuando el procesador queda libre, se elige para ejecución el proceso ejecutable de mayor prioridad: Si hay más de uno con dicha prioridad, se elige uno de forma no determinista Si se necesita ejecutar una tarea de más prioridad que el que ocupa el procesador, éste es expulsado (pre-empted) 5 1 PROGRAMACIÓN CONCURRENTE 1.4 Sincronización El método yield devuelve voluntariamente el procesador public class Test4 { public static void main (String[] arg) { ImprimeNum in1 = new ImprimeNum("Pepe"); ImprimeNum in2 = new ImprimeNum("Carlos"); ImprimeNum in3 = new ImprimeNum("Maria"); in1.setPriority(Thread.MIN_PRIORITY); in2.setPriority(Thread.NORM_PRIORITY); in3.setPriority(Thread.MAX_PRIORITY); in1.start(); in2.start(); in3.start(); } } Los métodos de control (para afectar a la planifiación) son los siguientes: start .- Provoca que la tarea invoque su método run como una actividad independiente. A menos que se invoque sobre la tarea un método especial de control (ej. stop), la tarea termina cuando finaliza run isAlive .- Es un método que devuelve cierto cuando la tarea ha arrancado pero todavı́a no ha finalizado stop .- Termina la tarea de forma irrevocable. Tras la terminación puede invocarse de nuevo start para lanzar una nueva actividad usando la misma tarea. La forma alternativa stop(excepcion) detiene la tarea y lanza la excepción indicada. suspend .- Suspende temporalmente la tarea, de forma que prosigue cuando otra tarea ejecuta resume sobre la tarea detenida. sleep .- Suspende la tarea durante el número de milisegundos especificado, y luego la reactiva de forma automática join .- Suspende al invocante hasta que se completa la tarea indicada (un segundo argumento opcional indica un plazo máximo de espera en milisegundos) interrupt .- Provoca que se aborte la espera (iniciada con sleep, wait o join) lanzando una interrupción tipo InterruptedException 1.4. Sincronización En general las tareas deben cooperar entre sı́, y para ello comparten objetos (ej. una tarea modifica el estado de un objeto y otra tarea lee dicho estado). Nuestra responsabilidad es garantizar que las distintas tareas no interfieren entre sı́ (ej. una tarea no debe cambiar los datos mientras otra los usa). 6 1 PROGRAMACIÓN CONCURRENTE 1.4 Sincronización Ej.- el siguiente código no protege contra posibles interferencias, y por tanto no es determinista (dos tareas intentan modificar los campos del mismo objeto, y el orden en que se modifican los datos depende del intercalado concreto). public class Contador { int i=0; public void cuenta() { int limite = i+100; while (i++ != limite) System.out.println(i); } } public class TareaContador extends Thread { Contador c; public static void main (String[] arg) { Contador c = new Contador(); TareaContador t1 = new TareaContador(c); TareaContador t2 = new TareaContador(c); t1.start(); t2.start(); } public TareaContador(Contador c0) {c = c0;} public void run() {c.cuenta();} } 1.4.1. Exclusión mútua Las interferencias entre tareas únicamente pueden aparecer en los fragmentos de código que acceden a objetos compartidos (denominados secciones crı́ticas). Para garantizar el determinismo (evitar interferencias) es suficiente garantizar la exclusión mútua entre secciones crı́ticas. Con exclusión mútua garantizamos la ejecución secuencial (no concurrente) de las secciones crı́ticas → no pueden multiplexarse en el tiempo instrucciones correspondientes a distintas secciones crı́ticas. Java garantiza la exclusión mútua en el acceso a un método o bloque de código mediante la palabra synchronized public class ContadorSincronizado extends Contador { public synchronized void void cuenta () { int limite = i+100; while (i++ != limite) System.out.println(i); } } La palabra synchronized significa que dicho método es una sección crı́tica, o sea que debe ejecutarse en exclusión mútua con las restantes secciones crı́ticas del objeto (si las tareas t1 y t2 invocan métodos sincronizados sobre el mismo objeto, una de las dos debe esperar). Java garantiza que las operaciones primitivas (ej.- asignaciones sobre todos los tipos primitivos excepto long y double) son atómicas, y siempre 7 1 PROGRAMACIÓN CONCURRENTE 1.4 Sincronización pueden utilizarse con seguridad en contextos multitarea. Cualquier otro fragmento de código que debe comportarse como atómico requiere la etiqueta synchronized. La exclusión mútua se implementa mediante un contador interno de cada objeto Java. Dicho contador se incrementa cuando se invoca un método sincronizado, y se decrementa cuando finaliza la ejecución del mismo. Un método no sincronizado puede ejecutarse con independencia del valor del contador, pero los métodos sincronizados sólo se pueden ejecutar si el contador vale 0 (en caso contrario dicha tarea queda temporalmente suspendida). Los métodos definidos en interfaces no pueden etiquetarse como sincronizados. 1.4.2. Espera y Notificación Además de evitar interferencias, las tareas deben comunicarse información utilizando objetos compartidos. Algunos de los métodos definidos en un objeto sólo deben invocarse en determinados estados del objeto. ej.- en una lista podemos consultar la longitud independientemente del estado de la lista, pero sólo podemos extraer si la lista no está vacı́a. Cuando una tarea invoca un método sobre un objeto compartido, y el estado del objeto no permite la ejecución de ese método, dicha tarea debe esperar. En general, la estructura de un método sincronizado es: public synchronized tipoRetorno metodoEj (param) { while (no se puede ejecutar) wait(); .... // operaciones normales if (el nuevo estado permite reactivar a otro) notify(): } Los métodos de sincronización disponibles son: wait .- suspende la tarea actual (se deposita en una cola interna asociada al objeto) y libera la exclusión mútua sobre ese objeto. Existe un método wait(ms), donde se especifica la espera máxima en milisegundos (transcurrido ese plazo, se reactiva de forma automática) notify .- reactiva a una (arbitrariamente) de las tareas de la cola asociada al objeto notifyAll .- igual que notify, pero liberando a todas las tareas suspendidas sobre el objeto 8 1 PROGRAMACIÓN CONCURRENTE 1.5 Animación Si durante la espera en la cola asociada al objeto ocurre una interrupción (InterruptedException), se reactiva automáticamente la tarea (y se ejecuta la correspondiente cláusula catch). Ej.- Implementamos una lista en las que distintas tareas pueden insertar/extraer de forma simultánea. Los métodos Inserta y Extrae son sincronizados, pero Longitud no. La lista posee un tamaño máximo (indicado en la creación), se implementa mediante un vector circular, y sólo permite extraer si no está vacı́a e insertar si no está llena. public class Lista { int cabeza, cola, n, max; public Lista (int tallaMax) { max=tallaMax; v = new Object[max]; // para guardar los datos cabeza=cola=n=0; // inicialmente vacia } public int longitud() { return n; } public synchronized void inserta(Object x) { while (n==max) // lista llena try {wait();} catch(InterruptedException e) {return;} v[cola]=x; cola = (cola+1) % max; n++; notifyAll(); // reactivamos por si alguien desea extraer } public synchronized Object extrae() { while (n==0) // lista vacia try {wait();} catch(InterruptedException e) {return;} Object x=v[cabeza]; cabeza = (cabeza+1) % max; n--; notifyAll(); // reactivamos por si alguien desea insertar return x; } } 1.5. Animación La animación consiste en una sucesión rápida (y perfectamente sincronizada) de imágenes. Si deseamos realizar animaciones en un applet debemos: 1. Calcular la nueva imagen e invocar repaint para actualizar pantalla 2. Repetir el paso 1) de forma periódica (ej.- cada 50ms para obtener 20 imágenes por segundo) La siguiente discusión sobre animación nos permitirá ilustrar los mecanismos de programación concurrente. 9 1 PROGRAMACIÓN CONCURRENTE 1.5 Animación Ej1.- Animación simple de una pelota (cı́rculo rojo) rebotando dentro de una caja. La función paint se limita a dibujar la bola: import java.awt.*; public class BolaLoca extends java.applet.Applet implements Runnable { int x=30, y=40, r=20; // posicion y radio de la bola int incx=2, incy=2: // direccion y magnitud del movimiento public void init() {(new Thread(this)).start();} public void paint(Graphics g) { g.setColor(Color.red); g.fillOval(x, y, r, r); } } public void run() { while (true) { // bucle infinito int nx =x+incx; if (nx>=size().width || nx<0) incx*=-1; else x=nx; int ny =y+incy; if (ny>=size().heigth || ny<0) incy*=-1; else y=ny; repaint(); } } } Ej2.- Una animación correcta requiere una base de tiempos. El Ej1 muestra una bola con velocidad errática porque ésta depende de la multiplexación del tiempo de procesador. Incorporamos la base de tiempos: import java.awt.*; public class BolaLoca extends java.applet.Applet implements Runnable { int x=30, y=40, r=20; // posicion y radio de la bola int incx=2, incy=2: // direccion y magnitud del movimiento int retardo=50; // retardo de 50ms equivale a 20 fotogramas por segundo public void init() {(new Thread(this)).start();} public void paint(Graphics g) { g.setColor(Color.red); g.fillOval(x, y, r, r); } } public void run() { while (true) { // bucle infinito long inicio = (new Date()).getTime(); int nx =x+incx; if (nx>=size().width || nx<0) incx*=-1; else x=nx; int ny =y+incy; if (ny>=size().heigth || ny<0) incy*=-1; else y=ny; repaint(); 10 1 PROGRAMACIÓN CONCURRENTE 1.5 Animación try { Thread.currentThread.sleep(retardo-(new Date()).getTime()+inicio) } catch (InterruptedException e) {} } } } Ej3.- El ejemplo 2 muestra una velocidad constante, pero también aparece un notable efecto parpadeo debido a la falta de sincronización entre el momento en que el sistema desea actualizar pantalla y el momento en que desea hacerlo el programa. Para resolver el parpadeo debemos reducir al mı́nimo el intervalo de actualización de pantalla. Existen dos vı́as de solución (utilizar un área de clipping y utilizar doble buffering para pantalla), pero únicamente desarrollamos el esquema denominado doble buffering. Se trata de componer el dibujo sobre una variable interna, y no sobre pantalla (posteriormente se vuelca esa variable a pantalla, un proceso mucho más rápido que dibujar cada elemento individual). A;adimos al applet el siguiente código: private Image im; private Dimension tallaim; private Graphics g2; public final synchronized void update (Graphics g) { Dimension d=size(); if (im==null || (d.width != tallaIm.width) || (d.heigth != tallaIm.heigth)) { im = createImage(d.width, d.height); tallaIm = d; g2 = im.getGraphics(); } g2.clearRect(0,0,d.width,d.height); paint(g2); g.drawImage(im,0,0,null); } Ej4.- Añadimos la posibilidad de detener/reanudar la animación mediante sendos clicks de ratón import java.awt.*; public class BolaLoca extends java.applet.Applet implements Runnable { int x=30, y=40, r=20; // posicion y radio de la bola int incx=2, incy=2: // direccion y magnitud del movimiento Thread t; // tarea subyacente boolean activo; private Image im; 11 1 PROGRAMACIÓN CONCURRENTE 1.5 Animación private Dimension tallaim; private Graphics g2; public void init() { t=new Thread(this); activo=false; addMouseListener (new MouseAdapter() { public void mousePressed(MouseEvent e) { if (activo) t.suspend(); else t.resume(); activo = !activo; } }); } public void start() {t.start(); activo=true;} public final synchronized void update (Graphics g) { Dimension d=size(); if (im==null || (d.width != tallaIm.width) || (d.heigth != tallaIm.heigth)) { im = createImage(d.width, d.height); tallaIm = d; g2 = im.getGraphics(); } g2.clearRect(0,0,d.width,d.height); paint(g2); g.drawImage(im,0,0,null); } public void paint(Graphics g) { g.setColor(Color.red); g.fillOval(x, y, r, r); } } public void run() { while (true) { // bucle infinito int nx =x+incx; if (nx>=size().width || nx<0) incx*=-1; else x=nx; int ny =y+incy; if (ny>=size().heigth || ny<0) incy*=-1; else y=ny; repaint(); } } } 12 2 PROGRAMACIÓN EN RED 2. Programación en Red Las caracterı́sticas de portabilidad, junto a las facilidades para acceder a servicios básicos de red (URLs, sockets, etc.), justifican el desarrollo de programas Java diseñados para ejecución en red (ej. monitorización remota de equipos, aplicaciones de trabajo en grupo, etc.). Este capı́tulo introduce el soporte básico de red, y analiza la estructura de programas en red simples. 2.1. Contenido del paquete java.net El paquete java.net contiene el soporte para programación en red, organizado como sigue: general Datagrama Socket URL Clases ContentHandler InetAddress HttpURLConnection DatagramPacket DatagramSocket DatagramSocketImpl MulticastSocket ServerSocket Socket SocketImpl URL URLConnection URLEncoder URLStreamHandler Interfaces ContentHandlerFactory FileNameMap Excepciones BindException ConnectException NoRouteToHostException UnknownHostException UnknownServiceException SocketImplFactory URLStreamHandlerFactory URLStreamHandlerFactory MalformedURLException El resto del capı́tulo introduce los distintos conceptos (IP, socket, URL, etc.) y detalla las definiciones asociadas con cada uno. 2.2. Direcciones de Internet Cada ordenador conectado a Internet se identifica mediante una dirección única de 4 bytes denominada dirección IP (normalmente se escriben los cuatro valores separados por puntos: ej 199.3.45.234). Para simplificar su uso, asociamos nombres a las direcciones IP (ej. ”www.hp.com”, ”mistral.dsic.upv.es”). Un ordenador pueden disponer de varias direcciones IP (ej. porque dispone de varias tarjetas de red), pero generalmente sólo posee una. En cualquier caso, la máquina posee un nombre simbólico único. Una dirección IP corresponde a la clase java.net.InetAddress 13 2 PROGRAMACIÓN EN RED 2.2 Método InetAddress getByName(String host) throws UnknownHostException InetAddress[] getAllByName(String host) throws UnknownHostException InetAddress getLocalHost() throws UnknownHostException String getHostName() byte[] getAddress() int hashCode() boolean equals(Object x) String toString() Direcciones de Internet Explicación dirección IP del host todas las direcciones asociadas a ese host dirección IP de la máquina que ejecuta el código nombre correspondiente a la máquina local dirección IP de esta máquina como vector de bytes (mayor peso en pos 0) devuelve un código hash para esa dirección compara esta dir. con el objeto x convierte la dir. en una tira imprimible La clase InetAddress no posee constructores públicos. Para generar nuevos objetos pasamos el nombre de la máquina (o una tira con su dirección fı́sica) al método de clase getByName. try { InetAddress d1=InetAddress.getByName("mistral.dsic.upv.es"); InetAddress d2=InetAddress.getByName("128.34.5.12"); } catch (UnknowHostException e) { System.out.println(e.getMessage()); } Si la máquina posee varias direcciones utilizamos getAllByName import java.net.*; class A { public static void main (String[] arg) { try { InetAddress[] d=InetAddress.getAllByName("www.apple.com"); for (int i=0; i<d.length; i++) System.out.println(d[i]); } catch (UnknowHostException e) { System.out.println(e.getMessage()); } } } // muestra // www.applet.com/17.254.3.28 // www.applet.com/17.254.3.37 // www.applet.com/17.254.3.61 // www.applet.com/17.254.3.21 El método InetAddress.getLocalHost() devuelve un objeto InetAddress que contiene la dirección del ordenador en el que se está ejecutando el programa. 14 2 PROGRAMACIÓN EN RED 2.3 Ports try { InetAddress yo=InetAddress.getLocalHost(); } catch (UnknowHostException e) { System.out.println(e.getMessage()); } Dado un objeto InetAddress, podemos: Obtener su nombre simbólico como String (getHostName) Obtener su dirección IP como String (getHostAddress) Obtener su dirección IP como vector de bytes (getAddress) Averiguar si es o no una dirección multicast (isMulticastAddress) El siguiente programa imprime información sobre la máquina local import java.net.*; class A { public static void main (String[] arg) { try { InetAddress yo = InetAddress.getLocalHost(); System.out.println("mi nombre es "+yo.getHostName()+ " y mi direccion es "+yo.getAddress()); byte[] d = yo.getAddress(); for (int i=0; i<d.length; i++) { int unsignedByte = d[i] < 0 ? d[i]+256 : d[i]; System.out.println(unsignedByte+" "); } } catch (UnknowHostException e) { System.out.println("No conozco ni mi dirección"); } } } También podemos averiguar el nombre de la máquina dada su dirección IP 2.3. Ports En muchos casos hay que mantener abiertas varias conexiones simultáneas con máquinas distintas (ej. varias sesiones ftp, conexiones web, etc.). Con este fin el interfaz de red se divide desde un punto de vista lógico en 65536 ports distintos. El concepto de port es una abstracción (no representa ningún componente fı́sico). Cada paquete que circula por la red indica un número de port 15 2 PROGRAMACIÓN EN RED 2.4 Protocolos además de la dirección IP destino; cuando la máquina destino recibe un paquete, utiliza el número de port para determinar qué programa utilizará el contenido del mensaje (en un instante dado sólo puede haber un programa dado escuchando en cada port). Cada port puede recibir varias conexiones simultáneas (ej. un servicio web suele utilizar el port 80, y soporta varias conexiones simultáneas en dicho port). En resumen, a un port podemos asociar varias conexiones externas simultáneas, pero un sólo proceso local. Muchos servicios asumen números concretos de port (por convenio o por indicación del protocolo). Ej.- los servidores http escuchan generalmente en el port 80, los servidores SMTP en el 25, los de Echo en el 7, etc. Otros servicios no manejan ports predefinidos, sino que los descubren de forma dinámica (ej. NFS). 2.4. Protocolos Un protocolo define la forma en que se comunican dos máquinas. Ej El protocolo Daytime (RFC 867) indica que el cliente se conecta al puerto (port) 13 del servidor, tras lo cual el servidor contesta con una tira de caracteres que representa la hora actual y cierra la conexión El protocolo Time (RFC 868) indica que el cliente se conecta al port 9 del servidor, y entonces el servidor contesta con la hora actual en formato binario y cierra la conexión Existen tantos tipos distintos de protocolos como de servicios que los utilizan. Los protocolos Lockstep requieren una respuesta en cada paso, protocolos como FTP utilizan varias conexiones y permiten varias peticiones y respuestas por conexión, protocolos como HTTP sólo permiten una petición y respuesta por conexión, etc. 2.5. URLs Una URL (Uniform Resource Locator) es un mecanismo para identificar de forma no ambigua la ubicación de un recurso en Internet. Una URL puede descomponerse hasta en 5 partes, que corresponden a: protocolo .- JDK 1.1 entiende los protocolos mailto, ftp, file, http y appletresource. Además define y utiliza internamente los protocolos doc, netdoc, systemresource, verbatim máquina .- IP correspondiente a la máquina donde se localiza el servicio port .- port a utilizar fichero .16 2 PROGRAMACIÓN EN RED 2.5 URLs sección .- referencia dentro del fichero Ejemplos de URLs: http://www.javasoft.com file://Macintosh\%20HD/Java/docs/jdk%201.1/api/java.net.InetAddress.html mailto:jsendra@dsic.upv.es La clase java.net.URL representa una URL. Hay constructores para crear URLs y métodos que descomponen una URL en sus elementos, pero la utilidad más interesante es la posibilidad de obtener un stream (flujo de datos) de entrada a partir de una URL. de forma que podamos leer datos desde un servidor. El objetivo es separar la gestión de los datos descargados, y la gestión del protocolo utilizado para descargar los datos. El gestor de protocolo comunica con el servidor y lleva a cabo cualquier negociación previa con el mismo, y devuelve como resultado el fichero o secuencia de bytes solicitados. Entonces el gestor de contenido interpreta dicho fichero o secuencia de bytes y los traduce en algún tipo de objeto Java (ej. InputStream o ImageProducer). Cuando consumimos un objeto URL, Java busca un gestor adecuado para el protocolo (si no hay ninguno, lanzala excepción MalformedURLException). Podemos construir una URL a partir de una tira única o de tiras separadas para cada elemento de la URL (protocolo, máquina, port, fichero, sección). Además, muchas páginas HTML contienen URLs relativas (ej. permiten mantener un mismo documento en distintos mirrors sin invalidar las referencias internas), por lo que también existe un constructor para crear URLs relativas a una dada. // URL(String U) try { URL u = new URL("http:/www.poly.edu/schedule/fall97/bgrad.html#cs"); } catch (MalformedURLException e) {} // URL(String protocolo, String maquina, String fichero) try { URL u = new URL("http","www.poly.edu","schedule/fall97/bgrad.html#cs"); } catch (MalformedURLException e) {} // URL(String protocolo, String maquina, int port, String fichero) try { URL u = new URL("http","www.poly.edu",80,"schedule/fall97/bgrad.html#cs"); } catch (MalformedURLException e) {} // URL(String contexto, String u) try { 17 2 PROGRAMACIÓN EN RED 2.5 URLs URL u1 = new URL("http://sunsite.unc.edu/javafaq/course/week12/07.html); URL u2 = new URL(u1, "08.html"); } catch (MalformedURLException e) {} La clase URL posee cinco métodos para dividir una URL en sus componentes: getProtocol, getHost, getPort, getFile, getRef. try { URL u = new URL("http:/www.poly.edu/schedule/fall97/bgrad.html#cs"); System.out.println( "\nEl protocolo es :"+u.getProtocol()+ "\nLa maquina es :"+u.getHost()+ "\nEl port es :"+u.getPort()+ "\nEl fichero es :"+u.getFile()+ "\nLa seccion es :"+u.getRef()); } catch (MalformedURLException e) {} Si la URL no especifica un número de Port, getPort devuelve -1 (lo cual indica que se utiliza el port por defecto para ese protocolo). Si la URL no especifica un fichero, getFile devuelve /”. Sobre una URL también podemos abrir una conexión que devuelve un flujo de entrada (InputStream). Un flujo de entrada constituye una secuencia de bytes a los que podemos acceder mediante operaciones read y readBytes. Cuando se abre la conexión (operación openStream) se descartan todos los campos de cabecera y control previos a los datos. El siguiente ejemplo intenta la conexión con el servidor especificado, descarga los datos, y los escribe en pantalla. import java.net.*; import java.io.*; public class R6 { // lectura de datos desde una URL public static void main (String[] arg) { try { URL u = new URL(arg[0]); InputStream is = u.openStream(); DataInputStream dis = new DataInputStream(is); String linea; while ((linea=dis.readLine()) != null) System.out.println(linea); } catch (MalformedURLException e) {System.out.println(e.Message());} catch (IOException e) {System.out.println(e.Message());} } } 18 2 PROGRAMACIÓN EN RED 2.6. 2.6 Sockets Sockets En Internet enviamos datos entre máquinas utilizando TCP/IP. Los datos se dividen en paquetes de talla variable, pero finita (ej < 64k) denominados datagramas. Si perdemos un paquete se retransmite sólo el paquete extraviado (en lugar de reenviar todos los paquetes), y si los paquetes llegan desordenados pueden reordenarse en el receptor. Java permite manejar datagramas y Sockets. Un socket representa una comunicación fiable entre dos máquinas, ocultando al programador los detalles de codificación de paquetes, paquetes perdidos y retransmitidos, y paquetes que llegan fuera de orden. El software de red de java gestiona de forma transparente la división de los datos en paquetes (en el emisor) y su recepción y reconstrucción (en el receptor). Las operaciones básicas que soporta un socket son la conexión a una máquina remota (no puede conectarse simultáneamente a más de una máquina), envı́o de datos, recepción de datos, y cierre de conexión. La clase java.net.socket permite realizar todas las operaciones fundamentales sobre sockets: 19 2 PROGRAMACIÓN EN RED conexión 2.6 Sockets mediante la construcción de nuevos sockets. Cada socket se asocia exactamente con una máquina remota (para conectar con otra máquina hay que crear otro socket). Como mı́nimo hay que especificar la máquina y port a los que conectamos (la máquina puede especificarse mediante una tira o un objeto InetAddress, y el port debe ser un entero entre 1 y 65536). También podemos especificar la máquina y el port desde el que conectamos (un valor 0 para el port indica la elección arbitraria de uno de los ports disponibles). Todos los constructores no sólo crean el socket, sino que además intentan conectar el socket al servidor remoto (si no es posible, se activa una excepción IOException). public Socket (String host, int port) throws UnknownHostException, IOException public Socket (InetAddress dir, int port) throws UnknownHostException, IOException public Socket (String host, int port, InetAddress dirLocal, int portLocal) throws UnknownHostException, IOException public Socket (InetAddress dir, int port, InetAddress dirLocal, int portLocal) throws UnknownHostException, IOException envı́o y recepción Se realiza mediante streams (flujos) de entrada y salida. Existen métodos para obtener flujos para entrada y para salida en un socket public InputStream getInputStream () throws IOException public OutputStream getOutputStream () throws IOException cierre public synchronized void close () throws IOException fijar opciones public void setTcpNoDelay (boolean on) throws SocketException public boolean getTcpNoDelay () throws SocketException public void setSoLinger (boolean on, int val) throws SocketException public synchronized void setSoTimeout (int timeout) throws SocketException public synchronized int getSoTimeout () throws SocketException public static synchronized void setSocketImplFactory (SocketImplFactory f) throws SocketException obtención información public public public public public InetAddress getInetAddress() InetAddress getLocalAddress() int getPort() int getLocalPort() String toString() No podemos conectar con cualquier port en cualquier máquina: la máquina 20 2 PROGRAMACIÓN EN RED 2.6 Sockets remota debe estar esperando conexiones en ese port. El siguiente código permite averiguar qué ports esperan conexiones en una máquina dada: import java.net.*; import java.io.*; public class R7 { // explora ports public static void main (String[] arg) { try { busca(InetAddress.getLocalHost()); } catch (UnknownHostException e) { System.out.println("No conozco la maquina "); } } static void busca (InetAddress dir) { String host = dir.getHostName(); for (int port=0; port<20; port++) { try { Socket s = new Socket(dir,port); System.out.println("Hay un servidor escuchando en el port "+port); s.close(); } catch (IOException e) { System.out.println("nadie escucha en el port "+port); } } } } 2.6.1. Lectura/Escritura de datos desde/a un socket Una vez que hemos conectado un socket podemos enviar datos al servidor a través de un flujo de salida o leer datos mediante un flujo de entrada. El significado exacto de los datos enviados/recibidos depende del protocolo utilizado. El método getInputStream() devuelve un flujo de entrada (InputStream) que lee datos desde el socket. import java.net.*; import java.io.*; import java.util.Date; public class R8 { // lectura de datos desde un socket public static void main (String[] arg) { try { Socket s = new Socket("sunsite.unc.edu",13); InputStream is = s.getInputStream(); 21 2 PROGRAMACIÓN EN RED 2.6 Sockets DataInputStream dis = new DataInputStream(is); String hora = dis.readLine(); System.out.println(hora); s.close(); } catch (UnknownHostException ex) { System.out.println("No conozco la maquina "); System.out.println("hora local = "+(new Date()).toString()); } catch (IOException ex) { System.out.println("Error de Entrada/Salida "); System.out.println("hora local = "+(new Date()).toString()); } } } El método getOutputStream() devuelve un flujo de salida (OutputStream) que escribe datos en el socket. import java.net.*; import java.io.*; public class whois { public final static int port = 43; public final static String hostname = "whois.internic.net"; public static void main(String[] args) { Socket theSocket; DataInputStream theWhoisStream; PrintStream ps; try { theSocket = new Socket(hostname, port, true); ps = new PrintStream(theSocket.getOutputStream()); for (int i = 0; i < args.length; i++) ps.print(args[i] + " "); ps.print("\r\n"); theWhoisStream = new DataInputStream(theSocket.getInputStream()); String s; while ((s = theWhoisStream.readLine()) != null) { System.out.println(s); } } catch (IOException e) { System.err.println(e); } } 22 2 PROGRAMACIÓN EN RED 2.6 Sockets } En la mayor parte de los casos el cliente desea intercalar lecturas y escrituras. Algunos protocolos requieren una alternancia estricta entre lecturas y escrituras; otros, como HTTP, permiten varias operaciones de cada tipo (varias de lectura, varias de escritura), y otros protocolos permiten cualquier secuencias de lecturas y escrituras. Java no impone ninguna restricción (ej. una tarea puede leer de un socket mientras otra escribe en el mismo). El siguiente ejemplo envı́a una petición a un servidor http utilizando un flujo de salida sobre un socket; luego, lee la respuesta mediante un flujo de entrada (el servidor cierra la conexión cuando ha enviado la respuesta) import java.net.*; import java.io.*; public class R10 { // lectura-escritura a un socket para HTTP public static void main (String[] arg) { int port=80; for (int i=0; i<arg.length; i++) { try { URL u=new URL(arg[i]); if (u.getPort() != -1) port=u.getPort(); if (!(u.getProtocol().equalsIgnoreCase("http"))) { System.out.println("Lo siento, pero no entiendo HTTP"); continue; } Socket s = new socket(u.getHost(), u.getPort()); OutputStream os = s.getOutputStream(); PrintWriter pw = new PrintWriter(os, false); // no auto-flushing // a~ nadimos las marcas al final de linea pw.print("GET" + u.getFile() + " HTTP/1.0\r\n"); pw.print("Aceptamos: text/plain, text/html, text/*\r\n"); pw.print("\r\n"); pw.flush(); InputStream is = s.getInputStream(); DataInputStream dis = new DataInputStream(is); String linea; while ((linea = dis.readLine()) != null) { System.out.println(linea); } } catch (MalformedURLException ex) { System.out.println("No es una URL valida "); } catch (IOException ex) { System.out.println(e.getMessage()); } 23 2 PROGRAMACIÓN EN RED 2.6 Sockets } } } 24