Programación de comunicaciones en red

Comunicación entre aplicaciones.

En Java toda la comunicación vista en primer curso de DAM consiste en dos cosas

  • Entrada/salida por consola: con las clases System.in o System.out.

  • Lectura/escritura en ficheros: con las clases File y similares.

Se puede avanzar un paso más utilizando Java para enviar datos a través de Internet a otro programa Java remoto, que es lo que haremos en este capítulo.

Roles cliente y servidor.

Cuando se hacen programas Java que se comuniquen lo habitual es que uno o varios actúen de cliente y uno o varios actúen de servidores.

  • Servidor: espera peticiones, recibe datos de entrada y devuelve respuestas.

  • Cliente: genera peticiones, las envía a un servidor y espera respuestas.

Un factor fundamental en los servidores es que tienen que ser capaces de procesar varias peticiones a la vez: deben ser multihilo.

Su arquitectura típica es la siguiente:

while (true){
        peticion=esperarPeticion();
        hiloAsociado=new Hilo();
        hiloAsociado.atender(peticion);
}

Recordatorio de los flujos en Java

InputStreams y OutputStreams

Manejan bytes a secas. Por ejemplo, si queremos leer un fichero byte a byte usaremos FileInputStream y si queremos escribir usaremos FileOutputStream.

Son operaciones a muy bajo nivel que usaremos muy pocas veces (por ejemplo, solo si quisiéramos cambiar el primer byte de un archivo). En general usaremos otras clases más cómodas de usar.

Readers y Writers

En lugar de manejar bytes manejan caracteres (recordemos que hoy en día y con Unicode una letra como la ñ en realidad podría ocupar más de un byte).

Así, cuando queramos leer letras de un archivo usaremos clases como FileReader y FileWriter.

Las clases Readers y Writers en realidad se apoyan sobre las InputStreams y OutputStreams.

A veces nos interesará mezclar conceptos y por ejemplo poder tener una clase que use caracteres cuando a lo mejor Java nos ha dado una clase que usa bytes. Así, por ejemplo InputStreamReader puede coger un objeto que lea bytes y nos devolverá caracteres. De la misma forma OutputStreamWriter coge letras y devuelve los bytes que la componen.

BufferedReaders y PrintWriters

Cuando trabajamos con caracteres (que recordemos pueden tener varios bytes) normalmente no trabajamos de uno en uno. Es más frecuente usar líneas que se leen y escriben de una sola vez. Así por ejemplo, la clase PrintWriter tiene un método print(ln) que puede imprimir elementos complejos como floats o cadenas largas.

Además, Java ofrece clases que gestionan automáticamente los buffers por nosotros lo que nos da más comodidad y eficiencia. Por ello es muy habitual hacer cosas como esta:

lectorEficiente = new
        BufferedReader(new FileReader("fich1.txt"));
escritorEficiente = new
        BufferedWriter(new FileWriter("fich2.txt"));

En el primer caso creamos un objeto FileReader que es capaz de leer caracteres de fich1.txt. Como esto nos parece poco práctico creamos otro objeto a partir del primero de tipo BufferedReader que nos permitirá leer bloques enteros de texto.

De hecho, si se comprueba la ayuda de la clase FileReader se verá que solo hay un método read que devuelve un int, es decir el siguiente carácter disponible, lo que hace que el método sea muy incómodo. Sin embargo BufferedReader nos resuelve esta incomodidad permitiéndonos trabajar con líneas.

Elementos de programación de aplicaciones en red. Librerías.

En Java toda la infraestructura de clases para trabajar con redes está en el paquete java.net.

En muchos casos nuestros programas empezarán con la sentencia import java.net.* pero muchos entornos (como Eclipse) son capaces de importar automáticamente las clases necesarias.

La clase URL

La clase URL permite gestionar accesos a URLs del tipo http://marca.com/fichero.html y descargar cosas con bastante sencillez.

Al crear un objeto URL se debe capturar la excepción MalformedURLException que sucede cuando hay algún error en la URL, como por ejemplo escribir htp://marca.com en lugar de http://marca.com (obsérvese que el primero tiene un sola t en http en lugar de dos).

La clase URL nos ofrece un método openStream que nos devuelve un flujo básico de bytes. Podemos crear objetos más sofisticados para leer bloques como muestra el programa siguiente:

public class GestorDescargas {
        public void descargarArchivo(
                        String url_descargar,
                        String nombreArchivo){
                System.out.println("Descargando "
                                +url_descargar);
                try {
                        URL laUrl=new URL(url_descargar);
                        InputStream is=laUrl.openStream();
                        InputStreamReader reader=
                                        new InputStreamReader(is);
                        BufferedReader bReader=
                                        new BufferedReader(reader);
                        FileWriter escritorFichero=
                                new FileWriter(nombreArchivo);
                        String linea;
                        while ((linea=bReader.readLine())!=null){
                                escritorFichero.write(linea);
                        }
                        escritorFichero.close();
                        bReader.close();
                        reader.close();
                        is.close();
                } catch (MalformedURLException e) {
                        System.out.println("URL mal escrita!");
                        return ;
                } catch (IOException e) {
                        System.out.println(
                                "Fallo en la lectura del fichero");
                        return ;
                }
        }
        public static void main (String[] argumentos){
                GestorDescargas gd=new GestorDescargas();
                String base=
                        "http://10.13.0.20:8000"+
                                        "/ServiciosProcesos/textos/";
                for (int i=1; i<=5; i++){
                        String url=base+"tema"+i+".rst";
                        gd.descargarArchivo(url);
                }
        }
}

Repaso de redes

En redes el protocolo IP es el responsable de dos cuestiones fundamentales:

  • Establecer un sistema de direcciones universal (direcciones IP)

  • Establecer los mecanismos de enrutado.

Como programadores el segundo no nos interesa, pero el primero será absolutamente fundamental para contactar con programas que estén en una ubicación remota.

Una ubicación remota siempre tendrá una dirección IP pero solo a veces tendrá un nombre DNS. Para nosotros no habrá diferencia ya que si es necesario el sistema operativo traducirá de nombre DNS a IP.

Otro elemento necesario en la comunicación en redes es el uso de un puerto de un cierto protocolo:

  • TCP: ofrece fiabilidad a los programas.

  • UDP: ofrece velocidad sacrificando la fiabilidad.

A partir de ahora cuando usemos un número de puerto habrá que comprobar si ese número ya está usado.

Por ejemplo, es mala idea que nuestros servidores usen el puerto 80 TCP para aceptar peticiones, probablemente ya esté en uso. Antes de usar un puerto en una aplicación comercial deberíamos consultar la lista de «IANA assigned ports».

En líneas generales se pueden usar los puertos desde 1024 TCP a 49151 TCP, pero deberíamos comprobar que el número que elegimos no sea un número usado por un puerto de alguna aplicación que haya en la empresa.

En las prácticas de clase usaremos el 9876 TCP. Si se desea conectar desde el instituto con algún programa ejecutado en casa se deberá «abrir el puerto 9876 TCP». Abrir un puerto consiste en configurar el router para que SÍ ACEPTE TRÁFICO INICIADO DESDE EL EXTERIOR cosa que no hace nunca por motivos de protección.

Funciones y objetos de las librerías.

La clase URL proporciona un mecanismo muy sencillo pero por desgracia completamente atado al protocolo de las URL.

Java ofrece otros objetos que permiten tener un mayor control sobre lo que se envía o recibe a través de la red. Por desgracia esto implica que en muchos casos tendremos solo flujos de bajo nivel (streams).

En concreto Java ofrece dos elementos fundamentales para crear programas que usen redes

  • Sockets

  • ServerSockets

Sockets.

Un socket es un objeto Java que nos permite contactar con un programa o servidor remoto. Dicho objeto nos proporcionará flujos de entrada y/o salida y podremos comunicarnos con dicho programa.

Existe otro tipo de sockets, los ServerSocket. Se utilizan para crear programas que acepten conexiones o peticiones.

Todos los objetos mencionados en este tema están en el paquete java.net.

Creación de sockets.

En el siguiente código puede verse el proceso básico de creación de un socket. En los párrafos siguientes explicaremos el significado de los bloques de código.:

public class Conector {
        public static void main(String[] args) {
                String destino="www.google.com";
                int puertoDestino=80;
                Socket socket=new Socket();
                InetSocketAddress direccion=new InetSocketAddress(
                                destino, puertoDestino);
                try {
                        socket.connect(direccion);
                        //Si llegamos aquí es que la conexión
                        //sí se hizo.

                        InputStream is=socket.getInputStream();
                        OutputStream os=socket.getOutputStream();


                } catch (IOException e) {
                        System.out.println(
                                "No se pudo establecer la conexion "+
                                " o hubo un fallo al leer datos."
                        );
                }
        }
}

Para poder crear un socket primero necesitamos una dirección con la que contactar. Toda dirección está formada por dirección IP (o DNS) y un puerto. En nuestro caso intentaremos contactar con www.google.com:80.

String destino="www.google.com";
int puertoDestino=80;
Socket socket=new Socket();
InetSocketAddress direccion=new
        InetSocketAddress(
                        destino, puertoDestino);

Enlazado y establecimiento de conexiones.

El paso crítico para iniciar la comunicación es llamar al método connect. Este método puede disparar una excepción del tipo IOException que puede significar dos cosas:

  • La conexión no se pudo establecer.

  • Aunque la conexión se estableció no fue posible leer o escribir datos.

Así, la conexión debería realizarse así:

try {
        socket.connect(direccion);
        //Si llegamos aquí es que la conexión
        //sí se hizo.

        InputStream is=socket.getInputStream();
        OutputStream os=socket.getOutputStream();

}  //Fin del try
catch (IOException e) {
        System.out.println(
                "No se pudo establecer la conexion "+
                " o hubo un fallo al leer datos."
        );
} //Fin del catch IOException

Utilización de sockets para la transmisión y recepción de información.

La clase Socket tiene dos métodos llamados getInputStream y getOutputSream que nos permiten obtener flujos orientados a bytes. Recordemos que es posible crear nuestros propios flujos, con más métodos que ofrecen más comodidad.

El ejemplo completo

Podemos contactar con un programa cualquiera escrito en cualquier lenguaje y enviar las peticiones de acuerdo a un protocolo. Nuestro programa podrá leer las respuestas independientemente de como fuera el servidor.

public class Conector {
        public static void main(String[] args) {
                System.out.println("Iniciando...");
                String destino="10.8.0.253";
                int puertoDestino=80;
                Socket socket=new Socket();
                InetSocketAddress direccion=new InetSocketAddress(
                                destino, puertoDestino);
                try {
                        socket.connect(direccion);
                        //Si llegamos aquí es que la conexión
                        //sí se hizo.

                        InputStream is=socket.getInputStream();
                        OutputStream os=socket.getOutputStream();

                        //Flujos que manejan caracteres
                        InputStreamReader isr=
                                        new InputStreamReader(is);
                        OutputStreamWriter osw=
                                        new OutputStreamWriter(os);

                        //Flujos de líneas
                        BufferedReader bReader=
                                        new BufferedReader(isr);
                        PrintWriter pWriter=
                                        new PrintWriter(osw);


                        pWriter.println("GET /index.html");
                        pWriter.flush();
                        String linea;
                        FileWriter escritorArchivo=
                                        new FileWriter("resultado.txt");
                        while ((linea=bReader.readLine()) != null ){
                                escritorArchivo.write(linea);
                        }
                        escritorArchivo.close();
                        pWriter.close();
                        bReader.close();
                        isr.close();
                        osw.close();
                        is.close();
                        os.close();
                } catch (IOException e) {
                        System.out.println(
                                "No se pudo establecer la conexion "+
                                " o hubo un fallo al leer datos."
                        );
                } //Fin del catch
        } //Fin del main
} //Fin de la clase Conector

Programación de aplicaciones cliente y servidor.

Al crear aplicaciones cliente y servidor puede ocurrir que tengamos que implementar varias operaciones:

  • Si tenemos que programar el servidor deberemos definir un protocolo de acceso a ese servidor.

  • Si tenemos que programar solo el cliente necesitaremos conocer el protocolo de acceso a ese servidor.

  • Si tenemos que programar los dos tendremos que empezar por definir el protocolo de comunicación entre ambos.

En el ejemplo siguiente puede verse un ejemplo para Python 3 que implementa un servidor de cálculo. El servidor tiene un protocolo muy rígido (demasiado) que consiste en lo siguiente:

  1. El servidor espera que primero envíen la operación que puede ser + o -. La operación debe terminar con un fin de línea UNIX (\n)

  2. Despues acepta un número de dos cifras (ni una ni tres) terminado en un fín de línea UNIX.

  3. Despues acepta un segundo número de dos cifras terminado en un fin de línea UNIX.

import socketserver
TAM_MAXIMO_PARAMETROS=64
PUERTO=9876
class GestorConexion(
        socketserver.BaseRequestHandler):

        def leer_cadena(self, LONGITUD):
                cadena=self.request.recv(LONGITUD)
                return cadena.strip()

        def convertir_a_cadena(self, bytes):
                return bytes.decode("utf-8")

        def calcular_resultado(
                self, n1, op, n2):
                n1=int(n1)
                n2=int(n2)

                op=self.convertir_a_cadena(op)
                if (op=="+"):
                        return n1+n2
                if (op=="-"):
                        return n1-n2
                return 0
        """Controlador de evento 'NuevaConexion"""
        def handle(self):
                direccion=self.client_address[0]
                operacion   =   self.leer_cadena(2)
                num1        =   self.leer_cadena(3)
                num2        =   self.leer_cadena(3)
                print (direccion+" pregunta:"+str(num1)+" "+str(operacion)+" "+str(num2))

                resultado=self.calcular_resultado(num1, operacion, num2)
                print ("Devolviendo a " + direccion+" el resultado "+str(resultado))
                bytes_resultado=bytearray(str(resultado), "utf-8");
                self.request.send(bytes_resultado)
servidor=socketserver.TCPServer(("10.13.0.20", 9876), GestorConexion)
print ("Servidor en marcha.")
servidor.serve_forever()

La comunicación Java con el servidor sería algo así:

byte[] bSuma="+\n".getBytes();
byte[] bOp1="42\n".getBytes();
byte[] bOp2="34\n".getBytes();

os.write(bSuma);
os.write(bOp1);
os.write(bOp2);
os.flush();

InputStreamReader isr=
        new InputStreamReader(is);
BufferedReader br=
        new BufferedReader(isr);
String cadenaRecibida=br.readLine();
System.out.println("Recibido:"+
                cadenaRecibida);

is.close();
os.close();
socket.close();

Ejemplo de servidor Java

Supongamos que se nos pide crear un servidor de operaciones de cálculo que sea menos estricto que el anterior:

  • Cualquier parámetro que envíe el usuario debe ir terminado en un fin de línea UNIX (\n).

  • El usuario enviará primero un símbolo «+», «-«, «*» o «/».

  • Despues se puede enviar un positivo de 1 a 8 cifras. El usuario podría equivocarse y enviar en vez de «3762» algo como «37a62». En ese caso se asume que el parámetro es 0.

  • Despues se envía un segundo positivo de 1 a 8 cifras igual que el anterior.

  • Cuando el servidor haya recogido todos los parámetros contestará al cliente con un positivo de 1 a 16 cifras.

Antes de empezar crear el código que permita procesar estos parámetros complejos.

public class ServidorCalculo {
        public int extraerNumero(String linea){
                /* 1. Comprobar si es un número
                 * 2. Ver si el número es correcto (32a75)
                 * 3. Ver si tiene de 1 a 8 cifras
                 */
                int numero;
                try{
                        numero=Integer.parseInt(linea);
                }
                catch (NumberFormatException e){
                        numero=0;
                }
                /* Si el número es mayor de 100 millones no
                 * es válido tampoco
                 */
                if (numero>=100000000){
                        numero=0;
                }
                return numero;

        }
        public void escuchar(){
                System.out.println("Arrancado el servidor");

                while (true){

                }
        }
}

Así, el código completo del servidor sería:

public class ServidorCalculo {
        public int extraerNumero(String linea){
                /* 1. Comprobar si es un número
                 * 2. Ver si el número es correcto (32a75)
                 * 3. Ver si tiene de 1 a 8 cifras
                 */
                int numero;
                try{
                        numero=Integer.parseInt(linea);
                }
                catch (NumberFormatException e){
                        numero=0;
                }
                /* Si el número es mayor de 100 millones no
                 * es válido tampoco
                 */
                if (numero>=100000000){
                        numero=0;
                }
                return numero;
        }

        public int calcular(String op, String n1, String n2){
                int resultado=0;
                char simbolo=op.charAt(0);
                int num1=this.extraerNumero(n1);
                int num2=this.extraerNumero(n2);
                if (simbolo=='+'){
                        resultado=num1+num2;
                }
                return resultado;
        }

        public void escuchar() throws IOException{
                System.out.println("Arrancado el servidor");
                ServerSocket socketEscucha=null;
                try {
                        socketEscucha=new ServerSocket(9876);
                } catch (IOException e) {
                        System.out.println(
                                        "No se pudo poner un socket "+
                                        "a escuchar en TCP 9876");
                        return;
                }
                while (true){
                        Socket conexion=socketEscucha.accept();
                        System.out.println("Conexion recibida!");
                        InputStream is=conexion.getInputStream();
                        InputStreamReader isr=
                                        new InputStreamReader(is);
                        BufferedReader bf=
                                        new BufferedReader(isr);
                        String linea=bf.readLine();
                        String num1=bf.readLine();
                        String num2=bf.readLine();
                        /* Calculamos el resultado*/
                        Integer result=this.calcular(linea, num1, num2);
                        OutputStream os=conexion.getOutputStream();
                        PrintWriter pw=new PrintWriter(os);
                        pw.write(result.toString()+"\n");
                        pw.flush();
                }
        }
}

Y el cliente sería:

public class ClienteCalculo {
        public static BufferedReader getFlujo(InputStream is){
                InputStreamReader isr=
                                new InputStreamReader(is);
                BufferedReader bfr=
                                new BufferedReader(isr);
                return bfr;
        }
        /**
         * @param args
         * @throws IOException
         */
        public static void main(String[] args) throws IOException {
                InetSocketAddress direccion=new
                                InetSocketAddress("10.13.0.20", 9876);
                Socket socket=new Socket();
                socket.connect(direccion);
                BufferedReader bfr=
                                ClienteCalculo.getFlujo(
                                                socket.getInputStream());
                PrintWriter pw=new
                                PrintWriter(socket.getOutputStream());
                pw.print("+\n");
                pw.print("42\n");
                pw.print("84\n");
                pw.flush();
                String resultado=bfr.readLine();
                System.out.println
                        ("El resultado fue:"+resultado);
        }
}

Utilización de hilos en la programación de aplicaciones en red.

En el caso de aplicaciones que necesiten aceptar varias conexiones habrá que mover todo el código de gestión de peticiones a una clase que implemente Runnable

Ahora el servidor será así:

while (true){
        Socket conexion=socketEscucha.accept();
        System.out.println("Conexion recibida");
        Peticion p=new Peticion(conexion);
        Thread hilo=new Thread(p);
        hilo.start();
}

Pero ahora tendremos una clase Petición como esta:

public class Peticion implements Runnable{
        BufferedReader bfr;
        PrintWriter pw;
        Socket socket;
        public Peticion(Socket socket){
                this.socket=socket;
        }
        public int extraerNumero(String linea){
                /* 1. Comprobar si es un número
                 * 2. Ver si el número es correcto (32a75)
                 * 3. Ver si tiene de 1 a 8 cifras
                 */
                int numero;
                try{
                        numero=Integer.parseInt(linea);
                }
                catch (NumberFormatException e){
                        numero=0;
                }
                /* Si el número es mayor de 100 millones no
                 * es válido tampoco
                 */
                if (numero>=100000000){
                        numero=0;
                }
                return numero;

        }

        public int calcular(String op, String n1, String n2){
                int resultado=0;
                char simbolo=op.charAt(0);
                int num1=this.extraerNumero(n1);
                int num2=this.extraerNumero(n2);
                if (simbolo=='+'){
                        resultado=num1+num2;
                }
                return resultado;
        }
        public void run(){
                try {
                        InputStream is=socket.getInputStream();
                        InputStreamReader isr=
                                        new InputStreamReader(is);
                        bfr=new BufferedReader(isr);
                        OutputStream os=socket.getOutputStream();
                        pw=new PrintWriter(os);
                        String linea;
                        while (true){
                                linea = bfr.readLine();
                                String num1=bfr.readLine();
                                String num2=bfr.readLine();
                                /* Calculamos el resultado*/
                                Integer result=this.calcular(linea, num1, num2);
                                System.out.println("El servidor dio resultado:"+result);
                                pw.write(result.toString()+"\n");
                                pw.flush();
                        }
                } catch (IOException e) {
                }
        }
}

Ejercicio: servicio de ordenación

Crear una arquitectura cliente/servidor que permita a un cliente, enviar dos cadenas a un servidor para saber cual de ellas va antes que otra:

  • Un cliente puede enviar las cadenas «hola», «mundo». El servidor comprobará que en el diccionario la primera va antes que la segunda, por lo cual contestará «hola», «mundo».

  • Si el cliente enviase «mundo», «hola» el servidor debe devolver la respuesta «hola», «mundo».

Debido a posibles mejoras futuras, se espera que el servidor sea capaz de saber qué versión del protocolo se maneja. Esto es debido a que en el futuro se espera lanzar una versión 2 del protocolo en la que se puedan enviar varias cadenas seguidas.

Crear el protocolo, el código Java del cliente y el código Java del servidor con capacidad para procesar muchas peticiones a la vez (multihilo).

Se debe aceptar que un cliente que ya tenga un socket abierto envíe todas las parejas de cadenas que desee.

Una clase Protocolo

Dado que los protocolos pueden ser variables puede ser útil encapsular el comportamiento del protocolo en una pequeña clase separada:

public class Protocolo {
        private final String terminador="\n";
        public String getMensajeVersion(int version){
                Integer i=version;
                return i.toString()+terminador;
        }
        public int getNumVersion(String mensaje){
                Integer num=Integer.parseInt(mensaje);
                return num;
        }
        public String getMensaje(String cadena){
                return cadena+terminador;
        }
}

Una clase con funciones de utilidad

Algunas operaciones son muy sencillas, pero muy engorrosas. Alargan el código innecesariamente y lo hacen más difícil de entender. Si además se realizan a menudo puede ser interesante empaquetar toda la funcionalidad en una clase.

public class Utilidades {
        /* Obtiene un flujo de escritura
        a partir de un socket*/
        public PrintWriter getFlujoEscritura
                (Socket s) throws IOException{
                OutputStream os=s.getOutputStream();
                PrintWriter pw=new PrintWriter(os);
                return pw;
        }
        /* Obtiene un flujo de lectura
        a partir de un socket*/
        public BufferedReader
                getFlujoLectura(Socket s)
                                throws IOException{
                InputStream is=s.getInputStream();
                InputStreamReader isr=
                                new InputStreamReader(is);
                BufferedReader bfr=new BufferedReader(isr);
                return bfr;
        }
}

La clase Petición

public class Peticion implements Runnable {
        Socket conexionParaAtender;

        public Peticion ( Socket s ){
                this.conexionParaAtender=s;
        }
        @Override
        public void run() {
                try{
                        PrintWriter flujoEscritura=
                                Utilidades.getFlujoEscritura(
                                                this.conexionParaAtender
                                                );
                        BufferedReader flujoLectura=
                                Utilidades.getFlujoLectura(
                                                conexionParaAtender);
                        String protocolo=
                                        flujoLectura.readLine();
                        int numVersion=
                                        Protocolo.getNumVersion(protocolo);
                        if (numVersion==1){
                                String linea1=
                                                flujoLectura.readLine();
                                String linea2=
                                                flujoLectura.readLine();
                                //Linea 1 va despues en el
                                if (linea1.compareTo(linea2)>0){
                                         dicc
                                        flujoEscritura.println(linea2);
                                        flujoEscritura.println(linea1);
                                        flujoEscritura.flush();
                                } else {
                                        flujoEscritura.println(linea1);
                                        flujoEscritura.println(linea2);
                                        flujoEscritura.flush();
                                }
                        }
                }
                catch (IOException e){
                        System.out.println(
                                        "No se pudo crear algún flujo");
                        return ;
                }
        }
}

La clase Servidor

public class ServidorOrdenacion {
        public void escuchar() throws IOException{
                ServerSocket socket;
                try{
                        socket=new ServerSocket(9876);
                }
                catch(Exception e){
                        System.out.println("No se pudo arrancar");
                        return ;
                }
                while (true){
                        System.out.println("Servidor esperando");
                        Socket conexionCliente=
                                        socket.accept();
                        System.out.println("Alguien conectó");
                        Peticion p=
                                        new Peticion(conexionCliente);
                        Thread hiloAsociado=
                                        new Thread(p);
                        hiloAsociado.start();
                }
        } // Fin del método escuchar
        public static void  main(String[] argumentos){
                ServidorOrdenacion s=
                                new ServidorOrdenacion();
                try {
                        s.escuchar();
                } catch (Exception e){
                        System.out.println("No se pudo arrancar");
                        System.out.println(" el cliente o el serv");
                }
        }
}

La clase Cliente

public class Cliente {
        public void ordenar(String s1, String s2) throws IOException{
                InetSocketAddress direccion=
                                new InetSocketAddress("10.13.0.20", 9876);
                Socket conexion=
                                new Socket();
                conexion.connect(direccion);
                System.out.println("Conexion establecida");
                /* Ahora hay que crear flujos de salida, enviar
                 * cadenas por allí y esperar los resultados.
                 */
                try{

                        BufferedReader flujoLectura=
                                Utilidades.getFlujoLectura(conexion);
                        PrintWriter flujoEscritura=
                                Utilidades.getFlujoEscritura(conexion);

                        flujoEscritura.println("1");
                        flujoEscritura.println(s1);
                        flujoEscritura.println(s2);
                        flujoEscritura.flush();
                        String linea1=flujoLectura.readLine();
                        String linea2=flujoLectura.readLine();
                        System.out.println("El servidor devolvió "+
                                        linea1 + " y "+linea2);
                } catch (IOException e){

                }
        }
        public static void main(String[] args) {
                Cliente c=new Cliente();
                try {
                        c.ordenar("aaa", "bbb");
                } catch (IOException e) {
                        System.out.println("Fallo la conexion o ");
                        System.out.println("los flujos");
                } //Fin del catch
        } //Fin del main
} //Fin de la clase

Ampliación

Finalmente la empresa va a necesitar una versión mejorada del servidor que permita a otros cliente enviar un número de palabras y luego las palabras. Se desea hacer todo sin romper la compatibilidad con los clientes viejos. Mostrar el código Java del servidor y del cliente.

En el servidor se añade este código extra a la hora de comprobar el protocolo:

if (numVersion==2){
        System.out.println("Llegó un v2");
        String lineaCantidadPalabras=
        flujoLectura.readLine();
        int numPalabras=
                Integer.parseInt
                (lineaCantidadPalabras);
        String[] palabras=
                                new String[numPalabras];
        for (int i=0;i<numPalabras;i++){
                palabras[i]=
                        flujoLectura.readLine();
        }
        palabras=this.ordenar(palabras);
        for (int i=0; i<palabras.length; i++){
                flujoEscritura.println(palabras[i]);
        }
        flujoEscritura.flush();
}

Y finalmente solo habría que implementar un método en la petición que reciba un vector de String (las palabras) y devuelva el mismo vector pero ordenado.

Ejercicios

Sumas de verificación

Crear un servidor multihilo que permita a los clientes calcular «sumas de verificación» compuesta por la suma de los valores ASCII de los carácteres.

Así, la suma de verificación de la cadena «ABC» es 65+66+67=198.

El protocolo será el siguiente:

  • El servidor esperá recibir una línea con solo un número positivo. Dicho número es la cantidad de líneas que nos va a enviar el cliente. Supongamos que envía 3.

  • Despues se deben recibir 3 líneas. En cada línea hay una sola palabra, de la cual el servidor calculará las sumas.

  • Despues el servidor contesta al cliente, y enviará 3 líneas separadas. En cada línea estará la suma de verificación. Las sumas se envían en el mismo orden que se recibieron.

A modo de ejemplo si el cliente envió estas líneas

  • 2

  • ABC

  • ZZ

Entonces el servidor luego enviará

  • 198 (que es la suma para ABC)

  • 180 (que es la suma para ZZ)

Solución a las sumas de verificación

El código siguiente ilustra la clase que hace sumas de verificación.

public class Sumador {

    public static int sumaSimple(String cad) {
        int suma = 0;
        for (int i = 0; i < cad.length(); i++) {
            suma += cad.codePointAt(i);
        }
        return suma;
    }
}

A continuación se muestra el servidor.

public class Servidor {

    public void servir() {
        ServerSocket serverSocket;
        final int PUERTO = 9876;
        try {
            serverSocket = new ServerSocket(PUERTO);
            while (true) {
                Socket conexion;
                conexion = serverSocket.accept();
                HiloConexion hiloConexion;
                hiloConexion = new HiloConexion(conexion);
                Thread hilo = new Thread(hiloConexion);
                hilo.start();
            }
        } catch (IOException e) {
            //No se pudo crear el server
            //socket porque no tenemos permisos
            //Se pudo crear pero no fuimos
            //capaces de enviar o recibir nada
            //Todo funcionaba pero el usuario
            //interrumpió
        }
    }

    public static void main(String[] args) {
        Servidor servidor;
        servidor = new Servidor();
        servidor.servir();
    }
}

Y a continuación el cliente:

public class Cliente {

    public void verificarCadenas(BufferedReader bfr,
                                 PrintWriter pw) throws IOException {
        pw.println(2);
        pw.println("ABC");
        pw.println("ZZ");
        pw.flush();
        String suma1 = bfr.readLine();
        String suma2 = bfr.readLine();
        System.out.println(suma1);
        System.out.println(suma2);
    }

    public static void main(String[] args) {
        Cliente cliente;
        cliente = new Cliente();
        InetSocketAddress direccion;
        direccion = new InetSocketAddress("10.13.0.100",
                                          9876);
        Socket conexion;
        conexion = new Socket();
        try {
            conexion.connect(direccion);
            BufferedReader bfr;
            bfr = Utilidades.getFlujoLectura(conexion);
            PrintWriter pw;
            pw = Utilidades.getFlujoEscritura(conexion);
            cliente.verificarCadenas(bfr, pw);
            pw.close();
            bfr.close();
            conexion.close();
        } catch (IOException e) {
            //Quiza el servidor no está encendido
            //Quizá lo esté pero su cortafuegos
            //impide conexiones
            //...
        }
    }
}

Servidor de bases de datos

Se desea crear un pequeño servidor de datos en el cual los cliente puedan almacenar información de empleados. La unica información que se almacena es su código y su nombre (tampoco hay que preocuparse de qué hacer si se repite un código)

Un cliente puede enviar al servidor varias cosas:

  • Puede enviar una linea con el texto «CREAR». En ese caso el cliente debe enviar despues dos líneas. La primera línea contiene el codigo y la segunda el nombre.

  • Un cliente puede enviar la cadena «SELECT». En ese caso el servidor contesta con una línea numérica que indica cuantos empleados hay. Si contesta por ejemplo 2 significa que va a enviar 2 parejas de líneas (codigo, nombre). Si contesta 3 significa que va a enviar 6 líneas indicando los codigos y los nombres de los empleados.

Servidor de eco

Se desea crear una aplicación cliente/servidor que consista de las siguientes partes:

  • Por un lado habrá un servidor multihilo que implementará la capacidad de «hacer el eco» de las cadenas que reciba. Así, el servidor escuchará una cadena y cuando la reciba devolverá exactamente la misma cadena al cliente. Se da por sentado que se recibirá una sola línea y que el servidor siempre tiene memoria suficiente para leer dicha línea.

  • Por otro lado, se desea tener un cliente para hacer pruebas conectándose a dicha servidor. Un cliente simplemente se conecta al servidor, le envía una cadena y espera recibir la misma cadena.

  • En último lugar se desea tener una clase que compruebe la capacidad del servidor. Esta clase empezará lanzando muchos clientes a la vez y comprobará si ha habido alguna excepción. Si no la hay es que el servidor es capaz de manejar dicha cantidad de clientes y se incrementará la cantidad total de clientes simultáneos. El cliente irá incrementando la cantidad de clientes hasta lograr una excepción momento en el cual mostrará por pantalla la cantidad total de clientes que pudo manejar el servidor sin generar ninguna excepción.

Comentarios generales a la solución

Cuando probamos el servidor y el cliente en la misma máquina es muy difícil saber si los fallos se produjeron por culpa del servidor o del cliente, ya que ambos comparten una máquina con una misma RAM.

Por otro lado, incluso probando en máquinas distintas pueden obtenerse fallos por ejemplo al lanzar 3000 hilos y otro día al lanzar 4000 hilos. Recordemos que cuando lanzamos nuestro servidor puede haber otros procesos activos que consuman RAM y tiempo de CPU por lo que averiguar un valor real es absolutamente imposible

Solución al servidor

public class Servidor {

    public void servir() {
        System.out.println("Servidor activo!");
        ServerSocket serverSocket;
        final int PUERTO = 9876;
        try {
            serverSocket = new ServerSocket(PUERTO);
            while (true) {
                Socket conexion;
                conexion = serverSocket.accept();
                HiloConexion hiloConexion;
                hiloConexion = new HiloConexion(conexion);
                Thread hilo = new Thread(hiloConexion);
                hilo.start();
            }
        } catch (IOException e) {
            //No se pudo crear el server
            //socket porque no tenemos permisos
            //Se pudo crear pero no fuimos
            //capaces de enviar o recibir nada
            //Todo funcionaba pero el usuario
            //interrumpió
            System.out.println("Error en conexion " +
                               "o al crear los hilos o al procesar E/S");
        }
    }

    public static void main(String[] args) {
        Servidor servidor;
        servidor = new Servidor();
        servidor.servir();
    }
}

Solución HiloConexion

public class HiloConexion implements Runnable {

    BufferedReader bfr;

    PrintWriter pw;

    Socket socket;

    public HiloConexion(Socket socket) {
        this.socket = socket;
    }

    public void run() {
        try {
            /* Nuestro hilo se limita a
             * recibir una línea y reenviarla
             * de vuelta al cliente
             */
            bfr = Utilidades.getFlujoLectura(this.socket);
            pw = Utilidades.getFlujoEscritura(this.socket);
            String lineaRecibida;
            lineaRecibida = bfr.readLine();
            System.out.print(
                Thread.currentThread().getName());
            System.out.println(" recibio:" + lineaRecibida);
            pw.println(lineaRecibida);
            pw.flush();
        } catch (IOException e) {
            System.out.println("Hubo un fallo al enviar/recibir datos");
        }
    }
    //Fin del run
}

Solución Cliente

public class Cliente implements Runnable {

    Socket conexion;

    Random generador;

    BufferedReader bfr;

    PrintWriter pw;

    String[] palabras = { "Hola", "mundo", "Java", "hilo" };

    int numHilo;

    /* Esta variable se puede comprobar por
     * parte del lanzador para ver si hubo un fallo
     */
    boolean algoFallo = false;

    public Cliente() {
        generador = new Random();
        InetSocketAddress direccion;
        direccion = new InetSocketAddress("localhost",
                                          9876);
        Socket conexion;
        conexion = new Socket();
        try {
            conexion.connect(direccion);
            bfr = Utilidades.getFlujoLectura(conexion);
            pw = Utilidades.getFlujoEscritura(conexion);
        } catch (IOException e) {
            algoFallo = true;
        }
        //Fin del catch
    }

    public int getNumHilo() {
        return numHilo;
    }

    public void setNumHilo(int numHilo) {
        this.numHilo = numHilo;
    }

    public boolean servidorFunciona() {
        /* Elegimos una palabra al azar*/
        String palabra = palabras[generador.nextInt(
                                      palabras.length)];
        String eco;
        try {
            /* Si no pudimos obtener un flujo
             * de lectura o escritura con
             * el servidor es que estaba
             * colapsado
             */
            if ((bfr == null) || (pw == null)) {
                /* Indicamos que algo fallo*/
                algoFallo = true;
                /* Y decimos que este
                 * metodo no ha funcionado
                 */
                return false;
            }
            pw.println(palabra);
            pw.flush();
            eco = bfr.readLine();
            if (eco.equals(palabra)) {
                System.out.println("Hilo " + numHilo +
                                   " recibio bien:" + eco);
                return true;
            }
            //Fin del if
        } catch (IOException e) {
            return false;
        }
        /*Si se llega a este punto es porque
         *la palabra devuelta no fue la
         *que enviamos, o sea que el servidor falló
        */
        return false;
    }

    @Override
    public void run() {
        if (!servidorFunciona()) {
            /* Imprimimos un mensaje y
             * además cambiamos la variable
             * que indica que hubo un fallo
             */
            System.out.println("Fallo en el hilo " + numHilo);
            algoFallo = true;
        }
    }

    //Fin del run
    public boolean huboFallo() {
        return algoFallo;
    }
}

Solución LanzadorClientes

public class LanzadorClientes {

    public boolean servidorAtendioClientes(
        int numClientes) {
        //En este metodo lanzamos x clientes y luego comprobamos
        //si todos funcionaron bien. Si todo fue bien
        //devolvemos true y si no devolveremos false
        Thread[] hilos = new Thread[numClientes];
        Cliente[] clientes = new Cliente[numClientes];
        for (int i = 0; i < numClientes; i++) {
            Cliente cliente = new Cliente();
            cliente.setNumHilo(i);
            Thread hiloAsociado = new Thread(cliente);
            hilos[i] = hiloAsociado;
            hiloAsociado.start();
            clientes[i] = cliente;
        }
        System.out.println("Lanzados!");
        /* Esperamos que todos los hilos acaben
         * dandoles un plazo maximo mas bien pequeño
         * Si en ese tiempo no se completó
         * una operación tan simple, probablemente
         * el servidor falló*/
        for (int i = 0; i < numClientes; i++) {
            try {
                hilos[i].join();
            } catch (InterruptedException e) {
                System.out.println("Se interrumpió un hilo por parte "
                                   + "de alguna clase del cliente ");
            }
        }
        /* Comprobamos si todos los hilos están bien
         * y en cuanto uno sufra un fallo podemos
         * asumir que el servidor no pudo atender tantos
         * clientes*/
        for (int i = 0; i < numClientes; i++) {
            if (clientes[i].huboFallo()) {
                return false;
            }
        }
        //pudo atender a todos los clientes a la vez
        return true;
    }

    public static void main(String[] args) {
        LanzadorClientes lanzador = new
        LanzadorClientes();
        /* Vamos probando a lanzar muchos clientes
         * hasta que forcemos un fallo
         */
        for (int i = 1; i < 1000; i++) {
            boolean todoOK;
            int numClientes = i * 1000;
            System.out.println("Lanzando " + numClientes +
                               " clientes...");
            todoOK = lanzador.servidorAtendioClientes(
                         numClientes);
            /* Si algo no fue bien, indicamos la cantidad
             * de clientes con que se produjo el fallo
             */
            if (!todoOK) {
                System.out.println("El servidor pareció fallar con:"
                                   + numClientes);
                return;
            }
            //Fin del if
        }
        //Fin del main
    }
}