Java permite realizar aplicaciones que posean comunicación en red, mediante el uso del modelo cliente servidor.
Modelo Cliente Servidor
El modelo cliente servidor permite la comunicación entre un equipo denominado servidor y un conjunto de equipos denominados clientes. Los clientes se encuentran en constante comunicación con el servidor y a través de este, pueden establecer comunicación con otros clientes. El servidor es el encargado de aceptar solicitudes de conexión por parte de los clientes y los clientes deben conectarse al servidor a través de su dirección lógica y de su puerto de aplicación. Esto significa que todas las gestiones que se realizan se concentran en el servidor, de manera que en él se disponen los diferentes requerimientos del sistema.
Socket Y ServerSocket
El socket es el elemento que permite establecer una comunicación entre un servidor y múltiples clientes. Es posible trabajar comunicaciones mediante la clase Socket del paquete java.net. Esta clase permite crear conexiones que utiliza el protocolo TCP, entre dos computadores. Los Sockets con conectores entre dos computadores remotos en donde su comunicación es continua y finaliza cuando uno de los dos computadores cierra su conexión. La clase ServerSocket es una clase que pertenece al paquete java.net que sirve para atender peticiones de conexiones, lo cual es necesario para la creación de un servidor. Para crear un servidor, se debe crear un Socket y un ServerSocket. El ServerSocket debe tener como parámetro el número de puerto de la aplicación. Este número de puerto debe ser un número superior a 1024, con el fin de que la aplicación no presente conflicto con otras aplicaciones o protocolos de redes de computadores. La clase ServerSocket tiene el método accept el cual permite aceptar una conexión. Esto indica que el servidor debe tener un proceso iterativo que tenga un proceso para aceptar conexiones. El método accept retorna un Socket que contiene la información del cliente remoto que se ha conectado. La clase Socket, posee los métodos getInputStream que retorna un Stream con el cual el servidor puede recibir información por parte del cliente conectado y getOutputStream que retorna un Stream con el cual el servidor puede enviar información al cliente conectado. La implementación para crear un servidor con una sola conexión por parte de un cliente es la siguiente.
try { ServerSocket serverSocket = new ServerSocket(5000); System.out.println("Esperando nueva conexion"); Socket socket = serverSocket.accept(); System.out.println("Conexion desde: "+socket.getInetAddress().getHostName()+" IP: " + this.socket.getInetAddress().getHostAddress()); ObjectInputStream input = new ObjectInputStream(socket.getInputStream()); ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream()); } catch (IOException e) { e.printStackTrace(); }catch (ClassNotFoundException e) { e.printStackTrace(); }
En esta implementación de servidor, se obtiene un Socket al presentarse una conexión. La clase Socket además provee el método getInetAddress, el cual retorna un object instancia de la clase InetAddress que a su vez cuenta con los métodos getHostName y getHostAddress que permiten capturar el nombre y dirección IP respectivamente del cliente conectado. El cliente, puede conectarse al servidor, una vez que éste se encuentre esperando conexión. Entonces en el cliente debe crearse un Socket al cual se le envía por parámetro una dirección IP a través de la clase InetAddress y el número del puerto de servidor.
try { Socket socket = new Socket(InetAddress.getByName(127.0.0.1),5000); ObjectInputStream input = new ObjectInputStream(socket.getInputStream()); ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream()); } catch (IOException e) { e.printStackTrace(); }catch (ClassNotFoundException e) { e.printStackTrace(); }
De la misma forma que en el servidor, se puede obtener un Stream para enviar datos y otro para recibir datos. El modelo cliente servidor, indica que se puede conectar un servidor con múltiples clientes. Para ello, es necesario modificar la implementación anterior con el fin de permitir múltiples conexiones. La implementación para crear un servidor con múltiples conexiones por parte de clientes es la siguiente.
try { ServerSocket serverSocket = new ServerSocket(5000); int contador=1; while(true){ System.out.println("Esperando nueva conexion"); Socket socket = serverSocket.accept(); System.out.println( "Conexion " + contador +" desde: " + this.socket.getInetAddress().getHostName()+" IP: "+ this.socket.getInetAddress().getHostAddress()); contador++; ObjectInputStream input = new ObjectInputStream(socket.getInputStream()); ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream()); } } catch (IOException e) { e.printStackTrace(); }catch (ClassNotFoundException e) { e.printStackTrace(); }
Esta implementación atiende las conexiones de clientes mediante un proceso iterativo sin fin. Sin embargo, en el momento de recibir una conexión, crea los Stream de entrada y salida, pero ejecuta la siguiente iteración y queda de nuevo esperando conexión. Esto indica que este servidor estará en capacidad únicamente de aceptar conexiones pero no podrá enviar ni recibir mensajes. Para agregar funcionalidad de envió y recepción de mensajes, es necesario implementar un hilo, para que el servidor cree una instancia del hilo por cada conexión. Entonces, a través de cada hilo, el servidor puede enviar y recibir información. La implementación del hilo es la siguiente.
package Servidor; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class HiloServidor extends Thread { private ObjectOutputStream output; private ObjectInputStream input; private Servidor servidor; boolean activo=true; private String mensajeRecibido; public HiloServidor(String nombre, ObjectInputStream input, ObjectOutputStream output, Servidor servidor){ super(nombre); this.input=input; this.output=output; this.servidor=servidor; } public void run(){ while(this.activo){ try { this.mensajeRecibido = (String)this.input.readObject(); System.out.println(this.getName()+" dice: "+this.mensajeRecibido); this.servidor.enviarMensaje(this.mensajeRecibido); } catch (IOException e) { this.activo=false; } catch (ClassNotFoundException e) { e.printStackTrace(); } } } public void enviar (String mensaje){ try { this.output.writeObject(mensaje); this.output.flush(); } catch (IOException e) { e.printStackTrace(); } } }
Este hilo recibe a través del constructor, un nombre del hilo, un Stream de entrada, un Stream de salida y una referencia al servidor. Esta última se crea con el objetivo de tener acceso a los servicios implementados en el servidor. El método run se encarga de recibir información a través del Stream de entrada, por medio del método readObject, al cual se le debe aplicar un casting. El método enviar se encarga de enviar información a través del Stream de salida, por medio de los métodos writeObject y flush. Para que el servidor pueda hacer uso del hilo, debe modificarse de tal forma que por cada conexión pueda crear un hilo y almacenarlo. Al crear un hilo, el servidor debe enviar el nombre del hilo, el Stream de entrada, el Stream de salida y el servidor a través de la sentencia this. La implementación del nuevo servidor es la siguiente.
package Servidor; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.Iterator; import java.util.Vector; public class Servidor { private Socket socket; private ServerSocket serverSocket; private Vector hilosServidor = new Vector(); public void iniciarServidor(){ try { this.serverSocket = new ServerSocket(5000); int contador=1; while(true){ System.out.println("Esperando nueva conexion"); this.socket = serverSocket.accept(); System.out.println("Conexion " + contador +" desde: " + this.socket.getInetAddress().getHostName()+" IP: "+ this.socket.getInetAddress().getHostAddress()); contador++; ObjectInputStream input = new ObjectInputStream(this.socket.getInputStream()); ObjectOutputStream output = new ObjectOutputStream(this.socket.getOutputStream()); HiloServidor hiloServidor = new HiloServidor("Cliente "+contador, input, output, this); hiloServidor.start(); this.hilosServidor.add(hiloServidor); } } catch (IOException e) { e.printStackTrace(); }catch (ClassNotFoundException e) { e.printStackTrace(); } } public void enviarMensaje(String mensaje){ Iterator iterator = this.hilosServidor.iterator(); while(iterator.hasNext()){ HiloServidor hiloTemp=(HiloServidor)iterator.next(); hiloTemp.enviar(hiloTemp.getName()+" dice: "+mensaje); } } public static void main(String[] args) { Servidor servidor = new Servidor(); servidor.iniciarServidor(); } }
El método iniciarServidor, se encarga de recibir conexiones. Por cada conexión recibida crea un hilo que adiciona en un vector. Cada hilo creado, envía por el constructor el nombre, el Stream de entrada, el Stream de salida y una referencia al servidor. El método enviarMensaje, envía un mensaje que recibe por parámetro a todos los clientes, mediante un proceso iterativo de los hilos almacenados en el vector. En la aplicación del cliente, también se requiere crear un hilo que se encarga de recibir mensajes por parte del servidor. La implementación es la siguiente.
package Cliente; import java.io.IOException; import java.io.ObjectInputStream; public class HiloCliente extends Thread { private ObjectInputStream input; private boolean activo=true; public HiloCliente(ObjectInputStream input) { this.input=input; } public void run(){ while(this.activo){ String mensaje; try { mensaje = (String)this.input.readObject(); System.out.println(mensaje); } catch (IOException e) { this.activo=false; } catch (ClassNotFoundException e) { e.printStackTrace(); } } } }
El método run de forma permanente intenta leer un mensaje por parte del servidor. En caso que ocurra una excepción, el atributo activo pasa a falso y el hilo deja de leer a través del Stream de entrada que fue recibido por el constructor.
Chat
Una de las aplicaciones clásicas del modelo cliente servidor mediante Sockets es el chat. Un chat debe tener un servidor que reciba conexiones de múltiples clientes. Cuando un cliente se conecta, el servidor notifica a todos los clientes que este nuevo cliente se ha conectado. Cuando un cliente envía un mensaje, el servidor reenvía este mensaje a todos los clientes. La implementación del chat es la siguiente.
Clase Servidor
package Servidor; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.Iterator; import java.util.Vector; public class Servidor { private Socket socket; private ServerSocket serverSocket; private String nombreUsuarioConectado; private Vector hilosServidor = new Vector(); public void iniciarServidor(){ try { this.serverSocket = new ServerSocket(5000); int contador=1; while(true){ System.out.println("Esperando nueva conexion"); this.socket = serverSocket.accept(); System.out.println( "Conexion " + contador +" desde: " + this.socket.getInetAddress().getHostName()+" IP: "+ this.socket.getInetAddress().getHostAddress()); contador++; ObjectInputStream input = new ObjectInputStream(this.socket.getInputStream()); ObjectOutputStream output = new ObjectOutputStream(this.socket.getOutputStream()); this.nombreUsuarioConectado = (String)input.readObject(); System.out.println(this.nombreUsuarioConectado+" se ha conectado"); HiloServidor hiloServidor = new HiloServidor(this.nombreUsuarioConectado, input, output, this); hiloServidor.start(); this.hilosServidor.add(hiloServidor); this.enviarClienteConectado(); } } catch (IOException e) { e.printStackTrace(); }catch (ClassNotFoundException e) { e.printStackTrace(); } } public void enviarClienteConectado(){ Iterator iterator = this.hilosServidor.iterator(); while(iterator.hasNext()){ HiloServidor hiloTemp=(HiloServidor)iterator.next(); hiloTemp.enviar(this.nombreUsuarioConectado+" se ha conectado"); } } public void enviarMensaje(String mensaje){ Iterator iterator = this.hilosServidor.iterator(); while(iterator.hasNext()){ HiloServidor hiloTemp=(HiloServidor)iterator.next(); hiloTemp.enviar(hiloTemp.getName()+" dice: "+mensaje); } } public static void main(String[] args) { Servidor servidor = new Servidor(); servidor.iniciarServidor(); } }
En la clase Servidor, el método iniciarServidor se encarga de recibir conexiones por parte de clientes. Una vez hecha la conexión recibe un nombre de cliente, el cual será el identificador del hilo. El método enviarClienteConectado, se encarga de enviar a todos los clientes una notificación de conexión de un nuevo cliente. El método enviarMensaje, se encarga de enviar el mensaje que recibe por parámetro a todos los clientes conectados.
Clase HiloServidor
package Servidor; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; public class HiloServidor extends Thread { private ObjectOutputStream output; private ObjectInputStream input; private Servidor servidor; boolean activo=true; private String mensajeRecibido; public HiloServidor(String nombre, ObjectInputStream input, ObjectOutputStream output, Servidor servidor){ super(nombre); this.input=input; this.output=output; this.servidor=servidor; } public void run(){ while(this.activo){ try { this.mensajeRecibido = (String)this.input.readObject(); System.out.println(this.getName()+" dice: "+this.mensajeRecibido); this.servidor.enviarMensaje(this.mensajeRecibido); } catch (IOException e) { this.activo=false; } catch (ClassNotFoundException e) { e.printStackTrace(); } } } public void enviar (String mensaje){ try { this.output.writeObject(mensaje); this.output.flush(); } catch (IOException e) { e.printStackTrace(); } } }
En la clase HiloServidor, el método run se encarga de recibir mensajes del cliente asociado con el hilo. Una vez recibido un mensaje, invoca el método enviarMensaje de la clase Servidor con el fin de enviar dicho mensaje recibido a todos los clientes. El método enviar, se encarga de enviar al cliente asociado con el hilo, el mensaje recibido por parámetro.
Clase FCliente
import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.InetAddress; import java.net.Socket; import java.net.UnknownHostException; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.WindowConstants; import javax.swing.SwingUtilities; public class FCliente extends javax.swing.JFrame { private JPanel PConexion; private JTextField TIP; private JTextField TNombre; private JButton BEnviar; private JTextField TMensaje; private JButton BConectar; private JLabel LNombre; private JLabel LIP; private JTextArea TMensajes; private JPanel PMensaje; private Socket socket; private HiloCliente hiloCliente; private ObjectOutputStream output; private ObjectInputStream input; public static void main(String[] args) { SwingUtilities.invokeLater(new Runnable() { public void run() { FCliente inst = new FCliente(); inst.setLocationRelativeTo(null); inst.setVisible(true); } }); } public FCliente() { super(); initGUI(); } public JTextArea getTMensajes() { return TMensajes; } private void initGUI() { try { setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); BorderLayout thisLayout = new BorderLayout(); this.setTitle("Mini Chat. Cliente"); getContentPane().setLayout(thisLayout); { PConexion = new JPanel(); getContentPane().add(PConexion, BorderLayout.NORTH); { LIP = new JLabel(); PConexion.add(LIP); LIP.setText("IP:"); } { TIP = new JTextField(); PConexion.add(TIP); TIP.setText("127.0.0.1"); } { LNombre = new JLabel(); PConexion.add(LNombre); LNombre.setText("Nombre Cliente"); } { TNombre = new JTextField(); PConexion.add(TNombre); TNombre.setPreferredSize(new java.awt.Dimension(100, 20)); TNombre.setSize(100, 20); } { BConectar = new JButton(); PConexion.add(BConectar); BConectar.setText("Conectar"); BConectar.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { BConectarActionPerformed(evt); } }); } } { PMensaje = new JPanel(); BorderLayout PMensajeLayout = new BorderLayout(); PMensaje.setLayout(PMensajeLayout); getContentPane().add(PMensaje, BorderLayout.SOUTH); { TMensaje = new JTextField(); PMensaje.add(TMensaje, BorderLayout.CENTER); TMensaje.setEnabled(false); TMensaje.setPreferredSize(new java.awt.Dimension(348, 21)); } { BEnviar = new JButton(); PMensaje.add(BEnviar, BorderLayout.EAST); BEnviar.setText("Enviar"); BEnviar.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { BEnviarActionPerformed(evt); } }); BEnviar.setEnabled(false); } } { TMensajes = new JTextArea(); getContentPane().add(TMensajes, BorderLayout.CENTER); } pack(); setSize(400, 300); } catch (Exception e) { e.printStackTrace(); } } private void BConectarActionPerformed(ActionEvent evt) { if(!this.TNombre.getText().equals("")){ try { this.socket = new Socket(InetAddress.getByName(this.TIP.getText()),5000); this.output = new ObjectOutputStream(this.socket.getOutputStream()); this.input = new ObjectInputStream(this.socket.getInputStream()); this.output.writeObject(this.TNombre.getText()); this.output.flush(); this.hiloCliente = new HiloCliente(this,this.input); this.hiloCliente.start(); this.BConectar.setEnabled(false); this.TNombre.setEnabled(false); this.TIP.setEnabled(false); this.TMensaje.setEnabled(true); this.BEnviar.setEnabled(true); } catch (UnknownHostException e) { JOptionPane.showMessageDialog(this,"Error de servidor","Error",JOptionPane.ERROR_MESSAGE); } catch (IOException e) { JOptionPane.showMessageDialog(this,"Error de servidor","Error",JOptionPane.ERROR_MESSAGE); } }else{ JOptionPane.showMessageDialog(this, "Inserte Nombre","Error",JOptionPane.ERROR_MESSAGE); } } private void BEnviarActionPerformed(ActionEvent evt) { try { this.output.writeObject(this.TMensaje.getText()); this.output.flush(); } catch (IOException e) { e.printStackTrace(); } this.TMensaje.setText(""); } }
La clase FCliente, es un JFrame que ejecuta el cliente, para enviar y recibir mensajes del servidor. Este Frame, contiene un cuadro de texto para ingresar la dirección IP donde se ubica el servidor, un cuadro de texto para ingresar el nombre del cliente, un botón Conectar para realizar la conexión con el servidor, un cuadro de texto para que el cliente digite el mensaje que desea enviar, un botón que envía el mensaje del cuadro de texto anterior y un área de texto que permite incluir los mensajes recibidos.Al ejecutar el cliente, se presenta el siguiente JFrame
El método BConectarActionPerformed se invoca al hacer clic en el botón conectar y efectúa la conexión con el servidor a través de un socket indicando la dirección IP del servidor y puerto de la aplicación. Una vez el servidor atiende la conexión, se crea el Stream de entrada, el Stream de salida y el hilo del cliente.El método BEnviarActionPerformed, envía información al servidor a través del Stream de salida obtenido por el socket en el cliente.
Clase HiloCliente
import java.io.IOException; import java.io.ObjectInputStream; public class HiloCliente extends Thread { private ObjectInputStream input; private FCliente cliente; private boolean activo=true; public HiloCliente(FCliente cliente,ObjectInputStream input) { this.cliente=cliente; this.input=input; } public void run(){ while(this.activo){ String mensaje; try { mensaje = (String)this.input.readObject(); this.cliente.getTMensajes().append(mensaje+"\n"); } catch (IOException e) { this.activo=false; } catch (ClassNotFoundException e) { e.printStackTrace(); } } } }
En la clase HiloCliente, el método run se encarga de recibir mensajes del servidor. Una vez recibido un mensaje, lo coloca en el área de texto del Frame del cliente
Prueba de Chat
Al ejecutar el servidor, queda en espera de una conexión. Es importante anotar que el servidor se encuentra en la dirección IP 192.168.1.1. Presenta la siguiente información en consola.
esperando nueva conexion
Al ejecutar un primer Cliente, se presenta el Frame de la figura anterior. Al ingresar un nombre de cliente y dar clic en el botón Conectar, se realiza la conexión de este cliente con el servidor y se presenta la notificación de conexión. En este caso, en el cliente uno se presenta el mensaje “Cliente uno se ha conectado”. El resultado se presenta en la siguiente figura
En el servidor se captura la información de dicho cliente. Se presenta la siguiente información en consola.
Esperando nueva conexion Conexion 1 desde: HECTOR_1 IP: 192.168.1.2 Cliente uno se ha conectado Esperando nueva conexion
Al ejecutar el segundo Cliente e ingresar un nombre de cliente y dar clic en el botón Conectar, se realiza la conexión de este cliente con el servidor y se presenta la notificación de conexión. En este caso, tanto en el cliente uno como en el cliente dos, se presenta el mensaje “Cliente dos se ha conectado”. El resultado se presenta en la siguiente figura.
En el servidor se captura la información de dicho cliente. Se presenta la siguiente información en consola.
Esperando nueva conexion Conexion 1 desde: HECTOR_1 IP: 192.168.1.2 Cliente uno se ha conectado Esperando nueva conexion Conexion 2 desde: HECTOR_2 IP: 192.168.1.3 Cliente dos se ha conectado Esperando nueva conexion
Al ejecutar el tercer Cliente e ingresar un nombre de cliente y dar clic en el botón Conectar, se realiza la conexión de este cliente con el servidor y se presenta la notificación de conexión. En este caso, tanto en el cliente uno como en el cliente dos como en el cliente tres, se presenta el mensaje “Cliente tres se ha conectado”. El resultado se presenta en la siguiente figura.
En el servidor se captura la información de dicho cliente. Se presenta la siguiente información en consola.
Esperando nueva conexion Conexion 1 desde: HECTOR_1 IP: 192.168.1.2 Cliente uno se ha conectado Esperando nueva conexion Conexion 2 desde: HECTOR_2 IP: 192.168.1.3 Cliente dos se ha conectado Esperando nueva conexion Conexion 3 desde: HECTOR_3 IP: 192.168.1.4 Cliente tres se ha conectado Esperando nueva conexion
Al colocar un mensaje en el cuadro de texto inferior del cliente uno y dar clic en enviar, el servidor recibe dicho mensaje y lo reenvía a todos los clientes conectados. En el mensaje, el servidor agregar el texto “Cliente dice: ”, donde cliente hace referencia al nombre del cliente que envía el mensaje.
En el servidor se captura el mensaje enviado por el cliente uno, se visualiza en consola y se reenvía a todos los demás clientes. Se presenta la siguiente información en consola
Esperando nueva conexion Conexion 1 desde: HECTOR_1 IP: 192.168.1.2 Cliente uno se ha conectado Esperando nueva conexion Conexion 2 desde: HECTOR_2 IP: 192.168.1.3 Cliente dos se ha conectado Esperando nueva conexion Conexion 3 desde: HECTOR_3 IP: 192.168.1.4 Cliente tres se ha conectado Esperando nueva conexion Cliente uno dice: Hola mundo
Al colocar un mensaje en el cuadro de texto inferior del cliente dos y dar clic en enviar, el servidor recibe dicho mensaje y lo reenvía a todos los clientes conectados de la misma forma que se realizó el proceso con el cliente uno.
En el servidor se captura el mensaje enviado por el cliente uno, se visualiza en consola y se reenvía a todos los demás clientes. Se presenta la siguiente información en consola.
Esperando nueva conexion Conexion 1 desde: HECTOR_1 IP: 192.168.1.2 Cliente uno se ha conectado Esperando nueva conexion Conexion 2 desde: HECTOR_2 IP: 192.168.1.3 Cliente dos se ha conectado Esperando nueva conexion Conexion 3 desde: HECTOR_3 IP: 192.168.1.4 Cliente tres se ha conectado Esperando nueva conexion Cliente uno dice: Hola mundo Cliente dos dice: Hola Planeta Tierra
Profe gracias, que buen tutorial
ResponderEliminarCon gusto
EliminarExcelente..!!!!!!!!!!!
ResponderEliminarGracias
Eliminarprofe una pregunta en caso de querer hacer la comunicación con redes remotas se debe usar IP Publicas? ¿como seria el proceso? gracias
ResponderEliminarSi se requiere IP publica.. En el cliente, simplemente se coloca dicha direccion IP publica.
Eliminarme gustaría hacer una aplicación para controlar remotamente la propiedad setEnabled de un jbutton se podrá esto?
ResponderEliminarPara este caso, el mensaje debe incluir un valor que identifique esta funcionalidad. En el ejemplo presentado solo se envían mensajes de texto. Lo mejor es crear una clase serializable (Ej. ButtonControler.java) que contenga un atributo que controle el JButton (Ej. setEnabled). En el cliente, se debe crear una instancia de dicha clase (ButtonControler) y se asigna el valor del atributo (setEnable=true). En el servidor se debe recibir el objeto serializado y se hace casting con la clase (ButtonControler). Finalmente, puede consultar el valor del atributo
Eliminar