Algoritmos y Estructuras de Datos Compiladores e Intérpretes Herramientas Lenguaje de programación
!Prog C/C++
Linux Matemáticas
Mates Discretas
Programación Orientada a Objetos Redes y Computación Distribuida Sistemas Operativos

Paso de Mensajes

[date: 23-12-2024] [last modification: 31-12-2024]
[words: 2922] [reading time: 14min] [size: 35869 bytes]

En este artículo se discutirá el paradigma de la Computación Distribuida de más bajo nivel: paso de mensajes. En UNIX, se implementa mediante la API de Sockets.

Historia

En 1972 se diseña ARPANET, la primera red de largo alcance que implementó el stack TCP/IP (enrutamiento de paquetes). Su ventaja, es que si cae un nodo, el resto puede seguir funcionando. Se creó con la intención de conectar mainframes de investigación con los investigadores, pero pasó al departamento de defensa. Años más tarde, se empezarían a añadir universidades.

Por esas fechas, la empresa AT&T crea el sistema operativo UNIX. Se distribuyó de forma Open Source entre varias universidades, que lo utilizaron para la docencia.

Para entonces, los ordenadores no eran interoperables, sus componentes no eran intercambiables, por lo que comunicar varios ordenadores de diferentes marcas era un problema.

Entonces, el departamento de defensa marca un estándar.

La universidad de Berkeley tiene una versión modificada de UNIX y necesitaba implementar dicho estándar ==> Nace la API de sockets.

Como UNIX tenía una licencia Open Source, obligaba a que esta implementación también lo fuese.

API de Sockets

TCP/IP

Como se comentó en la introducción, se diseñó para ARPANET a principios de los años 80.

Capas TCP/IP
Datos o Enlace
  • Interacción con el hardware: permite intercambiar la tarjeta de red sin afectar al resto de capas.
  • Múltiples formas de comunicación con la misma interfaz
Red
  • Proporciona un mecanismo no fiable de comunicación entre sistemas
  • Introduce las direcciones IP ==> Cada nodo tiene una dirección de 32 bits
Transporte
  • Un proceso interacciona para el envío de datos
  • Se añade el concepto de puerto para que el SO sepa a qué proceso enviar el mensaje.
  • Controla el orden de transmisión de los bytes (Big-Endian vs Little-Endian) ==> Por convención es Big-Endian.

UDP

  • Muy similar a IP: no fiable, delega a la aplicación que los datos se reciban.
  • Simple y rápido
  • La unidad mínima es el propio paquete
  • Los paquetes no están ordenados
  • No hay control de congestión
  • No hay QoS (Quality of Service): no se garantiza un ancho de banda y pueden existir retardos.

Útil para aplicaciones que requieren una alta tasa de transferencia y donde las pérdidas no son importantes (ejemplo: videoconferencia).

TCP

Orientado a conexión, lo que significa que hay que abrir un canal, enviar los datos, y luego cerrarlo (de forma similar a un archivo).

  • Garantiza la recepción en orden y correcta ==> Números de secuencia
  • Coste: gran latencia (no vale para audio/vídeo)
  • Comunicación bidireccional, pero solo 2 procesos (punto a punto)
  • Control de congestión
Aplicación

Protocolos dedicados a propósitos específicos.

  • HTTP: páginas web
  • SMTP: correo electrónico
  • FTP: transferencia de archivos

Cada una de estas capas necesita su propia cabecera, por lo que el resultado final tendrá este aspecto:

    flowchart LR
    %% 3 ~~~ a@{ shape: brace-l, label: "Puerto origen y 
destino (16 bits)" } %% 2 ~~~ b@{ shape: brace-l, label: "IP origen y destino (32 bits)" } %% 1 ~~~ c@{ shape: brace-l, label: "Dirección MAC" } 1[Cabecera Ethernet] ~~~ 2[Cabecera IP] ~~~ 3[Cabecera TCP] ~~~ 4[Datos] ~~~ 5[Fin Ethernet]

Entonces, es necesario tener los siguientes 5 elementos para que la comunicación sea posible:

ProtocoloTCP / UDP
DirecciónIP local (32 bits)
IP remota (32 bits)
Procesopuerto local (16 bits)
puerto remoto (16 bits)

Out of Band

Generalmente la transmisión de información se realiza a través de una memoria intermedia.

    flowchart LR
    1[Proceso] --> 2 --> 3[Red] --> 4 --> 5[Proceso]

    subgraph Byte stream service layer
        2[Bufer de envío]
    end

    subgraph Byte stream service layer
        4[Bufer de recepción]
    end

    style 3 fill-opacity:0, stroke-opacity:0;

Pero en algunos casos nos interesa saltarnos ese mecanismo y hacer que se envíe directamente. Con esto conseguimos que los datos de Out of Band siempre se envían antes que el resto.

    flowchart LR
    1[Proceso] --> 2 --> 3[Red] --> 4 --> 5[Proceso]
    1 --> 2oob --> 3 --> 4oob --> 5

    subgraph Byte stream service layer
        2[Bufer de envío]
        2oob[Datos Out-of-Band]
    end

    subgraph Byte stream service layer
        4[Bufer de recepción]
        4oob[Datos Out-of-Band]
    end

    style 3 fill-opacity:0, stroke-opacity:0;

IPC de UNIX BSD 4.x

A partir de BSD 4.2 se implementó un modelo de IPC a través de llamadas al sistema gracias al concepto de sockets.

Socket

Abstracción software dentro de un nodo de una red de comunicaciones que sirve como endpoint.

  1. A través del socket se envían y se reciben mensajes
  2. Para enviar, los mensajes se ponen en una cola en el socket
  3. El sistema envía
  4. Para recibir, en el socket receptor los mensajes estarán en una cola hasta que el proceso destinatario los haya extraído

Ambos procesos deben crear sockets explícitamente para poder comunicar. Es responsabilidad del programador mantener el buffer vacío, por que si se llena, será Buffer Overflow.

Por defecto, un socket es bloqueante ==> Uso de hilos.
Este comportamiento se puede cambiar, pero es difícil de implementar (interrupciones hardware).

Como en UNIX todo es un archivo, los sockets funcionan de igual forma. Se abren mediante la función socket() y se devuelve un descriptor de archivo. Con el se pueden usar las llamadas para archivos read() y write().

#include <sys/socket.h>

// Parámetro domain
#define AF_INET ...  // Internet
#define AF_UNIX ...  // IPC

// Parámetro type
#define SOCK_DGRAM  ...  // UDP
#define SOCK_STREAM ...  // TCP

// Protocol a 0

int socket(int domain, int type, int protocol);
// En error devuelve -1, consultar errno

Consultar man socket para más información.

A continuación, se llama a bind() para asignarle un puerto. Si no se deshace el bind, el sistema se podrá reutilizar el puerto (ojo si el programa termina por CTRL-C).

#include <sys/socket.h>

int bind(
    int sockfd,  // Descriptor del socket
    const struct sockaddr *addr,
    socklen_t addrlen
);
// En éxito devuelve 0. En error, -1 (errno)

El parámetro addr es la dirección que se le quiere asignar. Como se trata de una función genérica, también debe funcionar para otros protocolos y dominios. Por ello, también se le debe pasar el tamaño de la estructura.

UDP o Datagramas

==> Con un mismo socket se pueden hablar con varias máquinas
==> Primero recvfrom() (bloqueante) y luego sendto()

    sequenceDiagram
    Server ->> Server: socket()
    Server ->> Server: bind()
    Server ->> Server: recvfrom()
    Note left of Server: Bloquearse 
hasta recibir datos Client ->> Client: socket() Client ->> Client: bind() Client ->> Server: sendto() Client ->> Client: recvfrom() Note left of Server: Procesar la petición Server -->> Client: sendto()

TCP o Flujos / Streams

    sequenceDiagram
    Server ->> Server: socket()
    Server ->> Server: bind()
    Server ->> Server: listen()
    Note left of Server: Se bloquea hasta recibir conexiones

    Client ->> Client: socket()
    Client ->> Server: connect()

    create participant Connection Server
    Server ->> Connection Server: accept()
    Connection Server ->> Connection Server: recv()

    Client ->> Connection Server: send()
    Client ->> Client: recv()
    Connection Server -->> Client: send()

    destroy Connection Server
    Server ->> Connection Server: close()

Nótese que para gestionar la conexión se crea un nuevo socket, el socket de conexión. Esto se debe a que la conexión es 1 a 1.

El API de sockets en Java

Para representar las direcciones IP, en Java se utilizan las siguientes clases:

    classDiagram
    class SocketAddress
    <<abstract>> SocketAddress

    class InetSocketAddress {
        +InetSocketAddress(addr: String, port: int )
        +InetSocketAddress(addr: InetAddress, port: int)
        +getPort() int
        +getAddress() InetAddress
    }

    class InetAddress {
        +getByName(String) InetAddress$
        %%+getByAddress(byte[]) InetAddress$
        %%+isReachable() bool
        %%+isReachable(timeout: int) bool
        %%+isMulticastAddress() bool
    }

    link SocketAddress "https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/net/SocketAddress.html"
    link InetSocketAddress "https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/net/InetSocketAddress.html"
    link InetAddress "https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/net/InetAddress.html"

    SocketAddress <|-- InetSocketAddress
    SocketAddress <|-- UnixDomainSocketAddress
    InetAddress <|-- Inet4Address
    InetAddress <|-- Inet6Address

Luego, si se quiere hacer uso del protocolo UDP para enviar datagramas, se utilizan las siguientes clases:

    classDiagram
    class AutoCloseable {
        <<interface>>
        +close()
    }

    class DatagramSocket {
        +bind(addr: SocketAddress)
        +connect(addr: SocketAddress)
        +connect(addr: InetAddress, port: int)
        +disconnect()
        +setSoTimeout(time: int)

        +getInetAddress() InetAddress
        +getPort() int
        +getChannel() DatagramChannel

        +send(packet: DatagramPacket)
        +receive(packet: DatagramPacket)
    }

    class MulticastSocket {
        +joinGroup(addr: SocketAddress, netIf: NetworkInterface)
        +leaveGroup(addr: SocketAddress, netIf: NetworkInterface)
    }

    class DatagramPacket {
        +setData()
        +getData() byte[]

        +setSocketAddress(addr: SocketAddress)
        +getSocketAddress() SocketAddress

        +setAdress(addr: InetAddress)
        +getAddress() InetAddress
        +setPort(port: int)
        +getPort() int
    }

    link AutoCloseable "https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/lang/AutoCloseable.html"
    link DatagramSocket "https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/net/DatagramSocket.html"
    link MulticastSocket "https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/net/MulticastSocket.html"
    link DatagramPacket "https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/net/DatagramPacket.html"

    DatagramSocket ..|> AutoCloseable
    DatagramSocket <|-- MulticastSocket
    DatagramSocket ..> DatagramPacket: Envía/Recibe

Alternativamente, también existen más clases para gestionar sockets por Streams (TCP).

Para recibir y enviar datos, se pueden usar las interfaces de InputStream y OutputStream. Otra alternativa es usar Channels.

Ambas clases utilizan SocketImpl, que es la clase real que implementa los sockets. El usuario puede crear su propia versión, tanto para clientes como para servidores.

    classDiagram
    class Closeable {
        <<interface>>
        +close()
    }
    class AutoCloseable {
        <<interface>>
        +close()
    }

    class Socket {
        +connect()
        +bind(addr: SocketAddress)
        +connect(addr: SocketAddress)

        +setSoTimeout(time: int)
        +getChannel() SocketChannel

        +getInputStream() InputStream
        +getOutputStream() OutputStream
    }

    class ServerSocket {
        +accept() Socket
        +bind(addr: SocketAddress)
        +getChannel() SocketChannel
    }

    link AutoCloseable "https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/lang/AutoCloseable.html"
    link Socket "https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/net/Socket.html"
    link ServerSocket "https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/net/ServerSocket.html"

    Socket ..|> Closeable
    ServerSocket ..|> Closeable
    ServerSocket ..|> AutoCloseable
    Socket <|-- SSLSocket
    ServerSocket <|-- SSLServerSocket
    ServerSocket ..> Socket: Crea

Sockets seguros: JSSE

En el diagrama de clases anterior, he incluido SSLSocket y SSLServerSocket. Estas son las implementaciones de los sockets seguros de Java: Java Secure Socket Extension (JSSE).

Ejemplo de servidor TCP

import java.io.*;
import java.net.*;

public class Server {
    public static final int PORT = 8180;

    public static void main(String[] args) {
        try (ServerSocket socket = new ServerSocket(PORT)) {

            while (true) {
                // Se realiza el accept de una conexión
                try (Socket conSocket = socket.accept()) {

                    // Este trabajo lo podría realizar otro hilo para gestionar
                    // varias conexiones a la vez.

                    DataInputStream input = new DataInputStream(conSocket.getInputStream());
                    DataOutputStream output = new DataOutputStream(conSocket.getOutputStream());

                    ... input.readByte() ...
                    ... output.write() ...
                } // Se cierra automáticamente el socket de conexión
            }

        } catch (Exception e) {
            e.printStrackTrace();
        }
    }
}

Ejemplo de cliente TCP

import java.io.*;
import java.net.*;


public class Client {
    public static void main(String[] args) {
        try (Socket socket = new Socket("127.0.0.1", Server.PORT)) {

            DataInputStream input = new DataInputStream(socket.getInputStream());
            DataOutputStream output = new DataOutputStream(socket.getOutputStream());

            ... input.readByte() ...
            ... output.write() ...

        } catch (Exception e) {
            e.printStrackTrace();
        }
    }
}

Multicast

La multidifusión se construye sobre el protocolo IP para permitir que un emisor se envíe un único paquete a muchos receptores.

Para poder recibir mensajes, es necesario registrarse a un grupo multicast, que se corresponde con una IPv4 de tipo D. Sencillamente, hace que el Sistema Operativo lea de unas IPs en concreto cuando se registra. Esto tiene la ventaja de que la asignación es dinámica: puedo dejar de escuchar o empezar a escuchar cuando quiera.

Con este mecanismo, es posible enviar datos a un grupo multicast sin pertenecer a él. Solo es necesario unirse para recibir mensajes.

Más características:

Routers multicast

Los routers solo implementan 2 capas del stack TCP/IP (Enlace y Red), por lo que necesitan algo extra para soportar multicast.

Diagrama de un paquete multicast por varias redes conectados por un router

Diagrama de un paquete multicast por varias redes conectados por un router

Entonces, cuando los routers detecten una IP dirigida a un grupo multicast, lo reenviarán por todas sus salidas los paquetes multicast. Esto provoca que el paquete viajará por toda la red, y posiblemente se quede atrapado en bucles.

La solución es implementar el mecanismo de TTL (Time To Live). El paquete se envía con un número de saltos máximo y cada vez que pase por un router, este lo decrementa en 1. Cuando se llegue a 0, el paquete simplemente se descarta.

Reservar una dirección

Cuando se crea un grupo temporal, se necesita una dirección IP multicast libre para evitar conflictos. El protocolo IP no resuelve este problema.

Tenemos la ventaja de que si se selecciona un TTL pequeño, es difícil entrar en conflicto con otros grupos.

Sin embargo, para comunicaciones a nivel de internet, sí es necesario reservar previamente una dirección con las Autoridades de Internet. El programa de directorio de sesiones (sd) sirve para arrancar o unirse a grupos multicast grandes, entre otros. En España, la agenda la gestiona RedIris.

Las direcciones de 244.0.0.1 a 244.0.0.255 están reservadas para grupos permanentes, incluso si no tienen ningún miembro.

Aplicaciones

Los principales usos de este mecanismo son los siguientes:

Sin embargo, hoy en día ya no se utiliza a escala global y casi nadie configura routers en modo multicast.

Ejemplo de uso de multicast

Ejemplo de uso de multicast

Hoy en día, el emisor se conecta a un servidor y se replican los paquetes de vídeo para los espectadores. Esto es menos eficiente dado que tiene un mayor consumo de ancho de banda, pero con el hardware actual, no resulta un problema.

Anterior: Paradigmas de la Computación Distribuida Volver a Redes y Computación Distribuida Siguiente: Hilos en Java