01 · La capa de transporte
Del paquete al programa
Un paquete IP llega al equipo correcto, pero en ese equipo hay muchos programas. La capa de transporte resuelve dos problemas que IP deja abiertos:
- Entregar al programa correcto: con el número de puerto (tema 30), que ambos protocolos de transporte agregan.
- Decidir qué garantías ofrecer sobre un servicio de red que no da ninguna. Eso es lo que separa a UDP de TCP.
Es un problema que ya conocemos en pequeño. En el bus CAN (tema 27) la propia trama incluye confirmación y reintento; en Ethernet (tema 29) una trama dañada se descarta y nadie avisa. Las redes de paquetes dejan esas decisiones al transporte, y permiten que cada aplicación elija: una lectura de temperatura que se repite cada segundo no necesita que se reenvíe la que se perdió, pero un archivo de configuración sí tiene que llegar completo.
02 · Lo mínimo
UDP: datagramas sin garantías
UDP (User Datagram Protocol) es casi IP con puertos. Su cabecera tiene solo 8 bytes: puerto de origen, puerto de destino, largo (cabecera más datos) y checksum. Cada datagrama es independiente: se arma, se envía y no se vuelve a pensar en él.
Qué ofrece y qué no:
- Conserva los límites de los mensajes. Cada
sendtoproduce un datagrama y cadarecvfromrecibe uno entero (o ninguno). - No hay conexión. No hace falta un saludo previo: el primer paquete ya lleva datos.
- No hay garantías. Un datagrama puede perderse, duplicarse o llegar desordenado, y UDP no avisa ni reintenta.
- El checksum protege cabecera y datos con la misma suma de 16 bits que vimos para IP, calculada además sobre un pseudo-encabezado que incluye las direcciones IP, para detectar un datagrama entregado al equipo equivocado. En IPv4 es opcional (se envía 0 si no se calculó); en IPv6 es obligatorio. Si el cálculo da 0, se envía 0xFFFF.
- Admite difusión y multicast, algo que TCP no puede hacer, porque TCP es siempre entre dos puntos.
Se usa donde la baja latencia y la simplicidad pesan más que la entrega perfecta, o donde el protocolo de aplicación se ocupa de la confiabilidad a su manera: DNS (una pregunta, una respuesta), DHCP, NTP, el protocolo ligero de IoT CoAP, la voz y el video en tiempo real, y las lecturas periódicas de sensores donde un valor perdido se compensa con el siguiente. El tamaño máximo recomendado de un datagrama para evitar la fragmentación en Ethernet es de 1472 bytes de datos (1500 − 20 de IP − 8 de UDP); para pasar por Internet sin problemas se suele usar mucho menos.
03 · Un cable virtual
TCP: un flujo confiable de bytes
TCP (Transmission Control Protocol) ofrece algo muy distinto: una conexión entre exactamente dos programas, por la que viaja un flujo de bytes en cada sentido, con estas garantías:
- Sin pérdidas. Lo que se pierde se reenvía.
- En orden. El receptor entrega los bytes en el orden en que se enviaron, aunque los paquetes hayan llegado mezclados.
- Sin duplicados.
- Con control de flujo: el emisor no abruma a un receptor lento.
- Con control de congestión: el emisor frena cuando la red se satura.
El costo: un saludo inicial antes de enviar datos, una cabecera de al menos 20 bytes, memoria en cada extremo para guardar el estado de la conexión y los datos en vuelo, y latencia variable cuando hay que reenviar algo.
Hay que fijarse bien en una consecuencia: TCP entrega un flujo, no mensajes. Si el emisor hace dos envíos, «TEMP\n» y «PING\n», el receptor puede recibir los diez bytes en una sola lectura, o «TEM» y luego el resto. TCP corta el flujo en segmentos como le conviene y no respeta los límites de los envíos. La aplicación tiene que delimitar sus mensajes: con un carácter de fin de línea (como hace HTTP o la mayoría de los protocolos de texto), con un prefijo que indique el largo, o con un tamaño fijo. Es el error más común de quien empieza con sockets.
En la cabecera TCP aparecen los campos que cumplen estas funciones: el número de secuencia (el número del primer byte de datos del segmento dentro del flujo), el número de confirmación (el próximo byte que el receptor espera), la ventana (cuántos bytes más está dispuesto a recibir), el checksum (con pseudo-encabezado, como UDP) y seis indicadores de un bit, de los cuales los más importantes son SYN (abre una conexión), ACK (el número de confirmación es válido), FIN (no hay más datos que enviar), RST (cortar de inmediato: la conexión no existe o hubo un error) y PSH (entregar ya a la aplicación). Las opciones opcionales (por ejemplo el tamaño máximo de segmento MSS, el escalado de ventana, las confirmaciones selectivas) alargan la cabecera.
04 · Un acuerdo previo
Abrir y cerrar una conexión
Antes de enviar datos, los dos extremos se ponen de acuerdo con el saludo de tres vías (three-way handshake). Uno de los extremos, el servidor, está escuchando en un puerto. El otro, el cliente, inicia la conexión:
- SYN. El cliente envía un segmento con SYN y su número de secuencia inicial (ISN), x. Se elige distinto cada vez, por seguridad y para no confundir segmentos de conexiones viejas.
- SYN + ACK. El servidor responde con SYN, su propio ISN y, y un ACK con x + 1: «recibí tu SYN, espero el byte x + 1».
- ACK. El cliente confirma con y + 1. La conexión está establecida.
Un SYN o un FIN consumen un número de secuencia aunque no lleven datos, por eso el ACK es el número + 1. En el saludo también se acuerdan opciones, como el MSS (tamaño máximo de datos por segmento, normalmente 1460 en Ethernet, 1500 − 40) y si se usará escalado de ventana. Cada extremo guarda un estado por conexión; un servidor que acepta SYN sin límite puede agotar su memoria con conexiones a medio abrir (ataque «SYN flood»), y por eso los servidores embebidos tienen un número máximo de conexiones simultáneas bajo (el W5500, por ejemplo, tiene ocho sockets).
El cierre es independiente en cada sentido: un extremo envía FIN para decir «no tengo más para enviar», el otro lo confirma, y cuando termina envía su propio FIN. Son cuatro segmentos (o tres si el ACK y el FIN del segundo viajan juntos). El que cierra primero queda un rato en el estado TIME_WAIT (un par de minutos, a veces menos), para reabsorber segmentos retrasados de esa conexión antes de que se reutilice el puerto. Una conexión también puede terminar de golpe con un segmento RST, por ejemplo si llega un segmento para un puerto en el que nadie escucha («conexión rechazada») o si un programa se cierra de forma anormal.
Los estados que vale la pena reconocer en un diagnóstico son: LISTEN (el servidor espera), SYN_SENT y SYN_RECEIVED (en el saludo), ESTABLISHED (conexión abierta), FIN_WAIT, CLOSE_WAIT y TIME_WAIT (durante el cierre). Un servidor con muchas conexiones en CLOSE_WAIT tiene una aplicación que no cierra su lado.
05 · Contar bytes y reenviar
Secuencia, confirmación y retransmisión
TCP numera cada byte del flujo. El número de secuencia de un segmento es el del primer byte que lleva. El receptor responde con un número de confirmación (ACK) que dice «recibí todo hasta antes de este byte; mandame el siguiente». Es una confirmación acumulativa: un solo ACK de 1301 confirma todos los bytes hasta el 1300. Por ejemplo, un cliente que envía 100 bytes con secuencia 1001 espera un ACK con 1101.
Cómo se detecta una pérdida:
- Por temporizador. Al enviar un segmento, el emisor arranca un temporizador, el RTO (Retransmission Timeout). Si no llega el ACK a tiempo, retransmite. El RTO se calcula a partir de las mediciones del tiempo de ida y vuelta (RTT): un promedio suavizado
SRTTy una medida de variaciónRTTVAR, conRTO = SRTT + 4 · RTTVAR. Si vuelve a vencer, se duplica (retroceso exponencial). La norma fija un mínimo de 1 segundo. - Por ACK duplicados. Si un segmento se pierde pero llegan los siguientes, el receptor sigue repitiendo el mismo ACK («sigo esperando el 1301»). Tras tres ACK duplicados el emisor reenvía el segmento sin esperar el temporizador (retransmisión rápida). Esto reacciona en un RTT y no en un RTO.
Muchas implementaciones agregan confirmaciones selectivas (SACK): el receptor informa qué bloques recibió fuera de orden, y el emisor reenvía solo los que faltan.
Dos mecanismos más afectan a los dispositivos que envían mensajes cortos. El algoritmo de Nagle junta datos pequeños en un segmento mientras haya algo sin confirmar; ahorra paquetes pero agrega demora. Y el ACK retrasado hace que el receptor espere (hasta unas decenas de milisegundos) por si puede confirmar junto con datos propios. La combinación puede producir demoras de 40 a 200 ms en protocolos de pedido y respuesta de mensajes cortos; muchas bibliotecas permiten desactivar Nagle (TCP_NODELAY) cuando la latencia importa.
Como complemento, TCP puede enviar sondas periódicas (keepalive) para detectar que el otro extremo desapareció sin cerrar, por ejemplo por un corte de energía. Sin ellas una conexión inactiva puede quedar «abierta» para siempre en un extremo. Muchos protocolos de IoT (MQTT, tema 32) definen su propio mecanismo de latido en la aplicación.
06 · No ahogar al receptor
Control de flujo: la ventana
Cada ACK lleva el campo ventana: cuántos bytes más puede aceptar el receptor en su memoria de recepción. El emisor no puede tener más bytes «en vuelo» (enviados y sin confirmar) que esa ventana. Si el receptor está lento y su memoria se llena, anuncia ventana cero, y el emisor se detiene hasta que se le avise que hay lugar. Es un control de flujo punta a punta, que ajusta la velocidad al más lento de los dos.
La ventana también fija un límite de rendimiento. Como el emisor solo puede enviar una ventana por cada tiempo de ida y vuelta (RTT), la velocidad máxima es:
velocidad ≤ ventana / RTT
Con una ventana de 64 KiB (el máximo del campo de 16 bits) y 20 ms de RTT, queda en 26 Mbit/s, aunque el enlace sea de 100 Mbit/s. Para llenar el enlace hace falta una ventana de al menos el producto ancho de banda × RTT (bandwidth-delay product): 100 Mbit/s × 20 ms = 250 000 bytes. Para ventanas mayores que 65 535, la opción de escalado de ventana multiplica el campo por una potencia de dos, que se acuerda en el saludo.
En un microcontrolador la ventana es una cuestión de memoria: la pila TCP/IP reserva un buffer de recepción por conexión, y si ese buffer es de unos pocos kilobytes, el rendimiento queda limitado mucho antes de que el enlace se sature, sobre todo si el RTT es grande (por ejemplo, hablando con un servidor en la nube a 100 ms). Con 2920 bytes de ventana y 20 ms de RTT, la velocidad máxima es de 1,17 Mbit/s en un enlace de 100 Mbit/s. Si el RTT sube a 100 ms, queda en 0,23 Mbit/s. Para un sensor que manda unos pocos bytes por segundo no importa, pero sí para una actualización de firmware.
07 · No ahogar la red
Control de congestión
El control de flujo protege al receptor. El control de congestión protege a la red: si todos los emisores mandaran lo más rápido posible, los routers intermedios descartarían paquetes por desborde y las retransmisiones empeorarían el problema (un colapso). TCP no recibe ningún aviso explícito de la red; infiere la congestión de las pérdidas y ajusta su velocidad. Cada emisor mantiene una ventana de congestión (cwnd), y puede tener en vuelo como máximo el menor entre cwnd y la ventana del receptor.
- Arranque lento (slow start). Se empieza con una ventana chica (hoy, 10 segmentos; antes 1 a 4) y se duplica en cada RTT, por cada ACK recibido se suma un segmento. Es exponencial: se llega rápido a la capacidad. El nombre es histórico: es «lento» solo comparado con enviar todo de golpe.
- Evitación de la congestión. Al superar un umbral (
ssthresh), la ventana crece de a un segmento por RTT: aumento lineal y cauteloso. - Reacción a la pérdida. Si el temporizador vence, la red está muy congestionada: el umbral pasa a la mitad de la ventana y se vuelve a empezar desde una ventana mínima. Si la pérdida se detecta por tres ACK duplicados (la red sigue entregando algo), TCP reduce la ventana a la mitad y continúa en evitación sin volver al principio (fast recovery).
El resultado es el conocido patrón «diente de sierra»: la ventana crece despacio hasta que se pierde un paquete, se corta a la mitad, y vuelve a crecer. La regla se resume como AIMD (aumento aditivo, disminución multiplicativa) y hace que varias conexiones que comparten un enlace converjan a un reparto parejo. Variantes modernas (CUBIC, BBR) cambian la fórmula, no la idea.
Para el diseño de dispositivos embebidos hay tres consecuencias. Primero, una conexión nueva empieza despacio: abrir una conexión por cada dato pequeño paga siempre el saludo y el arranque lento; conviene mantener la conexión abierta. Segundo, la pérdida baja el rendimiento mucho más de lo que sugiere su porcentaje, sobre todo en enlaces inalámbricos con pérdidas por ruido, que TCP confunde con congestión. Tercero, sobre una red local sana no se nota nada: los efectos aparecen en Internet, con RTT grandes, y en redes con pérdidas.
08 · Una decisión de diseño
TCP o UDP: cómo elegir
| Característica | TCP | UDP |
|---|---|---|
| Conexión | Sí (saludo de tres vías) | No |
| Unidad de datos | Flujo de bytes: sin límites de mensaje | Datagramas: conserva los límites |
| Entrega y orden | Garantizados | No garantizados |
| Retransmisión | Automática | La hace la aplicación, si hace falta |
| Control de flujo y de congestión | Sí | No |
| Cabecera | 20 bytes o más | 8 bytes |
| Estado en memoria | Por conexión (buffers, temporizadores) | Casi ninguno |
| Difusión y multicast | No | Sí |
| Latencia | Variable: reenvíos, Nagle, arranque lento | Mínima |
| Ejemplos | HTTP, MQTT, Modbus TCP, SSH, correo | DNS, DHCP, NTP, CoAP, voz y video, descubrimiento |
La regla práctica: TCP si el dato tiene que llegar completo y en orden (comandos, configuración, archivos, telemetría que no se puede perder), UDP si el dato caduca o el protocolo se ocupa de la pérdida (una lectura repetida cada segundo, mensajes de descubrimiento, sincronización de hora). En IoT, lo más común es TCP con un protocolo de aplicación sobre él (MQTT, HTTP), por su simplicidad para el programador; UDP aparece cuando el dispositivo es muy pequeño o a pilas (CoAP) y cuando se necesita multicast. Quien elige UDP para «ahorrar» y luego agrega confirmaciones, números de secuencia y reintentos en su propia aplicación está reconstruyendo una parte de TCP, normalmente peor.
09 · La interfaz del programador
Sockets: clientes y servidores
Un socket es el punto de acceso a la red que el sistema operativo (o la biblioteca del microcontrolador) ofrece al programa. Nació en Unix, se llama «sockets de Berkeley» y es casi igual en C, Python, Java, JavaScript y en las bibliotecas de Arduino y ESP32. Un socket TCP queda identificado por los cinco datos que vimos en el tema 30 (protocolo, IP y puerto locales, IP y puerto remotos).
El servidor TCP pasa por cuatro llamadas:
socket(): crea el socket.bind(): lo asocia a un puerto (y a una dirección local, o a todas).listen(): lo pone a escuchar, con una cola de conexiones pendientes.accept(): espera una conexión y devuelve un socket nuevo para ese cliente; el original sigue escuchando para los demás.
El cliente hace socket() y luego connect() con la IP y el puerto del servidor (aquí se dispara el saludo de tres vías). Ambos intercambian datos con send() y recv() y terminan con close(). Con UDP no hay listen, accept ni connect: se hace bind() si se va a recibir, y se usan sendto() y recvfrom(), que indican a quién se manda o de quién se recibe.
Un ejemplo completo en Python: un servidor de «eco por línea» y su cliente. Como se ejecutan en una computadora y no en el navegador, el bloque no tiene botón de ejecución; podés copiarlo a dos archivos y correrlos en dos terminales:
# servidor.py
import socket
servidor = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # TCP
servidor.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # reutilizar el puerto tras reiniciar
servidor.bind(("0.0.0.0", 5000))
servidor.listen()
print("Escuchando en el puerto 5000")
while True:
conexion, direccion = servidor.accept() # bloquea hasta que llega un cliente
print("Cliente", direccion)
with conexion, conexion.makefile("rw", newline="\n") as flujo: # lee y escribe líneas completas
for linea in flujo: # el delimitador es '\n': TCP no conserva mensajes
flujo.write("recibido: " + linea.strip() + "\n")
flujo.flush()
# cliente.py
import socket
with socket.create_connection(("127.0.0.1", 5000), timeout=5) as s: # connect() con tiempo límite
s.sendall(b"TEMP\nPING\n") # dos mensajes en un solo envío
flujo = s.makefile("r")
print(flujo.readline().strip()) # recibido: TEMP
print(flujo.readline().strip()) # recibido: PING
# udp_receptor.py y udp_emisor.py
import socket
rx = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP
rx.bind(("0.0.0.0", 9999))
datos, origen = rx.recvfrom(1500) # un datagrama entero, o nada
print(datos, "desde", origen)
tx = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
tx.sendto(b"T=23.5", ("127.0.0.1", 9999)) # sin conexión previa
Cuatro errores comunes con sockets. Primero, suponer que un recv() trae un mensaje entero (en TCP puede traer parte de uno o varios): hay que acumular y delimitar. Segundo, bloquear el programa esperando datos o una conexión: en un microcontrolador, el bucle principal se detiene; hay que usar lecturas no bloqueantes, tiempos límite o consultar available(). Tercero, no manejar los errores y las desconexiones: el otro extremo puede cerrar, el cable puede desconectarse, el servidor puede no responder; el programa tiene que reconectar. Cuarto, olvidarse de cerrar: cada socket abierto consume memoria, y en un dispositivo con ocho sockets se agotan rápido.
10 · Con el microcontrolador
Con el microcontrolador
El primer programa convierte la placa con W5500 (tema 29 y 30) en un servidor TCP en el puerto 5000 que entiende comandos de una línea (PING, TEMP) y delimita los mensajes con el fin de línea. Lo podés probar desde una computadora con ncat 192.168.1.50 5000 (o nc) o con un programa de terminal en modo «raw»; escribí PING y Enter y debería responder PONG:
#include <SPI.h>
#include <Ethernet.h>
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; // cambiar el último byte en cada placa
const int PIN_CS = 10;
EthernetServer servidor(5000);
void responder(EthernetClient &cliente, String comando) {
comando.trim();
comando.toUpperCase();
if (comando == "PING") cliente.println("PONG");
else if (comando == "TEMP") cliente.println("23.5"); // un valor de ejemplo
else cliente.println("?");
}
void setup() {
Serial.begin(115200);
Ethernet.init(PIN_CS);
if (Ethernet.begin(mac, 10000) == 0) { Serial.println("Sin DHCP"); while (true) delay(1000); }
servidor.begin();
Serial.print("Servidor en "); Serial.print(Ethernet.localIP()); Serial.println(":5000");
}
void loop() {
static String linea; // acumula hasta el fin de línea: TCP es un flujo
EthernetClient cliente = servidor.available(); // un cliente con datos para leer, si lo hay
if (cliente) {
while (cliente.available()) {
char c = cliente.read();
if (c == '\n') { responder(cliente, linea); linea = ""; }
else if (c != '\r' && linea.length() < 40) linea += c;
}
}
Ethernet.maintain();
}
El segundo programa usa UDP: envía una lectura cada 5 segundos a una computadora (que puede recibirla con nc -u -l 9999) y escucha comandos en el puerto 8888. No hay conexión: si nadie escucha, los datagramas se pierden sin error.
#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>
byte mac[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED };
const int PIN_CS = 10;
const unsigned int PUERTO_LOCAL = 8888;
IPAddress destino(192, 168, 1, 10); // la computadora que recibe: ajustarla
const unsigned int PUERTO_DESTINO = 9999;
EthernetUDP udp;
void setup() {
Serial.begin(115200);
Ethernet.init(PIN_CS);
if (Ethernet.begin(mac, 10000) == 0) { Serial.println("Sin DHCP"); while (true) delay(1000); }
udp.begin(PUERTO_LOCAL); // abrir el puerto local para recibir
Serial.print("UDP en "); Serial.print(Ethernet.localIP()); Serial.print(':'); Serial.println(PUERTO_LOCAL);
}
void loop() {
static unsigned long ultimo = 0;
if (millis() - ultimo >= 5000) {
ultimo = millis();
udp.beginPacket(destino, PUERTO_DESTINO);
udp.print("T=23.5"); // un datagrama = un mensaje
udp.endPacket();
}
int n = udp.parsePacket(); // tamaño del datagrama recibido, o 0
if (n > 0) {
char buffer[32];
int largo = udp.read(buffer, sizeof(buffer) - 1);
buffer[largo > 0 ? largo : 0] = 0;
Serial.print("De "); Serial.print(udp.remoteIP()); Serial.print(':'); Serial.print(udp.remotePort());
Serial.print(" -> "); Serial.println(buffer);
}
Ethernet.maintain();
}
Dos observaciones. Con TCP, el programa tiene que acumular los bytes hasta el delimitador, como hace la variable linea; no puede suponer que cada read trae un mensaje. Con UDP, cada parsePacket es un datagrama completo, pero puede no llegar: si la aplicación necesita saber que llegó, tiene que pedir una respuesta y reenviar si no la recibe. Ambos programas son bloqueantes solo en el sentido de que consultan el estado en cada vuelta del loop: el resto del programa puede seguir leyendo sensores.
11 · Analogía con programación
Transporte en Python
El navegador no puede abrir sockets de red, así que el modelo siguiente no los usa: arma los bytes de los datagramas y segmentos con sus checksums, sigue el saludo de tres vías con sus números de secuencia, calcula el temporizador de retransmisión, la evolución de la ventana de congestión y la velocidad máxima por ventana, y muestra cómo un flujo TCP se divide en mensajes con un delimitador.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.
ORIGEN = bytes([192, 168, 1, 50])
DESTINO = bytes([192, 168, 1, 10])
def checksum(datos):
if len(datos) % 2:
datos += b"\x00"
suma = sum((datos[i] << 8) | datos[i + 1] for i in range(0, len(datos), 2))
while suma > 0xFFFF:
suma = (suma & 0xFFFF) + (suma >> 16)
return ~suma & 0xFFFF
def pseudo(protocolo, largo):
return ORIGEN + DESTINO + bytes([0, protocolo]) + largo.to_bytes(2, "big")
def datagrama_udp(p_origen, p_destino, datos):
largo = 8 + len(datos)
cab = p_origen.to_bytes(2, "big") + p_destino.to_bytes(2, "big") + largo.to_bytes(2, "big") + b"\x00\x00"
suma = checksum(pseudo(17, largo) + cab + datos) or 0xFFFF
return cab[:6] + suma.to_bytes(2, "big") + datos
def segmento_tcp(p_origen, p_destino, seq, ack, indicadores, ventana, datos=b""):
cab = (p_origen.to_bytes(2, "big") + p_destino.to_bytes(2, "big") + seq.to_bytes(4, "big") + ack.to_bytes(4, "big")
+ bytes([0x50, indicadores]) + ventana.to_bytes(2, "big") + b"\x00\x00\x00\x00")
suma = checksum(pseudo(6, len(cab) + len(datos)) + cab + datos)
return cab[:16] + suma.to_bytes(2, "big") + cab[18:] + datos
SYN, ACK, PSH, FIN = 0x02, 0x10, 0x08, 0x01
def rto(muestras):
srtt, var = muestras[0], muestras[0] / 2
for r in muestras[1:]:
var = 0.75 * var + 0.25 * abs(srtt - r)
srtt = 0.875 * srtt + 0.125 * r
return srtt, var, srtt + 4 * var
def congestion(rondas, umbral, ronda_perdida=0):
cwnd, salida = 1, []
for r in range(1, rondas + 1):
salida.append(cwnd)
if r == ronda_perdida:
umbral, cwnd = max(2, cwnd // 2), 1
elif cwnd < umbral:
cwnd = min(cwnd * 2, umbral)
else:
cwnd += 1
return salida
def velocidad_maxima(ventana_bytes, rtt_s, enlace_bps):
return min(enlace_bps, ventana_bytes * 8 / rtt_s)
udp = datagrama_udp(50000, 9999, b"T=23.5")
print("UDP:", udp.hex(" ").upper(), "| resto al verificar:", checksum(pseudo(17, len(udp)) + udp))
cliente_isn, servidor_isn = 1000, 5000
print("Saludo de tres vías:")
for texto, seg in (("SYN ", segmento_tcp(50035, 1883, cliente_isn, 0, SYN, 8192)),
("SYN+ACK ", segmento_tcp(1883, 50035, servidor_isn, cliente_isn + 1, SYN | ACK, 8192)),
("ACK ", segmento_tcp(50035, 1883, cliente_isn + 1, servidor_isn + 1, ACK, 8192))):
print(" ", texto, seg.hex(" ").upper())
datos = segmento_tcp(50035, 1883, cliente_isn + 1, servidor_isn + 1, PSH | ACK, 8192, b"hola")
print("Datos 'hola':", datos.hex(" ").upper(), "-> próximo número de secuencia:", cliente_isn + 1 + 4)
print(" resto al verificar:", checksum(pseudo(6, len(datos)) + datos))
print("RTO con mediciones 100, 120, 90, 300, 110 ms:", tuple(round(x, 1) for x in rto([100, 120, 90, 300, 110])))
print("Congestión (umbral 16, pérdida en la ronda 9, 14 rondas):", congestion(14, 16, 9))
for ventana, rtt in ((65535, 0.02), (2920, 0.02), (262144, 0.02)):
print(f"Ventana {ventana} bytes, RTT {rtt * 1000:.0f} ms, enlace de 100 Mbit/s: {velocidad_maxima(ventana, rtt, 100e6) / 1e6:.2f} Mbit/s")
print("Producto ancho de banda × RTT (100 Mbit/s, 20 ms):", int(100e6 * 0.02 / 8), "bytes")
# TCP es un flujo de bytes: dos envíos pueden llegar juntos.
flujo = b"TEMP\n" + b"PING\n"
print("Mensajes recuperados del flujo con el delimitador '\\n':", flujo.decode().split("\n")[:-1])
Resultado: el datagrama UDP con «T=23.5» mide 14 bytes, sale como C3 50 27 0F 00 0E DD 3F 54 3D 32 33 2E 35 y su resto de verificación es 0. En el saludo de tres vías, el SYN lleva secuencia 1000, el SYN+ACK secuencia 5000 con confirmación 1001, y el ACK secuencia 1001 con confirmación 5001; los datos «hola» salen con secuencia 1001 (checksum 0x552A) y el próximo número de secuencia es 1005. Con mediciones de 100, 120, 90, 300 y 110 ms, el RTT suavizado es 123,8 ms, la variación 61,0 ms y el RTO 367,7 ms. La ventana de congestión con umbral 16 y pérdida en la ronda 9 evoluciona como 1, 2, 4, 8, 16, 17, 18, 19, 20, 1, 2, 4, 8, 10. Con 64 KiB, RTT de 20 ms y un enlace de 100 Mbit/s la velocidad máxima es 26,21 Mbit/s; con 2920 bytes, 1,17 Mbit/s; con 256 KiB, los 100 Mbit/s completos, para un producto ancho de banda × RTT de 250 000 bytes. Y el flujo TEMP
PING
se separa en los mensajes TEMP y PING.
Observá que el segmento SYN consume el número 1000 y el SYN+ACK confirma el 1001, que los datos «hola» llevan secuencia 1001 y que el siguiente segmento empezaría en 1005. Y que los dos mensajes enviados juntos se recuperan del flujo solo gracias al delimitador.
12 · Exploración interactiva
Laboratorio: cabeceras, ventana, congestión y temporizador
El laboratorio tiene cinco herramientas. La primera arma una cabecera TCP con su checksum. La segunda arma un datagrama UDP. La tercera relaciona la ventana, el RTT y el rendimiento. La cuarta dibuja la ventana de congestión ronda por ronda. La quinta calcula el temporizador de retransmisión a partir de mediciones de RTT.
1. Segmento TCP
Elegí los puertos, los números y los indicadores.
2. Datagrama UDP
Elegí los puertos y los datos.
3. Ventana y rendimiento
Elegí el RTT, el enlace y la ventana.
4. Ventana de congestión
Elegí las rondas, el umbral y cuándo se pierde un segmento.
5. Temporizador de retransmisión
Escribí una lista de mediciones.
En las herramientas 1 y 2 el checksum usa un pseudo-encabezado con las direcciones 192.168.1.50 (origen) y 192.168.1.10 (destino). La herramienta 3 supone que el emisor no está limitado por la congestión. La 4 es un modelo simplificado por rondas: ante la pérdida, el umbral se reduce a la mitad y la ventana vuelve a 1. La 5 aplica las fórmulas de la norma (pesos 1/8 y 1/4).
Recorrido guiado
- Con los valores iniciales (PSH + ACK, secuencia 1001, confirmación 5001, ventana 8192, datos «hola»): cabecera
C3 73 07 5B 00 00 03 E9 00 00 13 89 50 18 20 00 55 2A 00 00, checksum 0x552A, verificación en cero, segmento de 24 bytes y próximo número de secuencia 1005 (se consumieron 4). - Cambiá los indicadores a SYN y borrá los datos: el segmento mide 20 bytes y el próximo número de secuencia es 1002: el SYN consume uno. Con SYN + ACK y los puertos invertidos (origen 1883, destino 50035), secuencia 5000 y confirmación 1001, se obtiene el segundo mensaje del saludo (checksum 0x2A06).
- Cambiá un solo byte de los datos o del número de secuencia: el checksum cambia. Es la verificación que hace el receptor antes de aceptar el segmento.
- En la herramienta 2, con los valores iniciales (UDP, «T=23.5»):
C3 50 27 0F 00 0E DD 3F, 14 bytes de largo y checksum 0xDD3F. Sin datos, el largo es 8. - En la herramienta 3, con los valores iniciales (RTT 20 ms, 100 Mbit/s, 64 KiB): producto ancho de banda × RTT de 250 000 bytes; velocidad máxima 26,21 Mbit/s (26,2 % del enlace). «La ventana es el cuello de botella»; la ventana necesaria supera los 65 535 bytes y exige escalado.
- Probá 2920 bytes (dos segmentos): 1,17 Mbit/s, solo 1,2 % del enlace. Con 256 KiB: los 100 Mbit/s completos. Subí el RTT a 100 ms con 64 KiB: 5,24 Mbit/s.
- En la herramienta 4, con los valores iniciales: 1 2 4 8 16 17 18 19 20 | 1 2 4 8 10. Arranque lento hasta el umbral (16), crecimiento lineal hasta 20, pérdida en la ronda 9, nuevo umbral 10 (la mitad de 20) y vuelta a empezar. Poné la ronda de pérdida en 0: sin pérdidas, crece sin parar.
- En la herramienta 5, con las mediciones iniciales (100, 120, 90, 300, 110 ms): SRTT 123,8 ms, RTTVAR 61,0 ms y RTO 367,7 ms (con el mínimo de 1 s de la norma, 1000 ms). La medición aislada de 300 ms hizo subir mucho la variación. Con una sola medición de 100: RTO = 100 + 4 · 50 = 300 ms.
Desafío: una actualización de firmware
Un dispositivo con una ventana de recepción de 5840 bytes baja un firmware de un servidor en Internet con un RTT de 80 ms, a través de un enlace de 100 Mbit/s. ¿Cuánto tarda en bajar 1 MiB? Con la herramienta 3: 5840 bytes / 0,08 s = 73 000 B/s, es decir 0,58 Mbit/s. Un megabyte (1 048 576 bytes) tarda unos 14,4 segundos. Con una ventana de 65 535 bytes serían 6,55 Mbit/s y 1,3 segundos. La ventana, que depende de la memoria del dispositivo, domina el tiempo; el enlace de 100 Mbit/s casi no importa.
13 · Un procedimiento
Diagnóstico de conexiones TCP y UDP
Cuando una conexión falla o es lenta, conviene saber en qué paso se corta. Con IP sana (tema 30), se prueba de abajo hacia arriba:
- ¿Llega el SYN? Desde un cliente,
ncat -v IP PUERTOotelnet IP PUERTOdicen si se pudo abrir la conexión. «Connection refused» significa que el equipo respondió con RST: llegó al destino, pero no hay nadie escuchando en ese puerto. Un «timeout» sin respuesta significa que el SYN nunca contestó: un firewall, una ruta incorrecta o un equipo apagado. - ¿Está el servidor escuchando? En el equipo servidor,
netstat -anoss -tlnmuestran los puertos enLISTEN. - ¿Hay tráfico? Un analizador de paquetes (Wireshark,
tcpdump) muestra el saludo, los números de secuencia, los reenvíos y los RST. Para ver el tráfico de un dispositivo hay que capturar en un puerto espejado del conmutador o en el propio extremo. - ¿Hay retransmisiones? Muchos reenvíos indican pérdidas en el camino: cable, wifi, congestión.
- ¿Hay ventana cero o ventana chica? Un receptor lento o un buffer pequeño frena al emisor.
- ¿Se delimitan bien los mensajes? Si la aplicación recibe mensajes cortados o pegados, falta delimitar (tema 31, sección 03).
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| «Connection refused» | El equipo existe, pero el servicio no escucha en ese puerto (o escucha en otra) | Verificar el puerto y que el programa esté en marcha |
| La conexión no responde (timeout) | Firewall, NAT sin redirección, IP incorrecta, equipo apagado | Seguir el diagnóstico IP del tema 30; revisar reglas |
| Mensajes que llegan partidos o pegados | Se trata el flujo TCP como mensajes | Delimitar con fin de línea o prefijo de largo y acumular |
| Los datos llegan con 40 a 200 ms de demora en protocolos de mensajes cortos | Nagle y ACK retrasado | Desactivar Nagle (TCP_NODELAY) o enviar cada mensaje en un solo write |
| El servidor deja de aceptar clientes | Se agotaron los sockets (W5500: 8) por conexiones sin cerrar | Cerrar los sockets, limitar clientes y usar tiempos límite |
| La conexión parece abierta, pero el otro lado ya no está | Corte sin cierre (energía, cable), sin keepalive | Latido de aplicación o keepalive de TCP |
| Descarga muy lenta de un servidor lejano | Ventana de recepción chica con RTT grande | Aumentar el buffer de recepción si hay memoria |
| Los datagramas UDP se pierden «a veces» | Buffer de recepción lleno, red congestionada o datagramas muy grandes (fragmentados) | Leer más rápido, reducir el tamaño y agregar confirmación en la aplicación |
14 · Comprensión
Confusiones frecuentes
«Un send() corresponde a un recv()»
No en TCP: es un flujo de bytes. Un envío puede llegar en varios trozos o varios envíos en uno solo. Hay que delimitar los mensajes. En UDP sí hay correspondencia: un datagrama por lectura.
«TCP garantiza que el otro lado procesó el dato»
Garantiza que sus bytes llegaron al buffer del receptor, no que la aplicación los haya leído ni actuado. Si hace falta saber que se ejecutó un comando, la aplicación tiene que contestar.
«UDP es más rápido porque su protocolo es más rápido»
Tiene menos cabecera y ninguna espera, de modo que la latencia es menor, pero los bytes viajan por la red a la misma velocidad. La ventaja es no esperar saludos ni confirmaciones, no sufrir reenvíos que demoran a los datos posteriores.
«El saludo de tres vías es para autenticar al otro extremo»
Es para sincronizar los números de secuencia y comprobar que el otro extremo recibe. No autentica a nadie; para eso están TLS y otros mecanismos de la aplicación.
«El ACK confirma que el paquete llegó»
Confirma el flujo de bytes recibido de forma contigua hasta ese número. Un ACK de 1301 dice «tengo todo hasta el 1300»; no menciona segmentos aislados (salvo SACK).
«Una ventana grande siempre acelera la transferencia»
Solo si la ventana era el límite. Si el límite es el enlace o la congestión, una ventana mayor no ayuda; y en un dispositivo con poca memoria, un buffer grande puede dejarlo sin recursos para otras conexiones.
«Si mi conexión TCP se corta, puedo seguir donde quedé»
No: la conexión desaparece con su estado. La aplicación tiene que reconectar y, si hace falta, retomar desde donde se quedó (por ejemplo, pidiendo desde el byte N). Por eso conviene diseñar los protocolos para tolerar reconexiones.
«Usar UDP me evita implementar fiabilidad»
Al revés: con UDP la fiabilidad (si hace falta) queda a cargo de la aplicación. Es una buena elección cuando el dato caduca o la pérdida es tolerable.
15 · Práctica guiada
Predecí antes de comprobar
1. Números de secuencia
Un cliente abre una conexión con ISN = 7000 y el servidor con ISN = 300. Escribí los números de secuencia y de confirmación de los tres segmentos del saludo.
Ver solución
SYN: seq = 7000. SYN + ACK: seq = 300, ack = 7001. ACK: seq = 7001, ack = 301. El SYN consume un número, por eso las confirmaciones valen uno más.
2. Confirmación acumulativa
El emisor envía tres segmentos de 100 bytes con secuencias 2001, 2101 y 2201; el segundo se pierde. ¿Qué ACK manda el receptor tras recibir el primero y después el tercero?
Ver solución
Tras el primero: ack = 2101 (espera el byte 2101). Tras el tercero, que llega fuera de orden: ack = 2101 otra vez (ACK duplicado), porque todavía falta el segundo. Cuando se retransmita el segundo y llegue, el ACK pasará a 2301 de golpe.
3. Límite por ventana
Un dispositivo tiene una ventana de recepción de 4380 bytes y habla con un servidor con RTT de 50 ms por un enlace de 100 Mbit/s. ¿Cuál es la velocidad máxima de TCP?
Ver solución
4380 · 8 / 0,05 = 700 800 bit/s, es decir 0,70 Mbit/s. El enlace de 100 Mbit/s queda casi sin usar.
4. Congestión
Con umbral inicial de 8 segmentos, sin pérdidas, ¿cuánto vale la ventana en las rondas 1 a 8?
Ver solución
1, 2, 4, 8 (llega al umbral), y después crece de a uno: 1, 2, 4, 8, 9, 10, 11, 12.
5. Temporizador
La primera medición de RTT es de 200 ms. ¿Cuánto valen SRTT, RTTVAR y RTO? Si la segunda medición es de 280 ms, ¿cuánto vale el nuevo RTO?
Ver solución
Primera: SRTT = 200, RTTVAR = 100, RTO = 200 + 4 · 100 = 600 ms. Segunda: RTTVAR = 0,75 · 100 + 0,25 · |200 − 280| = 95; SRTT = 0,875 · 200 + 0,125 · 280 = 210; RTO = 210 + 4 · 95 = 590 ms.
6. Delimitar mensajes
Un dispositivo recibe por TCP el flujo TEMP\nPI y en la siguiente lectura NG\nTEMP\n. ¿Qué mensajes hay y qué queda pendiente al terminar?
Ver solución
Concatenados: TEMP\nPING\nTEMP\n, es decir, tres mensajes completos: TEMP, PING y TEMP, sin nada pendiente. El programa tiene que acumular lo que llegó entre lecturas hasta encontrar cada \n.
7. Elegir el protocolo
Un nodo envía cada segundo la temperatura a un servidor, y además recibe comandos de configuración que no pueden perderse. ¿Qué protocolo usarías para cada cosa?
Ver solución
La telemetría periódica puede ir por UDP (un valor perdido se compensa con el siguiente) o por TCP si se prefiere no perder ninguno. Los comandos de configuración deben ir por TCP: tienen que llegar completos y en orden. En la práctica, muchos sistemas llevan todo por una sola conexión TCP (por ejemplo con MQTT, tema 32), por simplicidad.
16 · Síntesis
Ideas para recordar
- La capa de transporte entrega al programa correcto con puertos y decide qué garantías dar. UDP: 8 bytes de cabecera, sin conexión, sin garantías, conserva los límites de los mensajes, admite difusión. TCP: conexión de a dos, flujo ordenado y confiable de bytes, con control de flujo y de congestión.
- TCP es un flujo de bytes, no de mensajes: un send no es un recv. La aplicación debe delimitar sus mensajes (fin de línea, prefijo de largo, tamaño fijo) y acumular lo que llega.
- La conexión se abre con el saludo de tres vías (SYN, SYN+ACK, ACK) y los números de secuencia iniciales; SYN y FIN consumen un número. Se cierra con FIN en cada sentido (TIME_WAIT) o de golpe con RST. «Connection refused» es un RST: el equipo responde pero nadie escucha.
- Cada byte tiene un número; el ACK es acumulativo («espero el byte N»). Una pérdida se detecta por el temporizador (RTO = SRTT + 4·RTTVAR, mínimo 1 s) o por tres ACK duplicados (retransmisión rápida). Nagle y el ACK retrasado pueden agregar demoras en mensajes cortos.
- La ventana limita lo que se puede enviar sin confirmación: velocidad máxima = ventana / RTT. Para llenar el enlace hace falta una ventana igual al producto ancho de banda × RTT. En un microcontrolador la memoria fija la ventana y, con RTT grande, el rendimiento.
- El control de congestión infiere la saturación de las pérdidas: arranque lento (duplicar por RTT) hasta un umbral, aumento lineal después, y al perder, umbral a la mitad. Es el diente de sierra de AIMD. Una conexión nueva empieza despacio: conviene mantener abierta la conexión.
- Se elige TCP para lo que debe llegar completo y en orden, y UDP para lo que caduca, lo que necesita difusión o lo que el protocolo de aplicación resuelve (DNS, DHCP, NTP, CoAP). Reconstruir confiabilidad sobre UDP es reimplementar mal TCP.
- Con sockets, el servidor hace socket, bind, listen y accept (que devuelve un socket por cliente) y el cliente socket y connect; los dos usan send/recv y close. UDP usa sendto/recvfrom. En un microcontrolador hay pocos sockets, que no hay que bloquear ni olvidar de cerrar.
¿Listo para el siguiente tema?
Antes de avanzar, decidí qué protocolo usarías para una consulta DNS (UDP) y para una transferencia de firmware (TCP) y por qué; calculá la velocidad máxima de un TCP con ventana de 8760 bytes y RTT de 40 ms (1,75 Mbit/s); y explicá por qué un cliente que hace send("A") y send("B") puede recibir AB en una sola lectura. Si podés justificar las tres respuestas, tenés los fundamentos del transporte.
En el próximo tema subimos a la capa de aplicación para dispositivos: HTTP y MQTT. Veremos cómo se estructuran los pedidos y las respuestas, cómo se representan los datos con JSON, y cómo funciona el modelo de publicar y suscribirse con un broker, con los niveles de calidad de servicio.