Introducción a la Comunicación en Red y Sockets
La comunicación en red es un pilar fundamental en el desarrollo de software moderno. Permite que aplicaciones ejecutándose en diferentes dispositivos (o en el mismo dispositivo) intercambien información, posibilitando desde la navegación web hasta los juegos multijugador en tiempo real.
Escenarios que precisan comunicación en red
La necesidad de comunicar procesos a través de una red surge en múltiples escenarios:
- Sistemas Distribuidos: Aplicaciones divididas en componentes que se ejecutan en diferentes máquinas para mejorar el rendimiento y la escalabilidad.
- Acceso a Recursos Remotos: Aplicaciones que necesitan acceder a bases de datos, sistemas de archivos o servicios alojados en servidores remotos.
- Comunicación Interpersonal: Aplicaciones de mensajería, correo electrónico, videoconferencia y redes sociales.
- Internet de las Cosas (IoT): Dispositivos sensores y actuadores que envían datos a servidores centrales para su procesamiento.
- Juegos Multijugador: Sincronización del estado del juego entre múltiples jugadores conectados simultáneamente.
Arquitectura Cliente-Servidor
El modelo más común para la comunicación en red es la arquitectura Cliente-Servidor. En este modelo, los roles están claramente definidos:
El Servidor
Es el proceso que ofrece un servicio y espera pasivamente a que lleguen peticiones.
- Rol Pasivo: Inicialmente está a la escucha (listening) en un puerto específico.
- Funciones: Procesa las peticiones de los clientes, realiza las operaciones necesarias (acceso a BD, cálculos, etc.) y envía una respuesta.
- Ejemplo: Un servidor web (Apache, Nginx) esperando peticiones HTTP.
El Cliente
Es el proceso que inicia la comunicación solicitando un servicio.
- Rol Activo: Conoce la dirección IP y el puerto del servidor e inicia la conexión.
- Funciones: Envía una solicitud al servidor y espera la respuesta para procesarla y presentarla al usuario.
- Ejemplo: Un navegador web (Chrome, Firefox) solicitando una página web.
Aunque el modelo Cliente-Servidor es predominante, existe también el modelo P2P (Peer-to-Peer), donde todos los nodos actúan simultáneamente como clientes y servidores, compartiendo recursos directamente entre ellos sin un servidor centralizado.
Concepto de Socket
Un Socket (enchufe) es una abstracción software que sirve como punto final de una comunicación entre dos procesos. Permite a los programas enviar y recibir datos a través de la red como si estuvieran escribiendo o leyendo de un archivo.
Para que dos procesos se comuniquen, cada uno debe tener un socket. La conexión se define por la pareja de sockets:
- Dirección IP: Identifica al host (máquina) en la red.
- Puerto: Identifica al proceso o aplicación específica dentro del host.
Tipos de Sockets: TCP vs UDP
Los sockets se clasifican principalmente según el protocolo de transporte que utilizan:
Sockets de Flujo (Stream Sockets) - TCP
Utilizan el protocolo TCP (Transmission Control Protocol).
- Orientados a conexión: Se debe establecer una conexión antes de transmitir datos (handshake).
- Fiables: Garantizan que los datos llegan sin errores y en el mismo orden en que se enviaron. Si un paquete se pierde, se retransmite.
- Uso: Transferencia de archivos, correo electrónico, navegación web (HTTP), donde la integridad de los datos es crucial.
Sockets de Datagrama (Datagram Sockets) - UDP
Utilizan el protocolo UDP (User Datagram Protocol).
- No orientados a conexión: Se envían paquetes (datagramas) sin establecer una conexión previa.
- No fiables: No garantizan la entrega, ni el orden, ni la integridad de los datos. Son "best-effort".
- Rápidos: Al tener menos sobrecarga de control, son más rápidos y eficientes.
- Uso: Streaming de video/audio, juegos online en tiempo real, DNS, donde la velocidad es prioritaria sobre la pérdida ocasional de datos.
| Característica | TCP (Stream) | UDP (Datagram) |
|---|---|---|
| Conexión | Requiere conexión previa | Sin conexión |
| Fiabilidad | Alta (garantiza entrega y orden) | Baja (puede haber pérdidas o desorden) |
| Velocidad | Menor (mayor sobrecarga) | Mayor (menor sobrecarga) |
| Tipo de transmisión | Flujo continuo de bytes | Paquetes independientes |
Programación en Red en Kotlin
Kotlin, al ejecutarse sobre la JVM (Java Virtual Machine), utiliza las potentes librerías de red de Java (java.net). Esto nos permite crear aplicaciones de red robustas aprovechando la sintaxis concisa de Kotlin.
Clases y Métodos Principales en java.net
A continuación, detallamos las clases fundamentales que utilizaremos, sus métodos más importantes y ejemplos de uso.
1. java.net.InetAddress
Representa una dirección IP (IPv4 o IPv6). No tiene constructores públicos; se usan métodos estáticos para obtener instancias.
- Métodos clave:
getByName(host: String): Obtiene la IP a partir de un nombre de host (ej. "google.com") o string de IP.getLocalHost(): Devuelve la IP de la máquina local.hostAddress: Propiedad que devuelve la IP en formato String (ej. "192.168.1.5").isReachable(timeout: Int): Comprueba si la dirección es accesible (similar a un ping).
val google = InetAddress.getByName("google.com")
println("IP de Google: ${google.hostAddress}")
val local = InetAddress.getLocalHost()
println("Mi IP: ${local.hostAddress}")
2. java.net.Socket (Cliente TCP)
Implementa el lado del cliente en una conexión TCP.
- Constructor:
Socket(host: String, port: Int)intenta conectar inmediatamente. - Métodos clave:
getInputStream(): Devuelve unInputStreampara leer datos del servidor.getOutputStream(): Devuelve unOutputStreampara enviar datos al servidor.close(): Cierra la conexión.isConnected: Verifica si la conexión está establecida.
val socket = Socket("localhost", 5000)
// Escribir datos
socket.getOutputStream().write("Hola".toByteArray())
3. java.net.ServerSocket (Servidor TCP)
Escucha peticiones de conexión entrantes y crea un Socket para comunicarse con cada cliente.
- Constructor:
ServerSocket(port: Int)inicia la escucha en el puerto indicado. - Métodos clave:
accept(): Bloquea la ejecución hasta que llega un cliente. Devuelve un objetoSocketnuevo conectado a ese cliente.close(): Deja de escuchar en el puerto.
val server = ServerSocket(5000)
println("Esperando clientes...")
val clientSocket = server.accept() // Se detiene aquí hasta que alguien conecta
println("Cliente conectado desde ${clientSocket.inetAddress}")
4. java.net.DatagramSocket (UDP)
Se usa tanto para enviar como para recibir paquetes UDP. No mantiene una conexión persistente.
- Constructores:
DatagramSocket(): Para un cliente (asigna un puerto aleatorio disponible).DatagramSocket(port: Int): Para un servidor (escucha en un puerto fijo).
- Métodos clave:
send(packet: DatagramPacket): Envía un paquete.receive(packet: DatagramPacket): Bloquea hasta recibir un paquete y llena el buffer del argumento.soTimeout: Establece un tiempo máximo de espera para elreceive.
5. java.net.DatagramPacket (Paquete UDP)
Contenedor de datos para UDP. Contiene los datos (bytes), la longitud, y (para enviar) la dirección IP y puerto de destino.
- Constructores:
- Para recibir:
DatagramPacket(buffer: ByteArray, length: Int) - Para enviar:
DatagramPacket(data: ByteArray, length: Int, address: InetAddress, port: Int)
- Para recibir:
- Propiedades:
data,length,address,port.
// Preparar datos para enviar
val mensaje = "Hola UDP".toByteArray()
val destino = InetAddress.getByName("localhost")
val paqueteEnvio = DatagramPacket(mensaje, mensaje.size, destino, 5000)
// Preparar contenedor para recibir
val buffer = ByteArray(1024)
val paqueteRecepcion = DatagramPacket(buffer, buffer.size)
Ejemplo conceptual en Kotlin
Aunque profundizaremos en el código en las siguientes secciones, aquí tenéis un ejemplo de cómo se instancia un socket cliente en Kotlin:
import java.net.Socket
import java.io.PrintWriter
import java.util.Scanner
fun main() {
// Intentar conectar al servidor en localhost, puerto 1234
try {
val socket = Socket("localhost", 1234)
println("Conectado al servidor!")
// Obtener flujos de entrada y salida
val output = PrintWriter(socket.getOutputStream(), true)
val input = Scanner(socket.getInputStream())
// Enviar mensaje
output.println("Hola Servidor")
// Cerrar conexión
socket.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
En los próximos apartados veremos cómo implementar servidores y clientes completos, manejar múltiples conexiones y utilizar hilos para la concurrencia.