Está en la página 1de 22

Aspectos avanzados de Java: canales y Sockets

Introduccin a los canales


La comunicacin de Java con el "exterior" se realiza a travs de canales o flujos, que no son ms que caminos que usan los programas para comunicarse con otros programas o con cualquier dispositivo de entrada o salida, y por los que se transmite la informacin. Gracias a esta manera de enviar informacin, el origen y el destino de los datos no necesitan conocerse, ni saber qu va a hacer cada uno de ellos con los datos, ni qu tipo de datos se enva, ni por qu medio se est transmitiendo la informacin. Esto es as debido a que todas las operaciones de lectura y escritura se realizan sobre el canal. As, leeramos igual de un canal que viniera de un fichero que de uno que llegara desde Internet. Esta independencia permitira que si en un programa cambiase la fuente de los datos, slo habra que especificar el nuevo canal y no hara falta cambiar la forma en que se tratan los datos. Esta independencia que nos dan los canales tiene sus lmites, pues, en algn momento, tendremos que decirle al programa dnde queremos que nos escriba los datos o de dnde los queremos leer. Es ms, hay canales para el tipo de datos que enviamos o recibimos, ya sean caracteres unicode o bytes; dentro de esa divisin hay otra dependiendo del origen o destino de los datos: memoria, fichero, ...; y finalmente hay otra clasificacin segn el tipo de operacin que realizan los canales con los datos: los que realizan alguna operacin y los que no hacen nada con los datos.

Programacin en Java

Grupo EIDOS

Como vemos, esa supuesta independencia que nos dan los canales es slo a la hora de tratarlos, ya que cuando tenemos que crearlos la cosa se complica un poco y aqu s que tenemos que saber por dnde van a ir los datos, qu tipo de datos vamos a tratar y lo que vamos a hacer con ellos. Al tratar con canales de datos (en ingls streams, que literalmente seran corrientes de datos), debemos tener en cuenta dos cosas: Cuando accedemos a un canal se bloquean los dems procesos. Para evitar que el canal bloquee la ejecucin, lo normal ser colocarlo en un hilo de ejecucin separado. Si se produce algn error mientras se opera con un canal, se lanzar una excepcin del tipo IOException, por lo tanto tenemos que tratar estas posibles excepciones introduciendo la operacin con el canal en un bloque try-catch.

Ahora que ya sabemos lo que es un canal de datos, vamos a empezar por los ms sencillos que seguro ya hemos usado en distintos ejemplos: los canales estndar. Luego continuaremos con los canales especializados del paquete java.io.

Canales estndar de entrada/salida


Los canales estndar vienen definidos en la clase System y ya los hemos usado aunque no supiramos que se consideraban canales. Los canales estndar nos comunican, por defecto, con la pantalla y el teclado. La salida o entrada de estos canales se puede redirigir, pero en el caso de que queramos hacerlo, ser mejor usar uno de los canales especializados que veremos en el paquete java.io . La clase System implementa tres canales de comunicacin: Salida estndar (System.out): es la salida por pantalla que ya hemos usado desde nuestras aplicaciones. No la usamos desde los applets ya que en ellos no escribimos directamente en la pantalla (no est permitido). Los mtodos ms habituales son print() y println().

// Muestra Hola. Deja el cursor al final del texto System.out.print("Hola"); // Muestra Adis y salta de lnea System.out.println("Adis"); Cdigo fuente 242

Errores estndar (System.err): es un canal especial cuya nica diferencia con la salida estndar es que no puede ser redirigido: los datos siempre saldrn por pantalla. Por lo dems es igual que System.out y comparte sus mtodos.

System.err.println("Esto es un error!"); Cdigo fuente 243

346

Grupo EIDOS

17. Aspectos avanzados de Java: canales y Sockets

Entrada estndar (System.in): es la entrada por teclado. El mtodo ms usado es read() con el que leeramos caracteres hasta que le especifiquemos en una condicin. El mtodo read() tiene un comportamiento que merece ser explicado.

El mtodo read() devuelve un entero. Para estudiar su funcionamiento, vamos a ver un ejemplo muy simple: un programa que lee del teclado y luego nos muestra por pantalla lo que hemos escrito.

import java.io.*; class LecturaEstandar{ public static void main(String args[]) throws IOException{ int caracter; System.out.println("Escribe lo que quieras. Para terminar, pulsa ENTER"); System.out.println(); while ( (caracter = System.in.read()) != 13 ) System.out.println(caracter); } } Cdigo fuente 244

A la vista de la ejecucin de este programa nos podemos preguntar dnde guarda los caracteres que vamos introduciendo, ya que no hemos definido ningn array de enteros y sin embargo nos muestra todos los caracteres. Lo que ocurre cuando llamamos a read() es que se crea un canal de entrada que slo se lee cuando pulsamos la tecla ENTER (en la entrada por teclado no hay fin de fichero). Una vez que lo hemos pulsado, read() leer el canal secuencialmente, comprobando la condicin para dejar de leer. Mientras lee el canal, va mostrando los caracteres por la pantalla, de uno en uno. En la Figura 152 se puede ver un ejemplo del resultado.

Figura 152

Si cambisemos la condicin para dejar de leer y le forzramos a terminar al escribir A, la condicin sera la que se muestra en el Cdigo fuente 245.
347

Programacin en Java

Grupo EIDOS

while ((caracter = System.in.read()) != 65) // Cdigo ASCII de A Cdigo fuente 245

Al ejecutarlo ahora veramos lo que se muestra en la Figura 153

Figura 153

Esta vez nos deja escribir (llenar el canal) hasta que pulsamos ENTER. Una vez pulsado, es cuando se analizan los datos que hay en el canal, y vemos que slo lee las cuatro a ya que al llegar a la A, se cumple la condicin de salida del bucle e ignora el resto de los datos que hay en el canal. Con estos tres canales podemos realizar operaciones bsicas, pero no es suficiente para manejar ficheros, leer de Internet, filtrar datos, etc.... Para estas operaciones ms especficas usaremos los canales que nos proporciona el paquete java.io.

Canales de java.io
Las clases que ofrece el paquete java.io para manejar los distintos tipos de canales se dividen en el tipo de dato que manejan, ya sean caracteres o bytes. La diferencia entre los dos tipos es que Java usa caracteres Unicode de 16 bits y los bytes tienen 8 bits. Normalmente no hay diferencia al usar uno u otro, ya que si usamos un sistema operativo que use caracteres tradicionales de 8 bits, Java, al leer un carcter, leer 8 bits y no 16. Esto lo hace casi siempre, pero en algunos casos s puede guardar un carcter de 16 bits cuando nosotros queremos guardar uno de 8. Por esto, lo ms recomendable es trabajar con canales de bytes a no ser que estemos seguros de que queremos usar caracteres Unicode. Como los canales de byte y de carcter se comportan prcticamente igual, solo que unos tratando bytes y los otros tratando caracteres, sera ms lgico clasificar los canales por la forma de tratar los datos. Atendiendo a esto, tenemos otros dos tipos de canales: Canales de transmisin (Data Sink Streams): son canales que no realizan ninguna operacin con los datos que leen o escriben en el canal. Se limitan a transmitir los datos. Canales de proceso (Processing Streams): son canales que s realizan alguna operacin con los datos antes de que los reciba el programa. Estos canales actan sobre un canal de transmisin, es decir, el canal de transmisin establece el flujo de datos y el canal de proceso trabaja sobre los datos que hay en ese canal. Algunos procesos son: filtrado, concatenacin de canales, conversin de datos, recuento de lneas, etc...

348

Grupo EIDOS

17. Aspectos avanzados de Java: canales y Sockets

Todas las clases son subclases (clases hijas) de InputStream y de OutputStream que definen un canal cualquiera de entrada y otro de salida. Gracias a estas superclases, el tratamiento de los canales es prcticamente igual, salvando las peculiaridades de cada uno en particular. Estas clases tienen unos mtodos comunes para todos los canales. Los ms importantes son read() y write() para leer y escribir, pero implementan otros que no todos los canales pueden usar satisfactoriamente. Este uso de mtodos comunes es lo que permite la independencia del canal a la hora del tratamiento: podemos usar read() en todos los canales de entrada y write() en los de salida. Luego quedan las operaciones especiales que tenga cada canal que ser por lo que le hemos elegido: no vale cualquier canal para cualquier cosa. Los mtodos read() de InputStream son: int read() int read(byte cadena[]) int read(byte cadena[], int comienzo, int longitud)

El primer mtodo lee un byte y nos devuelve su valor en forma de entero. El segundo lee una cadena de bytes hasta que no haya ms datos disponibles y la guarda en la variable cadena. Devuelve el nmero de bytes ledos. El tercero lee una cadena de bytes desde comienzo hasta que lea los bytes especificados en longitud o no tenga ms datos para leer, y los guarda en cadena. Devuelve el nmero de bytes ledos. Los mismos mtodos estn disponibles para leer caracteres. Los mtodos write() de OutputStream son similares: write(int c) write(char cadena[]) write(char cadena[], int comienzo, int longitud)

Estos mtodos no creo que necesiten explicacin. La jerarqua de clases la vemos en las siguientes imgenes. Aqu estn separadas por tipo de dato que manejan, si leen o escriben y las operaciones que realicen con los datos (canales de transmisin en gris y canales de proceso en el color de fondo). Canales que manejan caracteres (Figura 154 y Figura 155)

Figura 154

349

Programacin en Java

Grupo EIDOS

Figura 155

Canales que manejan bytes (Figura 156 y Figura 157).

Figura 156

Figura 157

Pasemos, ahora, a explicar los distintos tipos de canales.

Canales de transmisin (Data Sink Streams)


Los canales de transmisin se usan para leer o escribir cadenas de caracteres, ficheros o pipes. Los pipes son un tipo de canal especial que veremos luego. En la Tabla 24 se muestran los canales de transmisin.

350

Grupo EIDOS

17. Aspectos avanzados de Java: canales y Sockets

Origen/Destino Memoria

Canales de Carcter CharArrayReader,


CharArrayWriter

Canales de Byte ByteArrayInputStream,


ByteArrayOutputStream

StringReader,
StringWriter

StringBufferInputStream PipedInputStream,
PipedOutputStream

Pipe Fichero

PipedReader,
PipedWriter

FileReader,
FileWriter
Tabla 24

FileInputStream,
FileOutputStream

Como podemos ver, por cada canal de carcter hay uno de byte y por cada canal de lectura hay uno de escritura (con excepcin de StringBufferInputStream). Los canales que escriben o leen de la memoria se crean sobre un array o una cadena existente. Para ver como funcionan los canales que trabajan con ficheros, llamados canales de fichero, veamos el Cdigo fuente 246 que los copia.

import java.io.*; public class CopiaFichero{ public static void main(String args[]) throws IOException{ FileReader entrada = new FileReader("entrada.txt"); FileWriter salida = new FileWriter("salida.txt"); int caracter; while ((caracter=entrada.read()) != -1) salida.write(caracter); entrada.close(); salida.close(); } } Cdigo fuente 246

Este programa va a leer el fichero ENTRADA.TXT y lo va a copiar en SALIDA.TXT. ENTRADA.TXT debe existir y puede ser cualquier fichero de texto (un TXT, un BAT, etc...). La lectura del fichero de entrada finaliza cuando se ha alcanzado el final del fichero, esto se identifica mediante el valor 1. Aqu vemos que el uso de los mtodos read() y write() no depende de que el canal sea de una clase o de otra. Si leyramos un String de la memoria y lo escribiramos en el disco, slo tendramos que cambiar el canal, no el mtodo de lectura. Cambiando el Cdigo fuente 247.
351

Programacin en Java

Grupo EIDOS

FileReader entrada = new FileReader("entrada.txt"); Cdigo fuente 247

Por el Cdigo fuente 248.

String cadena="Esta cadena es un ejemplo"; StringReader entrada = new StringReader(cadena); Cdigo fuente 248

Tendremos un programa que lee de la memoria y escribe en un fichero. Y slo hemos cambiado el canal. Los canales pipe son un tipo especial algo complejo que se usa para pasar informacin de un hilo de ejecucin a otro controlando toda la sincronizacin automticamente. Para usar los pipe hay que crear uno de entrada y otro de salida y relacionarlos, como muestra el Cdigo fuente 249

PipedInputStream entrada = new PipedInputStream(); PipedOutputStream salida = new PipedOutputStream(entrada); Cdigo fuente 249

De estos canales no vamos a ver ningn ejemplo porque son bastante complejos y se sale de las expectativas del curso.

Canales de proceso (Processing Streams)


Los canales de proceso realizan alguna operacin con los datos antes de recuperar o de introducir los datos en el canal. Estos canales actan sobre un canal de transmisin que les proporciona los datos. Proceso Buffering Filtrado Conversin entre Bytes y Caracteres Unin de Canales, concatenacin Canales de Carcter BufferedReader,
BufferedWriter

Canales de Byte BufferedInputStream,


BufferedOutputStream

FilterReader,
FilterWriter

FilterInputStream,
FilterOutputStream

InputStreamReader,
OutputStreamWriter

SequenceInputStream

352

Grupo EIDOS

17. Aspectos avanzados de Java: canales y Sockets

Serializacin de Objetos Conversin de Tipos Contador de Lneas Vuelta Atrs Impresin LineNumberReader PushbackInputStream PrintWriter
Tabla 25

ObjectInputStream,
ObjectOutputStream

DataInputStream,
DataOutputStream

LineNumberInputStream

PrintStream

Estos canales, al crearse a partir de uno de transmisin, se usan sobre todo para optimizacin de accesos o para aadir funcionalidades extra. Vamos a comentar estos canales.

Canales de Filtrado
Son unas clases abstractas de las que heredan los canales de buffering, conversin de tipos, contador de lneas, vuelta atrs e impresin.

Canales de buffer
Son uno de los tipos de canales ms importantes, ya que optimizan el acceso a los datos guardndolos en memoria, limitando as los accesos a la fuente de los mismos. Son los nicos canales que utilizan adecuadamente los mtodos mark() y reset() de la superclase InputStream, el mtodo mark() marca una posicin en el canal y el mtodo reset() vuelve a ella aunque nos hayamos movido. Vamos a ver un ejemplo que usa un BufferedReader para leer el cdigo HTML de una pgina Web y un FileWriter para guardarla en disco. Por cierto, esta pgina se puede abrir en el explorador.

import java.net.*; // En este paquete est la definicin de la URL import java.io.*; public class LeerUrl{ public static void main(String args[]) throws Exception{ URL eidos = new URL("http://www.eidos.es/"); FileWriter salida = new FileWriter("salida.htm"); BufferedReader entrada = new BufferedReader (new InputStreamReader (eidos.openStream())); int caracter; while ((caracter=entrada.read()) != -1) salida.write(caracter); entrada.close(); salida.close(); } } Cdigo fuente 250

353

Programacin en Java

Grupo EIDOS

La forma de usar los canales de proceso es a partir de un canal de transmisin, pero en este caso estamos usando InputStreamReader, que hemos dicho que es un canal de proceso. Esto es porque InputStreamReader proporciona datos: es un canal como los de transmisin pero que pasa los bytes a caracteres al leerlos y convierte los caracteres en bytes al escribirlos. Es el nico canal que se comporta como uno de transmisin. Tambin se pueden anidar canales. Por ejemplo, definir un canal PrintWriter a partir de entrada, que es un canal BufferedReader creado a partir de un InputStreamReader. Con esto conseguimos la funcionalidad de los tres canales en uno. Este ejemplo funcionara igual si no usramos BufferedReader, pero el acceso al canal es mejor usndolo. Adems, los canales de buffer se van a usar mayoritariamente para optimizar las operaciones de lectura y escritura, no porque sean estrictamente necesarios. Tambin podemos apreciar que la forma de leer es siempre la misma aunque cambiemos de canal. Este programa est hecho con canales de carcter, pero se puede hacer igualmente con canales de byte: BufferdedInputStream y FileOutputStream.

Canal de Concatenacin
El canal de concatenacin une varios canales en uno slo. Como parmetros acepta dos canales de transmisin del tipo InputStream, o bien una lista de estos del tipo Enumeration, que es un interfaz especial para hacer listas de datos. Vamos a ver un ejemplo, en el Cdigo fuente 251, de concatenacin de dos canales.

import java.io.*; public class UnirFicheros{ public static void main(String args[]) throws IOException{ FileInputStream entrada1 = new FileInputStream("hola.txt"); FileInputStream entrada2 = new FileInputStream("adios.txt"); SequenceInputStream ficheros = new SequenceInputStream(entrada1, entrada2); int c; while ((c = ficheros.read()) != -1) System.out.write(c); ficheros.close(); } } Cdigo fuente 251

Este programa abre dos canales que leen de los ficheros de texto HOLA.TXT y ADIOS.TXT, y los une en un solo canal SequenceInputStream cuyo contenido se muestra en la pantalla.

Canales de conversin de tipos


Hasta ahora hemos ledo los datos como carcter o byte. Para leer o escribir datos de otros tipos tenemos los canales DataInputStream y DataOutputStream con los que podemos grabar datos de tipo boolean, short, int, long, float y double, adems de char y byte.

354

Grupo EIDOS

17. Aspectos avanzados de Java: canales y Sockets

Para comprobar su funcionamiento, tenemos este ejemplo que graba un fichero con datos de tipo int y String, separados por un tabulador. Luego leemos de ese fichero, sacando la informacin por pantalla y haciendo un total de uno de sus campos.

import java.io.*; public class PruebaTipos{ public static void main(String[] args) throws IOException{ PruebaTipos obj = new PruebaTipos(); obj.escribeDatos(); obj.leeDatos(); } public void escribeDatos() throws IOException{ DataOutputStream salida = new DataOutputStream( new FileOutputStream("datos.dat")); int[] precios = { 3000, 5000, 7000, 6500, 750 }; int[] unidades = { 12, 8, 13, 29, 50 }; String[] descripcion = { "Camisetas","Baadores","Pantalones", "Zapatillas","Calcetines" }; for (int i = 0; i < precios.length; i ++){ salida.writeInt(precios[i]); salida.writeChar('\t'); salida.writeInt(unidades[i]); salida.writeChar('\t'); salida.writeBytes(descripcion[i]); salida.writeChar('\n'); } salida.close(); } public void leeDatos() throws IOException{ DataInputStream entrada = new DataInputStream( new FileInputStream("datos.dat")); int precio; int unidad; String desc; int total = 0; try{ while (true){ precio = entrada.readInt(); entrada.readChar(); // Ignoramos el tabulador unidad = entrada.readInt(); entrada.readChar(); // Ignoramos el tabulador desc = entrada.readLine(); System.out.println("Has pedido " + unidad + " unidades de " + desc + " a " + precio + " pts."); total = total + unidad * precio; } } catch (EOFException e) {} System.out.println("Por un total de: " + total + " Pts."); entrada.close(); } } Cdigo fuente 252

Lo primero que hacemos es crearnos un canal de salida capaz de tratar tipos a partir de un canal de fichero. Una vez que definimos los datos, los grabamos en columnas (separando los elementos por tabuladores). Creamos el fichero en disco y lo leemos. Si observamos datos.dat veremos que no distinguimos las cantidades: ya no las guarda como caracteres o bytes reconocibles.

355

Programacin en Java

Grupo EIDOS

El segundo mtodo de la clase, crea un canal de entrada para leer tipos a partir de un fichero. Definimos los campos que contiene el archivo y los leemos ignorando el tabulador que los separa (lo leemos sin hacer nada con l). En el bucle de lectura encontramos algo nuevo: ya no leemos hasta encontrar el final de los datos, sino que tenemos el cdigo de lectura en un bucle infinito dentro de un bloque try-catch. Esto se hace as porque tenemos que leer varios datos diferentes. En este caso no podramos repetir el bucle de lectura que hemos estado usando siempre, ya que no podemos asignar el resultado de una lectura a un solo dato. En todos los casos hemos estado usando el Cdigo fuente 253.

while ((c = ficheros.read()) != -1) Cdigo fuente 253

Con un canal de tipos, no podemos recoger la lectura en una sola variable puesto que tenemos que leer varias (en nuestro caso el precio, la descripcin y la cantidad). La manera de poder leerlo todo es poner el proceso de lectura en un bucle infinito dentro de un bloque try-catch, del que slo se saldr cuando se alcance el final del fichero y se lance la excepcin de fin de fichero que recogemos sin hacer nada. Si compilamos este ejemplo, el compilador nos dar el siguiente mensaje: String readLine() ha sido desaprobado por el autor de java.io.DataInputStream. Este mensaje indica que no es recomendable utilizar este mtodo ya que se ha quedado obsoleto, nosotros lo hemos utilizado y dejado en nuestro cdigo por simplicidad, ya que sino tendramos que utilizar un canal ms del tipo BufferedReader. En la Figura 158 se puede observar la ejecucin de este ejemplo.

Figura 158

Serializacin de Objetos
Ya hemos visto cmo escribir o leer tipos de datos en un canal, pero entre esos tipos no estn los objetos. La serializacin de objetos consiste en poder enviar objetos a un canal y luego reconstruir el objeto y el valor de sus atributos en el otro extremo del canal. Para reconstruir un objeto se tiene que importar el paquete donde est definido en el destino. Para ver cmo funciona, tenemos el Cdigo fuente 254 que escribe un objeto de tipo Date en un canal y luego lo recupera mostrndonos los valores que tena cuando se cre.
356

Grupo EIDOS

17. Aspectos avanzados de Java: canales y Sockets

import java.io.*; import java.util.Date; public class Serializacion{ public static void main(String args[]) throws Exception{ FileOutputStream fichero = new FileOutputStream("salida.dat"); ObjectOutput salida = new ObjectOutputStream(fichero); salida.writeObject("Hoy"); salida.writeObject(new Date()); salida.flush(); salida.close(); while (System.in.read() != 13); FileInputStream fichero1 = new FileInputStream("salida.dat"); ObjectInputStream entrada = new ObjectInputStream(fichero1); String hoy = (String)entrada.readObject(); Date fecha = (Date)entrada.readObject(); System.out.println(hoy); System.out.println(fecha); entrada.close(); } } Cdigo fuente 254

En la primera parte, creamos un canal de serializacin a partir de uno de fichero. Podemos definir el tipo del canal como ObjectOutput ya que es una superclase de ObjectOutputStream, aunque tambin podramos haber puesto directamente ObjectOutputStream, como haremos con el canal de entrada. Luego escribimos dos objetos: uno de la clase String y otro de la clase Date. Cuando llamamos al constructor de Date sin parmetros, nos crea un objeto con la fecha y la hora actuales. Una vez escritos, lo guardamos en disco y cerramos el canal. Esperamos a que se pulse la tecla ENTER para apreciar el cambio de hora desde que se cre el objeto Date hasta que mostramos su valor. La segunda parte es muy parecida a la primera: creamos un canal de lectura a partir de uno de fichero. Para leer los objetos usamos el mtodo readObject() de la clase ObjectInputStream. Tenemos que definir un objeto del tipo que vamos a leer y hacer un casting a ese tipo en el readObject(). Los objetos se leen en el orden en que fueron escritos. Una vez ledos, los mostramos comprobando que la hora que vemos es la de creacin. Nosotros tambin podemos serializar nuestras propias clases slo con que estas implementen el interfaz Serializable. Afortunadamente, este interfaz est vaco y slo acta a modo de etiqueta.

public class MiClase implements Serializable Cdigo fuente 255

Solamente con esta lnea, todos los objetos de esta clase sern serializables.

Canal Contador de Lneas


En Java hay un canal especial que reconoce los nmeros de lnea: LineNumberReader. Este canal slo se debe usar en canales de carcter. Java mantiene LineNumberInputStream por compatibilidad con
357

Programacin en Java

Grupo EIDOS

versiones anteriores. LineNumberInputStream no se debe usar porque un canal de bytes puede tener cualquier tipo de dato y no podemos dar por supuesto que vaya a llevar slo caracteres. Este canal es muy sencillo de usar, como vamos a ver en el Cdigo fuente 256.

import java.io.*; public class ContarLineas{ public static void main(String args[]) throws IOException{ FileReader fichero = new FileReader("entrada.txt"); LineNumberReader entrada = new LineNumberReader(fichero); String s; while ((s=entrada.readLine())!=null){ System.out.print("Linea "+(entrada.getLineNumber())+": "); System.out.print(s); System.out.println(); } } } Cdigo fuente 256

La creacin del canal es como la de todos los de proceso. ENTRADA.TXT debe existir y es un fichero de texto. Para leer usamos el mtodo ReadLine() que tienen todos los canales de entrada. El mtodo de LineNumberReader que nos proporciona el nmero de lnea en el que estamos es getLineNumber(). Leemos hasta que la cadena que devuelve readLine() sea nula (aqu no se alcanza el final de fichero). Un ejemplo de ejecucin del Cdigo fuente 256 se puede observar en la Figura 159.

Figura 159

Canales de impresin
Estos canales slo proporcionan mtodos de impresin a otros canales. Nos permiten mostrar los datos en una gran cantidad de formatos. Son unos canales muy sencillos de usar, por lo que a menudo se ver un canal de impresin creado a partir de cualquier otro canal para facilitar su uso. Sus mtodos principales son print() y println(), que estn sobrecargados para soportar cualquier tipo de dato. Estos mtodos son los que usan System.out y System.err.

358

Grupo EIDOS

17. Aspectos avanzados de Java: canales y Sockets

Canales de vuelta atrs


Este tipo de canal implementa la posibilidad de retroceder en la lectura de un canal. Hasta ahora los canales que hemos visto se tratan con avances secuenciales, exceptuando el uso de los mtodos mark() y reset(), que slo funcionan bien en los canales con buffer. Con PushbackReader y PushbackInputStream podemos retroceder tantos caracteres o bytes como podamos meter en el buffer de PushbackReader y PushbackInputStream. Esto significa que podemos leer datos varias veces, ya que despus de retroceder, leeremos otra vez los mismos datos. Como lenguaje orientado a objetos que es Java, hemos visto cmo separa todos los elementos a la hora de tratar con datos de fuentes externas. Esto le proporciona una potencia considerable a la hora de aadir nuevas funcionalidades o nuevos dispositivos de lectura o escritura: en definitiva, nuevos canales. El uso de canales est bastante restringido en los applets, ya que desde stos no podemos acceder a ningn fichero que est en la mquina del usuario, y no podemos cargar ni ejecutar ningn programa. Por lo tanto, todos estos apartados dedicados a los canales estn pensados para aplicaciones, as mismo se refleja en los ejemplos que se adjuntan.

Aplicaciones cliente/servidor en Java


Dentro del entorno de las aplicaciones Java vamos a tratar un tema de gran inters: la arquitectura cliente/servidor. Ms concretamente se va a comentar dentro de este apartado las herramientas que ofrece Java para poder realizar aplicaciones dentro del entorno de la arquitectura cliente/servidor. Las comunicaciones en Internet se realizan a travs de un protocolo llamado TCP/IP ( Transmission Control Protocol/Internet Protocol). TCP/IP es un conjunto de protocolos de comunicaciones que permite a diferentes mquinas conectadas a Internet comunicarse entre s. El protocolo TCP/IP ofrece comunicaciones fiables mediante servicios orientados a la conexin (protocolo TCP) y no fiables a travs de servicios no orientados a la conexin (protocolo UDP, User Datagram Protocol). Un servicio orientado a la conexin significa que permite intercambiar un gran volumen de datos de una manera correcta, es decir, se asegura que los datos llegan en el orden en el que se mandaron y no existen duplicados, adems tiene mecanismos que le permiten recuperarse ante errores. Las comunicaciones en Internet utilizando el protocolo TCP/IP se realizan a travs de circuitos virtuales de datos llamados sockets. Un socket bsicamente es una "tubera" que se crea para comunicar a dos programas. Cada programa posee un extremo de la tubera. La clase Socket del paquete java.net provee una implementacin independiente de la plataforma del lado cliente de una conexin entre un programa cliente y un programa servidor a travs de un socket. El lado del servidor es implementado por la clase ServerSocket. Por lo tanto para iniciar la conexin desde el lado del cliente se debe instanciar un objeto de la clase Socket, el constructor utilizado de esta clase tiene como parmetros la direccin IP (Internet Protocol) o el nombre de la mquina a la que se quiere conectar y el nmero de puerto en el que el servidor est esperando las peticiones de los clientes. Una direccin IP es un nmero de 32 bits para identificar de forma nica una mquina conectada a Internet. Se divide en grupos de 8 bits y se identifica con su nmero en notacin decimal separado cada uno de ellos por puntos, a este formato se le denomina ttrada punteada. Java permite manejar direcciones IP a travs de la clase InetAddress, que se encuentra tambin en el paquete java.net.

359

Programacin en Java

Grupo EIDOS

Debido a que recordar direcciones IP puede ser difcil y poco manejable se suelen identificar con nombres de mquinas, as la direccin IP 206.26.48.100 se corresponde con el nombre de la mquina java.sun.com que resulta ms fcil de recordar y significativo. Un mismo nombre puede tener diferentes direcciones IP, en Internet esta correspondencia entre direcciones IP y nombres la gestionan servidores de nombres que se encargan de traducir los nombres fciles de recordar a sus direcciones de 32 bits. En un programa Java para obtener el nombre de una direccin IP se utiliza el mtodo getHostName() de la clase InetAddress. Una vez comentado el concepto de direccin IP vamos a retomar la explicacin justo en el punto en el que se haba dejado. Despus de crear el socket, a continuacin se debern obtener los canales de entrada y de salida del socket. Los canales los acabamos de comentar en apartados anteriores. Para obtener los canales de entrada y de salida del socket se utilizan los mtodos getInputStream() y getOutputStream() de la clase Socket, una vez que se tienen los canales, el de entrada lo podremos tratar, por ejemplo, como un objeto de la clase DataInputStream y el de salida como un objeto de la clase PrintStream. Cuando ya disponemos de los canales de entrada y salida del socket, ya estamos en disposicin de poder comunicarnos con el servidor, es decir, la aplicacin Java que posee el servicio que queremos utilizar y que tiene el socket del lado del servidor. De todas formas, el servidor se comentar ms adelante dentro de este mismo apartado. Si lo que queremos es recibir informacin desde el servidor a travs del socket creado, se leer del flujo de entrada del socket. Para ello se entra en un bucle del tipo mientras cuya condicin es "mientras se siga recibiendo informacin a travs del flujo de entrada del socket". El cuerpo del bucle mientras se encargar de tratar esta informacin de forma adecuada y utilizarla para la tarea que resulte necesaria. Si deseamos enviar informacin al servidor lo haremos a travs del flujo de salida del socket, escribiendo en l la informacin deseada. Todo el proceso de comunicaciones se encuentra encerrado en un bloque try{...}catch(...){...} para atrapar los errores que se produzcan. En este caso se debern atrapar excepciones de entrada/salida, es decir, IOException. Una vez que se ha terminado el proceso de comunicacin entre la aplicacin cliente y el servidor, se procede a cerrar los canales de entrada y salida del socket, y tambin el propio socket (siempre se debe hacer en este orden). El siguiente esquema que se muestra es el que suelen presentar los clientes en general: Abrir un socket. Abrir el canal de entrada y el canal de salida del socket. Leer del canal de entrada y escribir en el canal de salida, atendiendo al protocolo del servidor. Cerrar los canales. Cerrar el socket.

El tercer paso es el que ms suele variar de un cliente a otro, dependiendo del servidor, los dems suelen ser iguales.

360

Grupo EIDOS

17. Aspectos avanzados de Java: canales y Sockets

Para construir la segunda aplicacin implicada en este proceso de comunicacin, es decir, la aplicacin que realiza la funcin de servidor, se deber instanciar un objeto de la clase ServerSocket. Esta es, como ya se haba indicado anteriormente, una clase del paquete java.net que provee una implementacin independiente de la plataforma del lado servidor de una conexin cliente/servidor a travs de un socket. El constructor del socket del servidor, ServerSocket(), necesita como parmetro un nmero de puerto en el que debe ponerse a escuchar las peticiones de los clientes, es decir, mediante este nmero de puerto se identificar el servicio que se debe prestar. Si el puerto especificado est ya ocupado se lanzar una excepcin a la hora de crear el socket de servidor. Cuando se ha creado el socket de servidor a continuacin se lanza sobre este socket el mtodo accept() de la clase ServerSocket. El mtodo accept() del socket del servidor se bloquea (espera) hasta que un cliente inicia una peticin de conexin en el puerto en el que el servidor est escuchando. Cuando el mtodo accept() establece con xito una conexin con el cliente devuelve un nuevo objeto de la clase Socket al que se le asigna un nuevo puerto local, dejando libre el puerto en el que el servidor espera las peticiones de los clientes. Este socket se utiliza como parmetro para el constructor de la clase encargada de tratar al cliente. Esta clase que implementa el interfaz Runnable, posee un objeto representa un hilo de ejecucin paralelo que es el verdadero encargado de servir al cliente, es decir, es un objeto de la clase Thread. Mltiples peticiones de los clientes pueden llegar al mismo puerto. Estas peticiones de conexin se encolan en el puerto, de esta forma el servidor debe aceptar las conexiones secuencialmente. Sin embargo los clientes pueden ser servidos simultneamente a travs del uso de hilos de ejecucin. Un hilo para procesar cada una de las conexiones de los clientes. Para cada uno de los clientes que se conectan al servidor se instancia un objeto que implementa el interfaz Runnable y que inicia un nuevo hilo de ejecucin a travs de un objeto de la clase Thread. Una vez que el servidor ha instanciado un hilo de ejecucin para tratar al cliente que se acaba de conectar, vuelve a escuchar en el mismo puerto en el que estaba anteriormente esperando la llegada de conexiones de otros clientes. De esta forma mientras el objeto de la clase Thread se ocupa de servir a un cliente, el servidor puede a la misma vez seguir esperando conexiones de otros clientes, ya que se trata de hilos de ejecucin paralelos. Un esquema general que suelen tener los servidores es el siguiente: mientras (true) { aceptar una conexin. crear un hilo de ejecucin que se encargue de servir al cliente. } En el caso de que exista algn error en las aceptaciones de conexin de los clientes se saldr del bucle mientras y se cerrar el socket del servidor, y se deber notificar este error. La clase que se encarga de servir al cliente, como ya se ha mencionado anteriormente, deber implementar el interfaz Runnable, por lo tanto deber implementar el mtodo run() de este interfaz. Dentro del mtodo run() se implementa el protocolo de comunicacin que existe entre el cliente y el servidor. Dentro de este mtodo podremos obtener los canales de entrada y de salida del socket en el que est conectado el cliente, para ello utiliza los mtodos getInputStream() y getOutputStream() de la clase Socket. En este momento se inicia la comunicacin con el cliente, atendiendo al protocolo de
361

Programacin en Java

Grupo EIDOS

comunicaciones determinado se escribir en el flujo de salida o se leer del flujo de entrada del socket la informacin necesaria, ya sean caracteres o bytes. Una vez servido por completo el cliente se cerrarn el socket y se finalizar la ejecucin del hilo. Si se ha produce algn error en el mtodo run() se dar por terminada la comunicacin con el cliente, se indicar el error a las aplicaciones cliente y servidor y se dispondr a cerrar los canales de entrada y de salida y el socket, y finalmente se detendr la ejecucin del hilo. En este apartado hemos visto de forma general la arquitectura cliente/servidor dentro del lenguaje Java, se han visto unos esquemas genricos que podrn ser utilizados para cualquier aplicacin cliente/servidor. Pero para que no quede todo en mera teora que suele ser un poco aburrida y fcil de olvidar, en el siguiente apartado vamos a ver un ejemplo sencillo. Este ejemplo se puede considerar como un pequeo resumen de todo lo visto en estos dos ltimos captulos, ya que vamos a utilizar multihilos, canales de entrada/salida y sockets. Adems se utiliza un applet, una aplicacin y tambin existe tratamiento de eventos, por lo tanto podemos ir un poco ms all y considerar que es un ejemplo sencillo que resume de forma escueta todo el presente curso. Nuestro ejemplo va estar constituido por un cliente y un servidor, el cliente va a ser un applet y el servidor una aplicacin. El cliente va a realizar una peticin muy sencilla, (que se puede considerar absurda, pero para el ejemplo nos sirve) la hora y fecha del servidor. El applet va a tener una caja de texto en la que va a mostrar la hora y fecha que le devuelva la aplicacin servidor. Tambin ofrece un botn, la pulsacin de este botn implica una peticin del cliente al servidor. En al mtodo actionPerformed() se establece la conexin con el servidor y se obtiene el canal de entrada, del que se obtendr la fecha y hora. El cdigo del applet cliente es como se muestra en el Cdigo fuente 257.

import java.applet.*; import java.awt.*; import java.awt.event.*; import java.io.*; import java.net.*; public class Cliente extends Applet implements ActionListener{ private Button boton; private TextField resultado; //canal de entrada private BufferedReader entrada; //socket del cliente private Socket socket; public void init(){ resultado=new TextField(40); add(resultado); boton=new Button("Obtener Fecha"); add(boton); boton.addActionListener(this); } public void actionPerformed(ActionEvent evento){ try{ //creamos el socket y nos conectamos al servidor socket=new Socket(InetAddress.getLocalHost(),6789); //obtenemos el canal de entrada entrada=new BufferedReader( new InputStreamReader(socket.getInputStream())); resultado.setText(entrada.readLine()); //se cierran los canales y el socket

362

Grupo EIDOS

17. Aspectos avanzados de Java: canales y Sockets

entrada.close(); socket.close(); }catch(UnknownHostException ex){ resultado.setText("Error al establecer la conexin: "+ex); }catch(IOException ex){ resultado.setText("Error de E/S: "+ex); } } } Cdigo fuente 257

El cdigo es bastante sencillo y respeta el esquema que comentamos para los clientes genricos, pero en este caso no se utiliza ningn canal de salida, ya que no enviamos ningn dato a la aplicacin servidor. Para leer del canal de entrada se utiliza el mtodo readLine() de la clase BufferedReader. La aplicacin servidor consta de dos clases en dos ficheros fuente distintos. La primera clase llamada Servidor es el servidor propiamente dicho, y su funcin es la de crear un objeto ServerSocket en el puerto 6789. A este servidor se conecta el applet anteriormente comentado. El servidor estar a la escucha para saber cuando se ha conectado un cliente, y cuando se conecta se crea una instancia de la segunda clase de esta aplicacin. La clase Servidor hereda de la clase Frame para dar una representacin grfica a nuestra aplicacin. Adems en la ventana se muestra el nmero de clientes que se han servidor hasta el momento, cada vez que pulsemos el botn del applet se crear una nueva peticin. El cdigo de esta clase se muestra en el Cdigo fuente 258.

import java.awt.*; import java.awt.event.*; import java.io.*; import java.net.*; public class Servidor extends Frame{ private ServerSocket sock; private Label servidos; public static void main(String args[]){ Socket s=null; Servidor serv=new Servidor(); //el servidor se pone a escuchar en el socket //que ha creado boolean escuchando=true; int numCliente=0; while(escuchando){ //espera hasta que un cliente comienza una conexin //en el puerto especificado try { s=serv.sock.accept(); numCliente++; serv.servidos.setText("Clientes Servidos= "+numCliente); }catch (IOException e) { System.out.println("Fallo en la aceptacin de llamadas: "+ e.getMessage()); //fuerza la salida del while continue; } //se crea un objeto de la clase que va a tratar la cliente new TrataCliente(s,numCliente); } //se cierra el socket try{ serv.sock.close();

363

Programacin en Java

Grupo EIDOS

}catch(IOException err){ System.out.println("Error al cerrar el socket"); System.exit(-1); } //se cierra la ventana del servidor serv.dispose(); System.exit(0); } //constructor public Servidor(){ super(" Ejemplo Servidor"); //se crea el socket del servidor en un puerto que no est ocupado try{ sock=new ServerSocket(6789); }catch(IOException err){ System.out.println("Error:\n Problemas al crear el socket: "+err); System.exit(-1); } setSize(200,110); show(); addWindowListener(new AdaptadorVentana()); servidos=new Label("Clientes Servidos=0"); setLayout(new BorderLayout()); add("Center",servidos); System.out.println("Iniciado el servidor en el puerto: " +sock.getLocalPort()); } class AdaptadorVentana extends WindowAdapter{ public void windowClosing(WindowEvent evento){ try{ sock.close(); }catch(IOException ex){ System.out.println("Error al cerrar el socket: "+ex); } dispose(); System.out.println("Apagando el servidor..."); System.exit(0); } } } Cdigo fuente 258

La segunda clase de la aplicacin se llama TrataCliente y tiene como funcin servir al cliente que se ha conectado y realizado la peticin, es decir, es la clase que calcula la hora del servidor y se la enva por el canal de salida correspondiente al cliente que se encuentre conectado. La clase TrataCliente implementa el interfaz Runnable por lo que contiene el mtodo run(), es precisamente en este mtodo el lugar en el que se va a tratar la peticin del cliente. Por cada cliente conectado se tendr un hilo de ejecucin distinto, y este se crear a travs del atributo hilo, que pertenece a la clase Thread. En el mtodo run() se crea un canal de salida de la clase PrintWriter, a partir del canal de salida del socket establecido entre el cliente y el servidor, para enviar la fecha y hora al cliente que haya realizado la peticin. Su cdigo completo es el que vemos en el Cdigo fuente 259.

import import import import

java.io.*; java.awt.*; java.net.*; java.util.*;

364

Grupo EIDOS

17. Aspectos avanzados de Java: canales y Sockets

public class TrataCliente implements Runnable{ //socket creado para comunicarse con el cliente private Socket sock=null; //Flujo de salida del socket por el que se enva //la fecha y hora actual al cliente private PrintWriter salida; //Hilo de ejecucin private Thread hilo=null; //constructor que crea el hilo y lo ejecuta public TrataCliente(Socket s, int id){ sock=s; hilo=new Thread(this,"Cliente "+id); hilo.start(); System.out.println("Iniciado el proceso: "+hilo.getName()); } public void run(){ try{ //es el canal de salida del socket // lo que debe leer el cliente salida=new PrintWriter(sock.getOutputStream()); salida.println(new Date()); }catch(Exception e){ System.out.println("Error en la transmisin del fichero"); System.exit(-1); } finalizar(); } public void finalizar() { //se cierra el canal de salida y el socket try{ salida.close(); sock.close(); }catch(IOException err){ System.out.println("Error al cerrar el socket"); } System.out.println("Finalizado el proceso: "+hilo.getName()); System.out.println(); //se finaliza y destruye el hilo de ejecucin hilo.stop(); hilo=null; } } Cdigo fuente 259

Un ejemplo de una ejecucin de este ltimo ejemplo es el que se puede apreciar en la figura 10.

Figura 160

365

Programacin en Java

Grupo EIDOS

Para probar correctamente este ejemplo, la aplicacin servidor y el applet cliente se deben encontrar en la misma mquina, es decir, ambos se deben hallar en el mismo servidor Web.

366