5 Sockets orientados a conexión¶
Sockets orientados a conexión¶
Los sockets orientados a conexión o streams sockets son aquellos que utilizan el protocolo TCP para la comunicación. Al emplear dicho protocolo, se garantiza la entrega de los mensajes en el orden en el que fueron enviados. En el protocolo TCP los mensajes llevan un acuse de recibo, de manera que si el emisor no recibe el acuse de recibo de un determinado mensaje, vuelve a transmitirlo.
Una vez establecida la conexión los procesos pueden intercambiar mensajes a través de un flujo o stream bidireccional de conexión (full-duplex) con acceso secuencial, sin tener que volver a preocuparse de la dirección IP o el puerto al que se ha realizado la conexión.
En los stream sockets el servidor suele seguir los siguientes pasos:
Algoritmo del servidor
- Inicia el servicio, poniéndose a la espera de que algún cliente se conecte (listen).
- Cuando un cliente solicita la conexión al servicio, el servidor recibe la solicitud y la acepta (accept).
- Se establece un canal de comunicación bidireccional entre los conectores de ambas aplicaciones o procesos. En un bucle, el servidor recibe una petición de información desde el cliente (receive), la procesa y le envía la respuesta (send).
- El servidor cierra el servicio en algún momento (close).
Los pasos 2 y 3 pueden llevarse a cabo de manera simultánea con varios clientes, mediante el uso de distintos hilos de ejecución.
El cliente, por su parte, suele llevar a cabo las siguientes acciones:
Algoritmo del servidor
- Solicita la conexión al servicio ofrecido por el servidor (debe conocer de antemano su dirección y su puerto) (connect).
- Una vez aceptada la solicitud de conexión, en un bucle, envía una petición de información al servidor (send), recibe la respuesta (receive) y la procesa.
- Cuando ya no requiere de más información, cierra la conexión con el servidor (close).
En la siguiente imagen se muestra el esquema de funcionamiento de una aplicación cliente/servidor en la que el servidor es capaz de gestionar simultáneamente varios clientes:
El paquete java.net
proporciona dos clases principales para trabajar desde Java con sockets orientados a conexión: la clase ServerSocket
, para crear el conector en el lado del servidor, y la clase Socket
para solicitar una conexión desde el cliente, o para representar en el servidor una conexión establecida.
Los pasos que debe seguir el servidor son los siguientes:
Algoritmo detallado del servidor
- Crear un objeto
ServerSocket
llamando a su constructorServerSocket(port)
, proporcionándole el puerto por el que desea a recibir las peticiones. - Utilizar el método
accept()
del objetoServerSocket
para indicar que queda a la espera de que algún cliente solicite conectarse a él. Se trata de un método síncrono bloqueante, que hace que el hilo de ejecución del servidor quede bloqueado hasta que el cliente solicite la conexión mediante la creación de un objetoSocket
que apunte a la dirección del servidor y al puerto establecido. Cuando esto sucede, se crea la conexión y el métodoaccept()
retorna un objetoSocket
que representa el conector de la conexión en el lado del servidor. - Si queremos que el servidor pueda atender simultáneamente a varios clientes es necesario que, una vez establecida la conexión, se traslade la gestión de la comunicación con un cliente a un hilo de ejecución distinto, de manera que el hilo principal del servidor pueda dedicarse a aceptar más clientes. De esta manera, la comunicación con cada cliente se realizará desde un hilo independiente.
- En el hilo de comunicación con el cliente, el servidor puede utilizar el método
getOutputStream()
del objetoSocket
para obtener el flujo de salida desde el servidor, de manera que pueda enviarle datos al cliente, y el métodogetInputStream()
para obtener el flujo de entrada al servidor, de manera que pueda recibir datos del cliente. - Para leer o escribir en dichos flujos se utilizarán los objetos lectores y escritores habituales, como
DataInputStream
yDataOutputStream
,BufferedReader
yPrintWriter
, oObjectInputStream
yObjectOutputStream
(en cuyo caso la clase de los objetos deberá implementar la interfazSerializable
). Si se trata de leer del flujo de un socket que ya ha sido cerrado por el cliente, se producirá una exceptionIOException
. - Cuando ya no desee leer o escribir en el socket correspondiente el servidor deberá cerrar el lector y el escritor llamando a sus métodos
close()
. Finalmente para cerrar la conexión con el cliente, el servidor puede ejecutar el métodoclose()
del objetoSocket
. Con esta operación, normalmente el hilo de gestión de la comunicación con el cliente finalizará su ejecución. - Para no aceptar más peticiones de conexión con clientes, el servidor ejecutará el método
close()
del objetoServerSocket
.
Proyecto StreamSocket¶
En este proyecto vamos a realizar una aplicación de demostración en el que el hilo principal lanza por un lado un hilo servidor, y por otro lado 10 hilos cliente que tratarán de conectarse al servidor. Lo hacemos con hilos servidor y clientes por tenerlo todo en el mismo proyecto, pero en la realidad correspondería a aplicaciones independientes.
Cada vez que el servidor acepte una conexión con un cliente se creará un nuevo hilo de gestión de la comunicación con dicho cliente.
La comunicación entre el cliente y el servidor tendrá el siguiente protocolo:
- Nada más establecerse la conexión, el cliente enviará un mensaje de saludo al servidor en forma de objeto de la clase
Message
, que contendrá un atributo con el autor y otro con el contenido del mensaje. - El servidor al establecerse la conexión entra en un bucle de lectura de mensajes del cliente, que termina cuando el mensaje recibido sea
null
o se produzca unaIOException
porque el cliente haya cerrado la conexión. Al terminar la conexión el servidor cerrará el socket correspondiente. - Por cada mensaje de saludo recibido por el servidor, éste lo muestra por pantalla y envía al cliente un mensaje de confirmación de que ha recibido el mensaje de saludo.
- El cliente, una vez enviado el mensaje de saludo, se pone a leer el mensaje de confirmación de recepción del mensaje que deberá mandar el servidor. Cuando lo reciba, lo mostrará por pantalla.
- Finalmente el cliente cerrará la comunicación, dado que tan sólo quería saludar.
Veamos en primer lugar el código del hilo principal, representado por la clase Main
:
public class Main {
private static final int SERVER_PORT = 60000;
private static final int NUMBER_OF_CLIENTS = 10;
public static void main(String[] args) {
String serverAddress = "localhost";
Thread server = new Thread(new Server(SERVER_PORT));
server.start();
Thread[] clients = new Thread[NUMBER_OF_CLIENTS];
for (int i = 0; i < NUMBER_OF_CLIENTS; i++) {
clients[i] = new Thread(new Client(i + 1, serverAddress, SERVER_PORT));
clients[i].start();
}
}
}
La clase Server
representa el comportamiento del servidor. Cada conexión será identificada por un número entero, que se va incrementando cada vez que se establece una conexión con un cliente.
Usamos la estructura try-with-resources, para que el serverSocket sea cerrado automáticamente incluso si se produce una excepción.
La llamada al método accept()
es bloqueante, por lo que el hilo servidor queda a la espera de que algún hilo cliente establezca un socket para su dirección IP y el puerto correspondiente. Una vez establecida la conexión, con objeto de que el servidor pueda aceptar más clientes inmediatamente, se crea un nuevo hilo de gestión de la conexión y se le pasa el objeto Socket
retornado por accept()
, que representa la conexión establecida. También se le pasa el número de conexión que la identifica.
public class Server implements Runnable {
private final int port;
private int connections = 0;
public Server(int port) {
this.port = port;
}
@Override
public void run() {
try (ServerSocket serverSocket = new ServerSocket(port)) {
while(!Thread.currentThread().isInterrupted()) {
Socket socket = serverSocket.accept();
new Thread(new ServerConnection(++connections, socket)).start();
}
} catch (IOException e) {
System.out.println("Server: Input / Output error in server socket");
}
}
}
Veamos ahora la clase Client
, que representa el cliente que se quiere conectar al servidor. Su constructor recibe el número de cliente que servirá para identificarlo, la dirección IP y el puerto del servidor con el que se debe conectar.
Para simular el tiempo que tarda el cliente en escribir el mensaje o el tiempo que tarde en cerrar la conexión, usaremos un valor aleatorio de 1 a 10 segundos.
public class Client implements Runnable {
private final int clientNumber;
private final String serverAddress;
private final int serverPort;
public Client(int clientNumber, String serverAddress, int serverPort) {
this.clientNumber = clientNumber;
this.serverAddress = serverAddress;
this.serverPort = serverPort;
}
@Override
public void run() {
try (Socket socket = new Socket(serverAddress, serverPort);
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream input = new ObjectInputStream(socket.getInputStream())
) {
sendMessage(output);
receiveMessage(input);
closeConnection();
} catch (IOException e) {
showConnectionError();
} catch (ClassNotFoundException e) {
showMessageFormatError();
} catch (InterruptedException ignored) {
}
}
private void sendMessage(ObjectOutputStream output)
throws InterruptedException, IOException {
Thread.sleep(ThreadLocalRandom.current().nextInt(1, 10) * 1000);
output.writeObject(new Message("Client #" + clientNumber, "Hello from client " +
clientNumber));
}
private void receiveMessage(ObjectInputStream input)
throws IOException, ClassNotFoundException {
Message message = (Message) input.readObject();
System.out.printf("Client #%d - Message from %s: %s\n", clientNumber,
message.getAuthor(), message.getContent());
}
private void closeConnection() throws InterruptedException {
Thread.sleep(ThreadLocalRandom.current().nextInt(1, 10) * 1000);
}
private void showConnectionError() {
System.out.printf("Client #%d: Can't connect with server in %s:%d\n",
clientNumber, serverAddress, serverPort);
}
private void showMessageFormatError() {
System.out.printf("Client #%d: Incorrect message format\n", clientNumber);
}
}
La clase Message
es una clase POJO, que debe implementar Serializable, dado que vamos a usarla con ObjectOutputStream
y ObjectInputStream
, y que contiene información sobre el autor del mensaje y sobre su contenido:
public class Message implements Serializable {
private final String author;
private final String content;
public Message(String author, String content) {
this.author = author;
this.content = content;
}
public String getAuthor() {
return author;
}
public String getContent() {
return content;
}
}
Finalmente, la clase ServerConnection
representa la conexión establecida entre un cliente y un servidor, desde el punto de vista del servidor.
class ServerConnection implements Runnable {
private final int connectionNumber;
private final Socket socket;
ServerConnection(int connectionNumber, Socket socket) {
this.connectionNumber = connectionNumber;
this.socket = socket;
}
@Override
public void run() {
try (ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream input = new ObjectInputStream(socket.getInputStream())
) {
Message receivedMessage;
while ((receivedMessage = receiveMessage(input)) != null){
sendConfirmationMessage(output, receivedMessage);
}
} catch (IOException e) {
showConnectionClosed();
} catch (ClassNotFoundException e) {
showMessageFormatError();
} catch (InterruptedException ignored) {
}
}
private Message receiveMessage(ObjectInputStream input)
throws IOException, ClassNotFoundException {
Message message = (Message) input.readObject();
if (message != null) {
System.out.printf("Server - Message from %s: %s\n", message.getAuthor(),
message.getContent());
}
return message;
}
private void sendConfirmationMessage(ObjectOutputStream output, Message message)
throws IOException, InterruptedException {
Thread.sleep(ThreadLocalRandom.current().nextInt(1, 10) * 1000);
output.writeObject(new Message("Server", "Received message from " +
message.getAuthor() + ": " +
message.getContent()));
}
private void showConnectionClosed() {
System.out.printf("Server: connection #%d closed\n", connectionNumber);
}
private void showMessageFormatError() {
System.out.printf("Server: Incorrect message format in connection #%d\n",
connectionNumber);
}
}