G.Bordel >Docencia >TAP Técnicas Actuales de Programación (curso 2010-2011)
desprotegido Intro. desprotegido Temario desprotegido Calendario desprotegido RPF desprotegido Recursos protegido Práctica protegido Gest. Alum.
tema_anterior Tema 11: Programacion en red tema_siguiente
  1. Sockets.
  2. Una aplicación cliente/servidor.[ejercicios]

11.2- Una aplicación cliente/servidor

En este apartado veremos un ejemplo simple de aplicación cliente-servidor. Se tratará simplemente, en primer lugar, de establecer la conexión y enviar Strings desde el cliente al servidor, para, más adelante, completar este ejemplo con la posibilidad de atender a multiples clientes y establecer el la comunicación de Strings en ambos sentidos.

En el siguiente recuadro tenemos el código de un servidor mínimo. En la línea 6 se instancia un objeto ServerSocket "escuchando" al puerto 6000, y posteriormente se entra en un ciclo infinito. La primera acción de este ciclo es esperar a un cliente (línea 8). Cuando surje, el método accept nos devuelve una conexión ("s") con él (un socket). Al objeto "s" le pedimos los streams de entrada y de salida en las líneas 11 y 12, que después recubrimos convenientemente a nuestras necesidades actuales como un BufferedReader y un PrintStream en 13 y 14 (podríamos haber prescindido de todo lo referente a salida puestoq ue este ejemplo no hara uso de ella, pero lo ponemos para mostrar el procedimiento habitual).

Una vez que disponemos de los streams podemos "leer" lo que nos envie el cliente e igualmente "escribirle". En este ejemplo nos limitamos a un bucle de lectura (líneas 17-19) del Reader sin que exista ninguna particularidad por el hecho de que el origen de los datos sea una máquina diferente. En el ejemplo se consulta si el contenido de la String recibida es "exit" para terminar el ciclo de lectura. En tal caso se cierra el Socket (línea 21) terminando el código del ciclo general y volviendo por tanto al punto de espera a un nuevo cliente (línea 8).

Observesé que en este ejemplo la conexión termina por cierre del Socket por parte del servidor. Lo mismo podría haberse hecho desde el cliente, lo que resultaría en una excepción en la lectura del Reader. Podía haberse establecido éste como mecanismo de terminación. En todo caso en una aplicación correcta habría de tenerse en cuenta esta posibilidad de cierre de la conexión or parte del cliente, de modo que habrá de atenderse a la excepción que aquí se arroja.

1-  import java.io.*;
2-  import java.net.*;
3-  
4-  public class Server {
5-    public static void main(String[] args)throws IOException{
6-      ServerSocket ss=new ServerSocket(6000);
7-      do {
8-        Socket s=ss.accept();
9-        System.out.println("\n\n***Conexion establecida con: "+s);
10-  
11-        InputStream is=s.getInputStream();
12-        OutputStream os=s.getOutputStream();
13-  
14-        BufferedReader br=new BufferedReader(new InputStreamReader(is));
15-        PrintStream ps=new PrintStream(os);
16-        String query;
17-        while (!(query=br.readLine()).equals("exit")) {
18-          System.out.println("He recibido: "+query);
19-        }
20-        System.out.println("***Cerrando conexion con: "+s+"\n\n");
21-        s.close();
22-      } while (true);
23-    }
24-  }

El cliente, para establecer una conexión con el servidor conocido, instancia directamente un Socket indicando la dirección de la máquina del servidor (como texto) y el número de puerto (línea 7). Del mismo modo que en el caso del servidor, el Socket permite acceder a los streams de entrada y salida que, a continuación, son recubiertos adecuadamente (9-10, 12-13).

Este cliente se limita a leer el teclado línea a línea y enviar la String al servidor, para lo cual recubre el System.in con un BufferedReader y en un ciclo infinito lee de teclado, escribe en el PrintStream que recubre al stream de salida del Socket y controla si el texto es "exit" para terminar en tal caso.

1-  import java.io.*;
2-  import java.net.*;
3-  
4-  public class Client {
5-  
6-    public static void main(String[] args) throws UnknownHostException, IOException {
7-      Socket s=new Socket("127.0.0.1",6000);
8-  
9-      InputStream is=s.getInputStream();
10-      OutputStream os=s.getOutputStream();
11-  
12-      BufferedReader br=new BufferedReader(new InputStreamReader(is));
13-      PrintStream ps=new PrintStream(os);
14-  
15-  
16-      BufferedReader teclado=new BufferedReader(new InputStreamReader(System.in));
17-      String q;
18-      do {  q=teclado.readLine();
19-        ps.println(q);
20-        if (q.equals("exit")) System.exit(0);
21-      } while (true);
22-    }
23-  }


Una vez vistos el servidor y el cliente anteriores, vamos a añadir un par de características necesarias para que sean minimamente de utilidad.

En primer lugar, el servidor sólo es capaz de servir a un cliente, de modo que si un segundo cliente pretende conectar, como el servidor no se encuentra en el accept, no obtiene respuesta. Para ello es necesario que el servidor tenga capacidad para llevar a cabo un hilo de ejecución asociado a cada cliente, de modo que lo que hacemos es instanciar un nuevo objeto cada vez que se retorna del accept, dándole el Socket y delegando en él toda la relación con el cliente.

La clase ThreadedSocket extiende Thread y en su método run se encarga de obtener los streams, recubrirlos adecuadamente y mantener el ciclo de comunicación con el cliente.

1-  import java.io.*;
2-  import java.net.*;
3-  
4-  public class ThreadedServer {
5-    public static void main(String[] args)throws IOException{
6-      ServerSocket ss=new ServerSocket(6000);
7-      do {
8-        Socket s=ss.accept();
9-        new ThreadedSocket(s);
10-      } while (true);
11-    }
12-  }
13-  
14-  class ThreadedSocket extends Thread {
15-    Socket s;
16-    ThreadedSocket(Socket s){super(); this.s=s; start();}
17-  
18-    public void run() {
19-        System.out.println("\n\n***Conexion establecida con: "+s);
20-  
21-      try{
22-        InputStream is=s.getInputStream();
23-        OutputStream os=s.getOutputStream();
24-  
25-        BufferedReader br=new BufferedReader(new InputStreamReader(is));
26-        PrintStream ps=new PrintStream(os);
27-        ps.println("OK");
28-        String query;
29-        while (!(query=br.readLine()).equals("exit")) {
30-          System.out.println("He recibido: "+query);
31-          ps.println("OK");
32-        }
33-        System.out.println("***Cerrando conexion con: "+s+"\n\n");
34-        s.close();
35-      } catch (Exception e) {
36-      } 
37-    }
38-  }  

En nuestro ejemplo inicial, el cliente sólo era capaz de enviar lineas de texto al servidor, pero normalmente un cliente también tendrá que recibir información, de modo que en este caso también hay tareas a llevar a cabo en paralelo: la que ya llevabamos a cabo -copiar del teclado al servidor- y la nueva -copiar del servidor a la salida estándar-.

Para llevar a cabo la nueva tarea definimos una clase (lo hacemos de modo local a la rutina main) que se encargará automáticamente de realizar el "eco" de lo recibido del servidor en la salida estándar. De este modo la simple instanciación de un objeto de esta clase nos basta para completar la funcionalidad buscada.

1-  import java.io.*;
2-  import java.net.*;
3-  
4-  public class ThreadedClient {
5-  
6-    public static void main(String[] args) throws UnknownHostException, IOException {
7-      Socket s=new Socket("127.0.0.1",6000);
8-  
9-      InputStream is=s.getInputStream();
10-      final BufferedReader br=new BufferedReader(new InputStreamReader(is));
11-  class Echo extends Thread{
12-      Echo(){setDaemon(true);start();}
13-      public void run(){
14-        try {while (true)System.out.println(br.readLine());}catch (IOException e){}}
15-      }
16-      Echo e=new Echo();
17-  
18-      PrintStream ps=new PrintStream(s.getOutputStream());
19-      BufferedReader teclado=new BufferedReader(new InputStreamReader(System.in));
20-      String q;
21-      do {q=teclado.readLine();
22-          ps.println(q);
23-    if (q.equals("exit")) System.exit(0);
24-      } while (true);
25-    }
26-  }

Se ha visto un ejemplo mínimo y se han añadido las dos funcionalidades necesarias para que tengamos la posibilidad de mantener un intercambio de frases entre un conjunto indeterminado de clientes y el servidor. Esto ha sido muy sencillo entre otras cosas porque no ha sido necesario establecer sincronismos entre hilos (no hay cooperación) ni un protocolo de comunicación (sólo se han cruzado cadenas de texto, no mensajes con diferentes contenidos).

Un modo muy sencillo de establecer comunicaciones cliente-servidor mediante un protocolo consiste en intercambiar objetos (en lugar de textos) recubriendo los streams con ObjectInputStream y ObjectOutputStream, de modo que la complejidad del protocolo queda embebida en la estructura de los objetos.

Siguiente Tema: 12- Java y XML


Plataforma de soporte a curso y contenidos (c) German Bordel 2005.