Capa de transporte — TCP y UDP

"IP entrega cartas en la calle correcta. TCP las entrega a la persona correcta dentro de la casa."

Qué vas a aprender en este capítulo

La capa de transporte lleva los datos del proceso origen al proceso destino — porque IP solo te lleva del host al host. Vas a aprender los dos protocolos dominantes: TCP (confiable, ordenado, orientado a conexión) y UDP (rápido, sin garantías). Vas a entender el famoso three-way handshake, control de congestión, y vas a escribir tu primer socket TCP en Python.

4.1 La idea: del host al proceso

💡 Intuición

IP entrega un paquete a una máquina (por su IP). Pero adentro de esa máquina hay muchos procesos: un servidor web, otro de email, un cliente de Spotify, un cliente de Telegram. ¿Cómo sabe el SO a cuál entregarle el paquete?

Cada proceso de red se identifica por un puerto — un número de 16 bits (0655350-65535). Junto con la IP, el par (IP, puerto) identifica únicamente un endpoint en Internet.

Ejemplos:

  • 142.250.10.139:443 = Google, servicio HTTPS.
  • 8.8.8.8:53 = Google DNS.
  • tu-IP:50000 = una conexión saliente cualquiera.

Capa de transporte aporta:

  • Multiplexión / demultiplexión por puertos.
  • Confiabilidad (TCP) o no (UDP).
  • Control de flujo (no inundes al receptor).
  • Control de congestión (no inundes la red).

Dependiendo de qué necesite la aplicación, elige TCP o UDP.

4.2 Puertos

📐 Fundamento

Rango total: 0 a 65535 (16 bits).

Tres rangos:

  • Well-known ports (0-1023): servicios estándar. Requieren privilegios de root para escuchar (en Linux/Unix).
    • 22 = SSH
    • 25 = SMTP
    • 53 = DNS
    • 80 = HTTP
    • 443 = HTTPS
    • 3306 = MySQL
  • Registered ports (1024-49151): asignados a aplicaciones específicas, pero no estándar oficial.
    • 8080 = HTTP alternativo
    • 8443 = HTTPS alternativo
    • 5432 = PostgreSQL
    • 6379 = Redis
  • Ephemeral ports (49152-65535): asignados dinámicamente para conexiones salientes.

Cuando un cliente abre una conexión:

Cliente            Servidor
---------          --------
49500       →      80 (HTTP)

El cliente usa un puerto efímero elegido por el SO (49500). El servidor escucha en su puerto fijo (80).

Ver puertos en uso:

$ ss -tulpn       # listening
$ ss -tn          # connected

Esto te muestra qué procesos tienen conexiones abiertas y a qué.

4.3 UDP — datagramas simples

📐 Fundamento

UDP (User Datagram Protocol) es el protocolo más simple: mandá y rezá.

Cabecera UDP (8 bytes):

+----------+-------------+
| puerto origen | puerto destino |
| longitud | checksum |
+----------+-------------+

Solo eso. Sin sequence numbers, sin handshake, sin garantía de entrega.

Características:

  • Sin conexión. Cada datagrama es independiente.
  • Sin orden. Los paquetes pueden llegar desordenados.
  • Sin retransmisión. Si se pierde, problema del programador.
  • Velocidad. Mucho menor overhead que TCP.

Cuándo elegir UDP:

  • DNS. Pregunta corta, respuesta corta. Si se pierde, retry.
  • Streaming en vivo. Mejor un frame perdido que un retraso.
  • Juegos en tiempo real. Latencia importa más que confiabilidad.
  • VoIP. Igual que streaming.
  • Multicast / broadcast. TCP no soporta múltiples receptores.

Ejemplo en Python:

import socket

# Servidor UDP
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(('', 9999))
data, addr = s.recvfrom(1024)
print(f"Recibido de {addr}: {data}")
s.sendto(b"Hola desde el servidor", addr)
# Cliente UDP
c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
c.sendto(b"Hola servidor", ('localhost', 9999))
respuesta, _ = c.recvfrom(1024)
print(respuesta)

Notá la simplicidad — sin connect, sin accept. Mandás y recibís.

QUIC (HTTP/3 corre sobre QUIC) usa UDP por debajo y reimplementa confiabilidad en user space. Combina lo mejor de ambos mundos.

4.4 TCP — el caballo de batalla

📐 Fundamento

TCP (Transmission Control Protocol). Más complejo, más útil. Garantías:

  1. Entrega confiable. Si no llega, retransmite.
  2. Orden. Los datos llegan en el orden enviado.
  3. Sin duplicados. Cada byte se entrega una sola vez.
  4. Control de flujo. No inunda al receptor.
  5. Control de congestión. No inunda la red.

A cambio: más overhead (handshakes, ACKs, ventanas).

Cabecera TCP (20+ bytes):

+---------+----------+
| puerto origen | puerto destino |
+---------+----------+
| Número de secuencia (4 bytes) |
+---------+----------+
| Número de ACK (4 bytes) |
+---------+----------+
| Offset | Reserved | Flags | Window |
+---------+----------+
| Checksum | Urgent pointer |
+---------+----------+
| Opciones (variable)              |
+---------+----------+
| Datos (variable)                  |
+---------+----------+

Flags clave:

  • SYN — inicio de conexión.
  • ACK — confirmación.
  • FIN — cerrar conexión.
  • RST — reset / cancelar.
  • PSH — enviar datos ya (sin esperar buffer lleno).

Three-way handshake

Para abrir una conexión TCP, tres mensajes:

Cliente              Servidor
   |                    |
   |─── SYN, seq=100 ──>|
   |                    |
   |<─── SYN-ACK, seq=500, ack=101 ──|
   |                    |
   |─── ACK, seq=101, ack=501 ──>|
   |                    |
   |── conexión abierta ──|
  1. Cliente: "quiero abrir, mi sec inicial es 100".
  2. Servidor: "OK, mi sec es 500, tu ACK es 101 (siguiente que espero)".
  3. Cliente: "OK, recibido, sigo de 101".

Después los datos fluyen.

Cierre — four-way handshake

Cliente             Servidor
   |─── FIN ─────────>│
   |<──── ACK ──────│
   |              (servidor termina de mandar lo que tenía)
   |<──── FIN ────│
   |─── ACK ─────────>│
   | conexión cerrada│

Asimétrico, cada lado cierra independientemente. Hay un estado intermedio donde una dirección está cerrada y la otra no.

Números de secuencia

Cada byte tiene un número de secuencia. El cliente y servidor usan números independientes (uno para cada dirección).

¿Por qué empezar en un número aleatorio? Para evitar ataques de inyección y confusión con conexiones previas. Los SO modernos usan ISN (Initial Sequence Number) aleatorio.

4.5 Confiabilidad: ACKs y retransmisión

📐 Fundamento

Cada byte enviado debe ser confirmado (ACKed). Si no llega ACK en cierto tiempo, retransmitir.

Cumulative ACK: un ACK con número NN significa "recibí todos los bytes hasta N1N-1, esperando NN".

Cliente envía:    seq=100, len=20  →
Servidor responde:                  ←  ACK=120
Cliente envía:    seq=120, len=30  →
Servidor responde:                  ←  ACK=150

Retransmisión:

Cliente envía:    seq=100, len=20  →   (se pierde)
Cliente envía:    seq=120, len=30  →
Servidor responde:                  ←  ACK=100  (todavía espera 100)
Cliente: ¡pérdida! retransmite seq=100

Selective ACK (SACK): mejora moderna. El receptor indica explícitamente rangos que recibió fuera de orden, así el emisor solo retransmite lo perdido.

RTT y timeout

RTT (Round-Trip Time) = tiempo de ida y vuelta. Variable según red.

El timeout para retransmitir se calcula adaptativamente:

RTO=SRTT+4RTTVARRTO = SRTT + 4 \cdot RTTVAR

Donde SRTT es el RTT suavizado y RTTVAR su variación. Adaptativo — si la red está lenta, el timeout crece.

4.6 Control de flujo y congestión

📐 Fundamento

Dos problemas distintos que TCP resuelve:

Control de flujo

¿Y si el receptor no puede procesar tan rápido como el emisor envía? Buffers se llenan, datos se pierden.

Ventana de recepción (rwnd): el receptor anuncia en cada ACK "tengo X bytes de espacio libre". El emisor no envía más que eso sin ACK.

Receptor:    "rwnd=8000"
Emisor manda:  bytes 1000-5000 (5KB enviados, dentro de rwnd)
Receptor procesa, ACK + nueva rwnd:
              "rwnd=10000"
Emisor manda más...

Si rwnd llega a 0, el emisor espera. Cuando el buffer se libera, el receptor avisa.

Control de congestión

¿Y si la red interna se satura? Routers descartan paquetes. Mil clientes mandando full speed colapsan el backbone.

Ventana de congestión (cwnd): el emisor mantiene una segunda ventana que estima cuánto puede mandar sin saturar la red.

Algoritmos clásicos:

Slow start: empezar con cwnd pequeño (1 segmento), doblar cada RTT mientras todo va bien. Crece exponencialmente.

Congestion avoidance: una vez detecta congestión, crece linealmente.

Detección de pérdida = señal de congestión:

  • 3 ACKs duplicados: retransmite ese segmento; cwnd a la mitad (Fast retransmit + fast recovery).
  • Timeout: cwnd a 1, vuelve a slow start.

Algoritmos modernos:

  • CUBIC (default Linux). Crece más rápido que el clásico tras pérdida.
  • BBR (Google). Modela explícitamente bandwidth y RTT — anda mejor en redes con buffers grandes.

El emisor envía min(rwnd,cwnd)\min(rwnd, cwnd) bytes en vuelo

Toma la más restrictiva:

  • Si el receptor está lento → flow control limita.
  • Si la red está saturada → congestion control limita.

Eso es lo más fascinante de TCP: dos algoritmos colaborando, sin coordinación central, mantienen Internet andando.

4.7 Sockets en Python

🛠️ En la práctica

Servidor TCP simple:

import socket

HOST, PORT = '', 8080

# AF_INET = IPv4, SOCK_STREAM = TCP
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))
s.listen(5)
print(f"Escuchando en puerto {PORT}...")

while True:
    cliente, addr = s.accept()
    print(f"Conexión de {addr}")
    cliente.send(b"Hola, mundo!\n")
    data = cliente.recv(1024)
    print(f"Cliente dijo: {data}")
    cliente.close()

Cliente TCP:

import socket

c = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
c.connect(('localhost', 8080))
respuesta = c.recv(1024)
print(respuesta)
c.send(b"Gracias!")
c.close()

Conceptos clave:

  • socket(AF_INET, SOCK_STREAM): crea un endpoint TCP/IPv4.
  • bind: asocia el socket a una IP/puerto.
  • listen(n): marca el socket como pasivo, con una cola de tamaño n.
  • accept: bloquea hasta que llega una conexión, devuelve un nuevo socket dedicado.
  • connect: abre conexión (cliente).
  • send/recv: intercambio.
  • close: cerrar.

Servir múltiples clientes

El servidor anterior solo atiende uno a la vez. Para múltiples:

Threads:

import socket, threading

def atender(cliente, addr):
    cliente.send(b"hola\n")
    cliente.close()

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('', 8080))
s.listen(5)
while True:
    cliente, addr = s.accept()
    t = threading.Thread(target=atender, args=(cliente, addr))
    t.start()

Async I/O:

import asyncio

async def atender(reader, writer):
    writer.write(b"hola async\n")
    await writer.drain()
    writer.close()

async def main():
    server = await asyncio.start_server(atender, '', 8080)
    async with server:
        await server.serve_forever()

asyncio.run(main())

Async escala a miles de conexiones por proceso (sin overhead de threads).

netcat — el cuchillo suizo

$ nc -lvp 8080      # escuchá en 8080

En otra terminal:

$ nc localhost 8080

Lo que escribas en cada lado, llega al otro. Útil para testear protocolos y como base para herramientas más complejas.

4.8 Diagnóstico — ss y tcpdump

🛠️ En la práctica

Conexiones activas:

$ ss -tn
State       Recv-Q   Send-Q       Local Address:Port      Peer Address:Port
ESTAB       0        0            192.168.1.10:50050      142.250.x.x:443
ESTAB       0        0            192.168.1.10:50051      151.101.x.x:443
TIME-WAIT   0        0            192.168.1.10:49999      8.8.8.8:443

ESTAB = conexión establecida. TIME-WAIT = se cerró pero el SO espera 60s antes de liberar el puerto (por seguridad de TCP).

Capturar tráfico:

$ sudo tcpdump -i any -n port 80 -A
14:33:21.001 IP 192.168.1.10.50000 > 1.2.3.4.80: Flags [S], seq 100, ...
14:33:21.005 IP 1.2.3.4.80 > 192.168.1.10.50000: Flags [S.], seq 500, ack 101, ...
14:33:21.005 IP 192.168.1.10.50000 > 1.2.3.4.80: Flags [.], ack 501, ...

Flags [S] = SYN. Flags [S.] = SYN+ACK. Flags [.] = ACK puro. Ahí estás viendo el three-way handshake en vivo.

Wireshark visualiza lo mismo con interfaz gráfica y mucho más detalle.

4.9 Resumen visual

TCP UDP
Modelo Stream (chorro de bytes) Datagramas
Conexión Sí (handshake) No
Confiable No
Ordenado No
Velocidad Más lento Más rápido
Overhead cabecera 20+ bytes 8 bytes
Casos HTTP, SSH, FTP DNS, video, juegos
Concepto Para qué
Puerto Identifica proceso
Three-way handshake Abrir conexión TCP
ACK Confirmar recepción
rwnd Control de flujo
cwnd Control de congestión
TIME-WAIT Esperar antes de reusar puerto

4.10 Ejercicios

✏️ Ejercicio 4.1 — Elegir el protocolo

Para cada caso, decí si usarías TCP o UDP y por qué:

a. Transferir un archivo de 1 GB. b. Llamada de Zoom. c. Login a un sistema bancario. d. Servicio de hora de red (NTP). e. Resolver el nombre google.com.

✏️ Ejercicio 4.2 — Captura del handshake

Ejecutá:

$ sudo tcpdump -i any -nn 'tcp and port 80'

En otra terminal:

$ curl http://example.com -o /dev/null

¿Cuántos paquetes ves al inicio? ¿Cuáles flags tienen?

✏️ Ejercicio 4.3 — Servidor de eco

Implementá un servidor TCP que devuelve lo que el cliente le manda (un "eco"). Probá con nc:

$ nc localhost 8080

✏️ Ejercicio 4.4 — Análisis del estado TIME-WAIT

Tu servidor web acepta 1000 conexiones por segundo, cada una corta. Después de un rato vés errores:

Cannot bind to port 8080: Address already in use

¿Qué pasa? ¿Cómo lo solucionás?

4.11 Para profundizar


Definiciones nuevas: capa de transporte, puerto, multiplexión, UDP, TCP, three-way handshake, four-way handshake, sequence number, ACK, retransmisión, RTT, timeout, control de flujo, control de congestión, rwnd, cwnd, slow start, socket, TIME-WAIT.