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 (). 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:
- Entrega confiable. Si no llega, retransmite.
- Orden. Los datos llegan en el orden enviado.
- Sin duplicados. Cada byte se entrega una sola vez.
- Control de flujo. No inunda al receptor.
- 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 ──|
- Cliente: "quiero abrir, mi sec inicial es 100".
- Servidor: "OK, mi sec es 500, tu ACK es 101 (siguiente que espero)".
- 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 significa "recibí todos los bytes hasta , esperando ".
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:
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 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ñon.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 | Sí | No |
| Ordenado | Sí | 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.
Solución
a. TCP. No podés perder bytes de un archivo. b. UDP. Latencia importa más; un frame perdido es aceptable. c. TCP. Pérdida o desorden serían catastróficos. d. UDP. Mensaje corto (4 bytes), si se pierde reintentás. NTP lo usa. e. UDP. DNS clásico — pregunta y respuesta cortas. (DNS-over-HTTPS sí usa TCP.)
✏️ 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?
Solución
Verás algo como:
Flags [S] ← SYN del cliente
Flags [S.] ← SYN-ACK del servidor
Flags [.] ← ACK del cliente (handshake completo)
Flags [P.] ← cliente envía GET
Flags [.] ← servidor ACK
Flags [P.] ← servidor envía respuesta
... etc
Flags [F.] ← FIN al cerrar
Los primeros tres son el three-way handshake. Después fluyen los datos.
✏️ 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
Solución
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('', 8080))
s.listen(5)
print("Eco listo en puerto 8080")
while True:
cliente, addr = s.accept()
print(f"Conexión de {addr}")
while True:
data = cliente.recv(1024)
if not data:
break
cliente.send(data)
cliente.close()
Probá con nc localhost 8080 — todo lo que escribas vuelve.
✏️ 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?
Solución
Diagnóstico: muchas conexiones en estado TIME-WAIT. Cuando un servidor cierra activamente, debe esperar 2·MSL (~60s) en TIME-WAIT antes de liberar el puerto. Con 1000 conexiones/s, acumulás miles en TIME-WAIT.
Verificar: ss -tn state time-wait | wc -l.
Soluciones:
SO_REUSEADDR(ya lo viste). Permite reusar puerto en TIME-WAIT.- Cliente cierra primero (no el servidor) — el TIME-WAIT queda en el cliente.
- HTTP keep-alive. Reusá conexiones en lugar de abrir/cerrar muchas.
- Tunear
tcp_tw_reusedel kernel (con cuidado). - HTTP/2 o HTTP/3 — multiplexan múltiples requests en una conexión.
En la realidad, HTTP keep-alive es la solución más común — fija el problema sin cambios al SO.
4.11 Para profundizar
- Tanenbaum cap. 6.
- Stevens, TCP/IP Illustrated, Vol 1. Histórico, técnico, espectacular.
- RFC 793 (TCP) y RFC 768 (UDP). Cortos y legibles.
- Próximo capítulo: Capa de aplicación — HTTP, DNS, TLS.
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.