WebRTC para transferencia de archivos: tutorial navegador a navegador
Transfiere archivos directamente entre navegadores con WebRTC. Canales de datos, señalización y configuración de conexión entre pares para compartir archivos en tiempo real.
La transferencia de archivos por WebRTC mueve bytes directamente entre dos navegadores usando un RTCDataChannel, sin ningún servidor en el camino de datos después del handshake inicial. Las piezas que necesitas: un canal de señalización (WebSocket o cualquier pequeño relé) para intercambiar ofertas SDP y candidatos ICE, servidores STUN para descubrir la NAT, un fallback TURN para NATs simétricas, y un canal de datos configurado para entrega ordenada y fiable. Una vez que la conexión entre pares está establecida, haces channel.send() con fragmentos de 16 KB a 256 KB y observas cómo los bytes fluyen sobre una asociación SCTP cifrada con DTLS 1.2 aproximadamente a la velocidad de la línea de red.
Cómo WebRTC consigue la comunicación entre pares
WebRTC no es magia: es ICE más SDP más DTLS más SCTP apilados juntos. El emisor crea un RTCPeerConnection, abre un canal de datos, genera una oferta SDP y la envía al receptor a través de tu canal de señalización. El receptor responde. Ambas partes intercambian candidatos ICE (IP local, IP reflexiva vía STUN, IP de relé vía TURN) hasta que encuentran un camino funcional. DTLS 1.2 hace el handshake de extremo a extremo, SCTP opera encima para streaming fiable, y tus bytes empiezan a fluir.
El cifrado es obligatorio y está integrado. No puedes desactivarlo. Eso es una ventaja de seguridad significativa: a diferencia de WebSockets, no necesitas recordar envolver nada en criptografía de capa de aplicación para protegerte de fisgones en la red. Para cifrado de extremo a extremo real contra tu propio servidor de señalización, añade una segunda ronda de AES-256-GCM encima, porque un servidor de señalización malicioso podría intercambiar sus propios certificados DTLS.
Señalización: la parte que WebRTC no define
WebRTC deliberadamente deja la señalización en tus manos. Un WebSocket en tu servidor está bien; también lo está un código de sala compartido publicado en una base de datos en tiempo real de Firebase, o incluso strings SDP pegados manualmente. Lo que importa es que ambos pares intercambien eventualmente una oferta, una respuesta y un flujo de candidatos ICE.
Servidor de señalización mínimo en Node:
const rooms = new Map();
wss.on('connection', (ws) => {
ws.on('message', (raw) => {
const msg = JSON.parse(raw);
if (msg.type === 'join') {
const room = rooms.get(msg.room) ?? new Set();
room.add(ws); rooms.set(msg.room, room);
} else {
for (const peer of rooms.get(msg.room) ?? []) {
if (peer !== ws) peer.send(raw);
}
}
});
});
Menos de 20 líneas. El servidor nunca ve bytes de archivo, solo metadatos SDP e ICE. Puedes alojarlo en un VPS de 5 € o en Cloudflare Workers y servir cientos de transferencias simultáneas.
Establecer la conexión entre pares
Crea la conexión con el STUN público de Google más un fallback TURN:
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'turn:turn.example.com:3478',
username: 'user', credential: 'pass' }
]
});
Alrededor del 15-25% de las conexiones residenciales están detrás de NATs simétricas que STUN no puede atravesar, por lo que TURN no es opcional para un servicio en producción. Ejecuta coturn en un VPS con suficiente ancho de banda para manejar el tráfico retransmitido, o paga por un proveedor TURN gestionado como Xirsys o Twilio.
Abre el canal de datos en el lado del oferente antes de crear la oferta:
const channel = pc.createDataChannel('file', {
ordered: true, maxRetransmits: null
});
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signaling.send({ type: 'offer', sdp: offer.sdp });
Ordenado + retransmisiones ilimitadas te da fiabilidad equivalente a TCP. El modo no ordenado es más rápido pero necesita reensamblado a nivel de aplicación.
Fragmentar el archivo para el canal
SCTP tiene un límite práctico de 256 KB por mensaje, y los navegadores más antiguos tienen problemas por encima de 16 KB. Un valor predeterminado seguro son fragmentos de 16 KB leídos del archivo mediante File.slice():
const chunkSize = 16 * 1024;
let offset = 0;
channel.bufferedAmountLowThreshold = 1024 * 1024;
function sendNext() {
while (offset < file.size && channel.bufferedAmount < 4 * 1024 * 1024) {
const chunk = file.slice(offset, offset + chunkSize);
chunk.arrayBuffer().then((buf) => channel.send(buf));
offset += chunkSize;
}
}
channel.onbufferedamountlow = sendNext;
sendNext();
La marca de agua de bufferedAmount evita que encoles gigabytes en el buffer de envío de SCTP y te quedes sin memoria. Cuando el buffer cae por debajo de 1 MB, rellena hasta 4 MB. Esto da un rendimiento cercano a 40-80 MB/s en redes locales y 5-20 MB/s en banda ancha residencial típica.
Recibir y escribir bytes en disco
En el lado receptor, escucha el canal entrante:
pc.ondatachannel = ({ channel }) => {
const chunks = [];
let received = 0;
channel.onmessage = ({ data }) => {
chunks.push(data);
received += data.byteLength;
updateProgress(received);
if (received === expectedSize) finish(chunks);
};
};
Para archivos de más de 500 MB, no acumules en RAM. Usa la File System Access API para escribir directamente a disco:
const handle = await window.showSaveFilePicker({ suggestedName: fileName });
const writable = await handle.createWritable();
channel.onmessage = async ({ data }) => writable.write(data);
Firefox y Safari aún no soportan showSaveFilePicker, así que recurre a un Blob + URL.createObjectURL para esos navegadores, limitado a 2 GB.
Enviar metadatos del archivo antes que los bytes
El receptor necesita conocer el nombre, tamaño y tipo MIME del archivo antes de que comience el flujo de bytes. Usa un pequeño handshake JSON en el canal de datos:
channel.send(JSON.stringify({
type: 'metadata', name: file.name,
size: file.size, mime: file.type, sha256: fileHash
}));
Luego cambia al modo binario. El receptor distingue basándose en si data es un string o un ArrayBuffer. Incluye un SHA-256 del archivo para verificación de integridad tras la transferencia, y opcionalmente una huella digital de clave si estás añadiendo AES-256-GCM a nivel de aplicación encima.
Manejar fallos de conexión
Los canales de datos WebRTC fallan de tres formas: ICE nunca completa (NAT, firewall bloqueado), el handshake DTLS falla (desfase de reloj, problemas de certificado), o la conexión se cae a mitad de transferencia (laptop en suspensión, cambio de red). Escucha pc.oniceconnectionstatechange y actúa sobre 'failed' o 'disconnected'. Chrome mantiene 'disconnected' unos segundos antes de pasar a 'failed'; Safari es menos paciente.
En caso de fallo a mitad de transferencia, reinicia ICE sin reconstruir toda la conexión:
await pc.restartIce();
const offer = await pc.createOffer({ iceRestart: true });
// reenviar vía señalización
Si el reinicio falla, recurre a la subida reanudable a través de tu servidor: una arquitectura híbrida P2P + servidor. Algunas herramientas de transferencia WebRTC como Wormhole y justbeamit usan este patrón porque maneja el 15% de condiciones de red donde el P2P puro simplemente no funciona.
Añadir cifrado de extremo a extremo contra tu propio servidor
El DTLS integrado de WebRTC protege contra atacantes de red pero no contra un servidor de señalización malicioso o comprometido. Para cifrado de extremo a extremo real, haz que ambos pares generen un par de claves ECDH P-256, intercambien claves públicas mediante un código fuera de banda corto (QR o frase de 6 palabras), deriven un secreto compartido mediante HKDF-SHA256, y cifren cada mensaje del canal de datos con AES-256-GCM antes de llamar a send. De esa forma, incluso si el servidor de señalización intercambia certificados DTLS, no puede leer tus archivos.
HexaTransfer usa una arquitectura híbrida: ciphertext almacenado en el servidor con AES-256-GCM en el lado del cliente, en lugar de P2P, cambiando la inmediatez por compartición tolerante a la desconexión. Pruébalo en https://hexatransfer.com — gratuito, sin cuenta, hasta 10 GB.
Dónde gana y pierde WebRTC
La transferencia de archivos por WebRTC brilla cuando ambos pares están en línea simultáneamente, cuando la privacidad frente a tu propio servidor importa, y cuando los archivos son suficientemente grandes (más de 100 MB) como para que el coste de ancho de banda del relé sea doloroso. Pierde cuando los usuarios quieren enviar y marcharse, cuando los destinatarios abren el enlace horas después, o cuando los destinatarios están en redes corporativas restrictivas que bloquean STUN y TURN. Para una herramienta de transferencia de propósito general, el P2P puro cubre cómodamente alrededor del 60% de los casos de uso. El otro 40% necesita un fallback respaldado por servidor, que es por qué casi todos los productos de "transferencia P2P" tienen un relé en su arquitectura.
Envía archivos grandes de forma segura con cifrado de extremo a extremo
Transfiere archivos de hasta 10 GB gratis con cifrado de extremo a extremo. Sin necesidad de cuenta. Tus archivos se cifran en tu navegador antes de subirlos: nadie más puede leerlos.
Enviar un archivo