Table of Contents
Programación en red
Arquitectura cliente-servidor
- En una aplicación que siga el modelo de la arquitectura cliente-servidor existe una aplicación servidor que ofrece sus servicios a través de la red (LAN, Internet, . . .) a múltiples hosts o clientes que pueden conectarse con ella
- Protocolos como la Web, FTP, correo electrónico, mensajería instantánea y otros se apoyan en esta arquitectura para funcionar
- Es una arquitectura centralizada, es el servidor quién da el servicio de forma concurrente a todos los clientes que se conectan a él. Si éste cae, todo el servicio se detiene
- El servidor tendrá que diseñarse como multihilo, puesto que tiene que dar servicio a muchos clientes al mismo tiempo. Hoy en día no tendría sentido pensar en una aplicación servidor que sólo pudiera atender a un cliente al mismo tiempo
- El cliente es también multihilo mucho mejor. Así, éste podrá estar atento a lo que el servidor le diga y a lo que el usuario quiera hacer en todo momento.
- Las conexiones realizadas necesitan conocer IP y puerto para establecerse, aunque el puerto a veces no se indique si se conoce de antemano por la aplicación cliente (well known ports) como ocurre, por ejemplo, cuando solicitamos una URL desde el navegador (no hace falta indicar el puerto de conexión)
Sockets
¿Qué es un socket?
Un socket es la conexión que se establece entre dos aplicaciones en dos hosts diferentes, una aplicación cliente en un host y otra aplicación servidor en otro (siguiendo la arquitectura cliente-servidor) a través de una red (LAN, WAN, . . .)
Sockets en Java
Java dispone de toda una API para trabajar con sockets y todo lo necesario para desarrollar aplicaciones cliente-servidor. Además, como ya se ha visto en el tema anterior, también disponemos de una API completa para desarrollar aplicaciones multihilo.
Para trabajar con sockets en Java disponemos de la clase Socket
y ServerSocket
para realizar conexiones desde un cliente o para establecer la conexión de un servidor, respectivamente.
Sockets cliente
Para que una aplicación Java pueda realizar una conexión de red mediante un socket cliente, necesitaremos dos parámetros: la dirección IP del host al que nos queremos conectar y el puerto donde “escucha” la aplicación servidor a la que nos queremos conectar. A continuación, es esencial establecer los flujos de comunicación que permitirán comunicarnos hacia el servidor (flujo de salida) y recibir los mensaje que éste nos envíe (flujo de entrada).
. . . // Realiza la conexión con el host remoto Socket socketCliente = new Socket("videosdeinformatica.com", 5555); // Establece los flujos de comunicación de entrada y salida PrintWriter salida = new PrintWriter(socket.getOutputStream(), true); BufferedReader entrada = new BufferedReader(new InputStreamReader(socket.getInputStream())); . . .
Sockets servidor
Los sockets servidor o ServerSocket
permiten que aplicaciones Java puedan establecer una conexión en un equipo en un puerto determinado y de esa manera ser capaces de recibir conexiones de clientes para comunicarse con dicha aplicación.
Para establecer un socket servidor sólo es necesario indicar el puerto en el que la aplicación quedará “escuchando” las conexiones de los clientes.
Una vez establecida la conexión, la clase ServerSocket
dispone del método accept()
que bloquea la ejecución de la aplicación hasta que se recibe la conexión de un cliente. En ese momento se devuelve una referencia al socket de dicho cliente y es posible establecer los flujos de comunicación con el mismo para comenzar a dar servicio. Hay que tener en cuenta, según se puede observar en el gráfico anterior, que el flujo de entrada del socket cliente será el de salida para el servidor y viceversa.
. . . // El servidor comienza a escuchar en el puerto 5555 ServerSocket socketServidor = new ServerSocket(5555); . . . // Recibe la conexión de un cliente Socket socketCliente = socketServidor.accept(); // Establece los flujos de comunicación con ese cliente PrintWriter salida = new PrintWriter(socketCliente.getOutputStream(), true); BufferedReader entrada = new BufferedReader(new InputStreamReader(socketCliente.getInputStream())); . . .
El ejemplo anterior describe un funcionamiento muy simple de un servidor puesto que sólo sería capaz de atender la petición de un cliente, ya que una vez recibida ésta y establecer sus flujos de comunicación no sigue esperando nuevas conexiones. Es por eso que necesitaríamos de la programación multihilo (Threads
) para dotar a nuestra aplicación de capacidades concurrentes.
A continuación se muestra una forma sencilla de hacer que nuestra aplicación servidor, al recibir una conexión la prepara y la lanza como un hilo. De esta forma es capaz de volver a escuchar nuevas conexiones mientras está atendiendo las ya recibidas.
. . . // El servidor comienza a escuchar en el puerto 5555 ServerSocket socketServidor = new ServerSocket(5555); . . . // Recibe la conexión de un cliente while (conectado) { Socket socketCliente = socketServidor.accept(); // Establece los flujos de comunicación con ese cliente PrintWriter salida = new PrintWriter(socketCliente.getOutputStream(), true); BufferedReader entrada = new BufferedReader(new InputStreamReader(socketCliente.getInputStream())); // Crea y lanza un hilo para atender a ese cliente ConexionCliente conexionCliente = new ConexionCliente(socketCliente, salida, entrada); conexionCliente.start(); } . . .
Aplicaciones cliente-servidor en Java
Cliente/Servidor echo
echo es un servicio que simplemente repite el mensaje que el cliente le envía a través de un socket. Es un servicio muy sencillo (y poco útil) pero que nos permitirá comprobar la conectividad y la funcionalidad de los sockets para un primer instante.
Cliente echo
. . . // Nombre y puerto del servidor String hostname = "videosdeinformatica.com"; int puerto = 7; try { Socket socket = new Socket(hostname , puerto); PrintWriter salida = new PrintWriter(socket.getOutputStream(), true); BufferedReader entrada = new BufferedReader(new InputStreamReader(socket.getInputStream())); // Captura el teclado del usuario BufferedReader teclado = new BufferedReader(new InputStreamReader(System.in) ); String cadena = null; // Envia lo que el usuario escribe por teclado al servidor y lee la respuesta while ((cadena = teclado.readLine()) != null) { salida.println(cadena); System.out.println(entrada.readLine()); } } catch (UnknownHostException uhe) { . . . } catch (IOException ioe) { . . . } . . .
Servidor echo
. . . int puerto = 7; try { ServerSocket socketServidor = new ServerSocket(puerto); // Espera la conexion con un cliente Socket socketCliente = socketServidor.accept(); // Establece los flujos de salida y entrada (hacia y desde el cliente, respectivamente) PrintWriter salida = new PrintWriter(socketCliente.getOutputStream(), true); BufferedReader entrada = new BufferedReader(new InputStreamReader(socketCliente.getInputStream())); // Envia algunos mensajes al cliente en cuanto este se conecta salida.println("Solo se repetir lo que me escribas"); salida.println("Cuando escribas ’.’, se terminara la conexion"); String linea = null; while ((linea = entrada.readLine()) != null) { if (linea.equals(".")) { socketCliente.close(); socketServidor.close(); break; } } } catch (IOException ioe) { . . . } . . .
Cliente/Servidor echo multihilo
En este caso, aprovechando las características de los hilos de Java, implementaremos una versión del servidor de echo capaz de atender múltiples conexiones simultáneas.
Servidor echo multihilo (Clase Servidor)
En este caso, para el servidor multihilo, tendremos una clase Servidor
, que se corresponde con el siguiente código, y que lanza un objeto ConexionCliente
para atender a cada uno de los clientes que se conectan al servidor. De esa manera, puesto que el objeto ConexionCliente
es un hilo, puede atender una petición mientras espera otra y asi sucesivamente.
. . . ServerSocket servidor = null; ConexionCliente conexionCliente = null; int puerto = 7; try { servidor = new ServerSocket(puerto); while (!servidor.isClosed()) { conexionCliente = new ConexionCliente(servidor.accept()); conexionCliente.start(); } } catch (IOException ioe) { . . . } . . .
Servidor echo multihilo (clase ConexionCliente)
En esta clase ConexionCliente
habrá que implementar lo necesario para atender una sola petición de cliente. Hay que tener en cuenta que habrá tanto objetos de esta clase como clientes conectados en un momento determinado. Al tratarse de un hilo se ejecutará en segundo plano y podrán atenderse múltiples de ellas al mismo tiempo.
public class ConexionCliente extends Thread { private Socket socket; private PrintWriter salida; private BufferedReader entrada; public ConexionCliente(Socket socket) throws IOException { this.socket = socket; salida = new PrintWriter(socket.getOutputStream(), true); entrada = new BufferedReader(new InputStreamReader( socket.getInputStream())); } @Override public void run() { salida.println("Solo se repetir lo que me escribas"); salida.println("Cuando escribas ’.’, se terminara la conexion"); try { String mensaje = null; while ((mensaje = entrada.readLine()) != null) { if (mensaje.equals(".")) { socket.close(); break; } salida.println(mensaje); } } catch (IOException ioe) { . . . } }
FTP
FTP (File Transfer Protocol) es un protocolo de transferencia de ficheros entre un cliente y un servidor. Básicamente se utilizar para subir/bajar ficheros a/desde un sitio remoto. Sus características principales son las siguientes:
- El lado servidor utiliza el puerto 21 para establecer su conexión
- Incorpora mecanismo de autenticación (usuario/contraseña) y soporte para usuarios anónimos (sólo lectura) y también existen versiones que soportan conexiones seguras utilizando el protocolor HTTPS
- También incorpora mecanismos de seguridad basada en permisos
- Permite navegar por el sitio remoto y realizar en él algunas acciones como crear, listar, eliminar directorios, copia de ficheros, . . . a través de comandos (mkdir, ls, rmdir, . . .)
- Es posible realizar implementaciones de clientes con interfaz gráfica por lo que el manejo puede llegar a ser muy intuitivo
- Es un protocolo cliente/servidor de forma que una máquina ejecuta el lado servidor almacenando los ficheros, de forma que los clientes pueden conectarse a él para descargarse esos ficheros o bien (si tienen permisos) pueden subir nuevos ficheros
- Es uno de los protocolos más antiguos de Internet pero sigue siendo ampliamente utilizado
A continuación se muestran ejemplos de código para implementar un cliente y un servidor FTP, utilizando la librería Apache MINA FtpServer
Cliente FTP
Cómo establecer una conexión
. . . FTPClient clienteFtp = new FTPClient(); clienteFtp.connect(IP, PUERTO); clienteFtp.login(USUARIO, CONTRASENA); /* * En el modo pasivo es siempre el cliente quien abre las conexiones * Da menos problemas si estamos detras de un firewall, por ejemplo */ clienteFtp.enterLocalPassiveMode(); clienteFtp.setFileType(FTPClient.BINARY_FILE_TYPE); . . .
Cómo descargar un fichero
. . . // Lista los ficheros del servidor (a modo de ejemplo) FTPFile[] ficheros = clienteFtp.listFiles(); for (int i = 0; i < ficheros.length; i++) { System.out.println(ficheros[i].getName()); } // Fija los ficheros remoto y local String ficheroRemoto = "/modelo.txt"; File ficheroLocal = new File("modelo.txt"); System.out.println("Descargando fichero '" + ficheroRemoto + "' del servidor . . ."); // Descarga un fichero del servidor FTP OutputStream os = new BufferedOutputStream(new FileOutputStream(ficheroLocal)); if (clienteFtp.retrieveFile(ficheroRemoto, os)) System.out.println("El fichero se ha recibido correctamente"); os.close(); . . .
Servidor FTP
El servidor
. . . FtpServerFactory serverFactory = new FtpServerFactory(); ListenerFactory miListenerFactory = new ListenerFactory(); miListenerFactory.setPort(PUERTO); serverFactory.addListener("default", miListenerFactory.createListener()); try { ConnectionConfigFactory miConnectionConfigFactory = new ConnectionConfigFactory(); miConnectionConfigFactory.setAnonymousLoginEnabled(true); ConnectionConfig connectionConfig = miConnectionConfigFactory.createConnectionConfig(); serverFactory.setConnectionConfig(connectionConfig); // Fija la configuracion de las cuentas de usuario PropertiesUserManagerFactory userManagerFactory = new PropertiesUserManagerFactory(); userManagerFactory.setFile(new File("usuarios.properties")); serverFactory.setUserManager(userManagerFactory.createUserManager()); FtpServer servidorFtp = serverFactory.createServer(); servidorFtp.start(); } catch ( . . . ) { . . . } ...
Fichero de configuración
# Password: "admin" # Username: "admin" ftpserver.user.admin.userpassword=21232F297A57A5A743894A0E4A801FC3 ftpserver.user.admin.homedirectory=./home ftpserver.user.admin.enableflag=true ftpserver.user.admin.writepermission=true ftpserver.user.admin.maxloginnumber=0 ftpserver.user.admin.maxloginperip=0 ftpserver.user.admin.idletime=0 ftpserver.user.admin.uploadrate=0 ftpserver.user.admin.downloadrate=0
HTTP
HTTP es un protocolo utilizado para la transferencia de hipertexto (páginas web) a través de la red (normalmente Internet). El hipertexto es un sistema de documentos (de texto) en el que éstos no están organizados de forma secuencial sino que es posible acceder a cualquiera de ellos desde cualquier otro, incluso si éstos provienen de diferentes fuentes (sitios web), mediante lo que se conoce como un hipervínculo. Sus características principales son las siguientes:
- Los servidores HTTP (o servidores web) utilizan el puerto 80 para establecer su conexión
- El lenguaje utilizado para crear los documentos (o páginas web) es el HTML
- Permiten además la transferencia de cualquier otro tipo de ficheros
- El protocolo por sí solo no establece mecanismos de seguridad, por lo que en un principio cualquier puede acceder al documento que quiera. Más adelante se han implementando herramientas que permiten añadir esa capa de seguridad donde se requiere
- Es un protocolo cliente/servidor en el que un servidor almacena una serie de documentos de forma que múltiples clientes pueden conectarse a él para acceder a los mismos
- Es un protocolo muy sencillo y antiguo y, probablemente, el más extendido y utilizado en Internet
Cliente HTTP (navegador web)
Para implementar un cliente HTTP basta con fijar una URL a un componente JEditorPane
, puesto que es capaz de renderizar páginas web HTML directamente (aunque se una forma bastante básica).
. . . JEditorPane epPagina = new JEditorPane(); String url = "http://psp.codeandcoke.com"; try { epPagina.setPage(url); } catch (IOException ioe) { // Error al intentar cargar la URL } } . . .
Servidor HTTP (servidor web)
En este caso implementamos directamente la versión multihilo del servidor HTTP. Así, de forma similar como hicimos con el servidor echo, en una clase principal esperamos las conexiones de clientes de forma que sean atendidos mediante hilos que serán lanzados por cada una de las conexiones recibidas.
. . . boolean conectado = true; ServerSocket servidor = null; try { servidor = new ServerSocket(80); while (conectado) { ConexionCliente conexionCliente = new ConexionCliente(servidor.accept()); conexionCliente.start(); } if (servidor != null) servidor.close(); } catch ( . . . ) { . . . } . . .
Ya en la clase ConexionCliente
será donde analicemos la petición recibida para comprobar que documento HTML nos ha solicitado el cliente.
. . . @Override public void run() { try { String peticion = entrada.readLine(); if (peticion.startsWith("GET")) { String[] partes = peticion.split(" "); String rutaFichero = partes[1].substring(1); /* Si no ha solicitado ninguna pagina es que ha solicitado la * pagina por defecto que normalmente es index.html */ if (rutaFichero.equals("")) rutaFichero = "index.html"; File fichero = new File("htdocs" + File.separator + rutaFichero); if (!fichero.exists()) { salida.writeBytes("HTTP/1.0 404 Not Found\r\n"); salida.writeBytes("\r\n"); salida.writeBytes("<html><body>Documento no encontrado</body></html >\r\n"); desconectar(); return; } else { . . . } } } } . . .
En el caso de que la petición prospere, habrá que preparar la respuesta, que estará compuesta de cierta información de protocolo más el contenido del documento solicitado.
. . . // Prepara el fichero que se tiene que enviar FileInputStream fis = new FileInputStream(fichero); int tamanoFichero = (int) fichero.length(); byte[] bytes = new byte[tamanoFichero]; fis.read(bytes); fis.close(); // Prepara las cabecera de salida para el navegador salida.writeBytes("HTTP/1.0 200 OK\r\n"); salida.writeBytes("Server: MiJavaHTTPServer\r\n"); if (rutaFichero.endsWith(".jpg")) salida.writeBytes("Content -Type: image/jpg\r\n"); else if (rutaFichero.endsWith(".html")) salida.writeBytes("Content -Type: text/html\r\n"); salida.writeBytes("Content -Length: " + tamanoFichero + "\r\n"); // Linea en blanco, obligatoria segun el protocolo salida.writeBytes("\r\n"); // Envia el contenido del fichero salida.write(bytes, 0, tamanoFichero); desconectar(); . . .
Proyectos de ejemplo
Todos los proyectos de este tema se pueden encontrar en el repositorio red de GitHub y aquí dejo enlaces a las descargas directamente para que sea más cómodo hacerse con algunos proyectos.
Los proyectos de los ejercicios que se vayan haciendo en clase estarán disponibles en el repositorio psp-ejercicios de GitHub
Prácticas
- Práctica 2.1 Creación de una aplicación cliente-servidor
© 2016-2019 Santiago Faci