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, . . .)
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.
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())); . . .
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(); } . . .
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.
. . . // 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) { . . . } . . .
. . . 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) { . . . } . . .
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.
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) { . . . } . . .
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 (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:
A continuación se muestran ejemplos de código para implementar un cliente y un servidor FTP, utilizando la librería Apache MINA FtpServer
. . . 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); . . .
. . . // 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(); . . .
. . . 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 ( . . . ) { . . . } ...
# 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 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:
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 } } . . .
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(); . . .
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
© 2016-2019 Santiago Faci