Стандартный путь переслать файл через интернет это облако: загрузить на сервер, получить ссылку, отправить ссылку. Работает везде, но съедает место на диске, требует доверия к хранилищу и гоняет байты два раза. WebRTC предлагает другой маршрут: браузеры соединяются напрямую, файл идёт от человека к человеку, а серверу остаётся роль знакомящего свата, который исчезает сразу после рукопожатия.

Такой обмен особенно хорош там, где ценны приватность и скорость: соседи в одной сети получают файл на скорости локальной сети, конфиденциальный документ не остаётся на чужом диске, а огромный архив не требует тарифа на хранение. Ниже разобраны все слои прямого обмена: канал, сигналинг, нарезка на части, сборка, контроль переполнения и честные ограничения технологии.

Зачем отдавать файлы напрямую когда рядом есть облака и что это даёт

Облачная пересылка стоит трёх ожиданий: загрузка на сервер, уведомление получателю, скачивание с сервера. На гигабайтном файле это гигабайт вверх и гигабайт вниз, даже если оба человека сидят в одном офисе. Прямой канал соединяет браузеры без посредника, и файл проходит путь один раз, между двумя машинами, минуя диски чужих дата-центров.

Приватность получается не декларацией, а конструкцией: канал зашифрован стандартными средствами транспорта, содержимое недоступно ни сигналинг-серверу, ни провайдеру, ни владельцу TURN-узла, который в идеале вообще не участвует. Для документов, которые не должны храниться на сторонних площадках даже сутки, это архитектурное свойство важнее любых обещаний в политике конфиденциальности.

Экономика тоже говорит в пользу прямого пути: отправитель не платит за хранение, сервис не несёт затрат на диски под чужие файлы, а масштабирование не упирается в трафик через центральный узел. Сервис прямого обмена растёт вместе с числом пар, а не с числом байтов, и это редкий случай, когда интересы пользователей и владельцев совпадают полностью.

Ограничения честно стоят рядом с достоинствами: оба человека обязаны быть в сети одновременно, пауза на неделю невозможна без третьего хранилища, а мобильные сети с частой сменой подключения разрывают долгие передачи. Прямой обмен это инструмент для живого диалога, а не для отложенной отправки.

Канал RTCDataChannel и надёжная побайтовая пересылка поверх DTLS

Транспортную основу прямого обмена даёт канал, который браузер создаёт на пиринговом соединении. Канал настраивается как надёжный и упорядоченный: потерянные байты пересылаются повторно, порядок фрагментов сохраняется, и на уровне приложения это выглядит как честный поток, в котором ничего не теряется и не переставляется.

const pc = new RTCPeerConnection({
  iceServers: [{ urls: "stun:stun.example.net:3478" }]
});

const dc = pc.createDataChannel("files", { ordered: true });

dc.onopen = () => sendHandshake();
dc.onmessage = (e) => handleChunk(e.data);

Шифрование встроено в сам транспорт: канал работает поверх протокола защищённых дейтаграмм, и отдельного слоя шифрования приложению строить не нужно. Для файлов это критично, потому что произвольные двоичные фрагменты без защиты летели бы через ту же сеть, что и открытый трафик.

Канал принимает и текст, и байты, поэтому протокол обмена строится просто: первой уходит строка с описанием файла, за ней фрагменты бинарного содержимого, в конце маркер завершения. Различение по типу сообщения снимает необходимость в собственной обёртке и упрощает код до читаемого минимума.

Байты идут в канал как есть, без текстовой упаковки, потому что перекодировка в текст раздувает объём примерно на треть и съедает процессор на обеих сторонах. Канал умеет бинарные фрагменты с рождения, и честная реализация пользуется этим, а не переизобретает упаковку.

Помимо надёжного режима канал умеет ненадёжный, когда потерянные фрагменты не пересылаются. Для файлов такой режим не подходит, зато понимание этого различия избавляет от соблазна ускорить передачу в ущерб целостности.

Сигналинг соединения и роль сервера как места встречи двух браузеров

Чтобы два браузера нашли друг друга, им нужен один обмен короткими служебными сообщениями: описанием сессии и списком сетевых кандидатов. Этот обмен и называют сигналингом, и вот здесь в проекте появляется сервер, обычно пара страниц и сокет-канал, которая сводит людей по короткому коду комнаты и после соединения перестаёт участвовать.

Схема знакома каждому, кто видел пароли из шести символов: один человек делится кодом, второй вводит, сервер передаёт служебные сообщения туда-обратно, и как только пиринговое соединение установлено, поток файлов идёт напрямую. Красиво в этой схеме то, что сервер не видит ни байта содержимого, его роль исчерпана в первые секунды.

Жизнеспособность соединения держится на обходе сетевых ограничений. Простые сети соединяются через серверы обнаружения, симметричные ограничения пробиваются ретрансляционным узлом, и тогда трафик идёт через него, но зашифрованным туннелем, где узел видит лишь обёртку. Ретрансляция это плата за совместимость, и на неё стоит закладывать бюджет в тяжёлых корпоративных сетях.

Плата за простоту сигналинга это его надёжность: сервер комнат обязан переживать обрывы и восстановления страниц, и правила повторного входа в комнату пишутся заранее. Прямой канал стабилен, а вот знакомство перед ним обязано быть неприхотливым.

Нарезка файла чанками описание передаваемого и сборка на принимающей стороне

Файл не отправляется целиком: канал ограничен по размеру одного сообщения, и рабочая практика нарезает содержимое на фрагменты по шестнадцать килобайт. Описание файла уходит первым сообщением: имя, размер, тип содержимого, и принимающая сторона сразу знает, сколько байт ждать и как назвать результат.

const CHUNK = 16384;

async function sendFile(file, dc) {
  dc.send(JSON.stringify({ name: file.name, size: file.size }));

  let offset = 0;
  while (offset < file.size) {
    const slice = file.slice(offset, offset + CHUNK);
    dc.send(await slice.arrayBuffer());
    offset += CHUNK;
    if (dc.bufferedAmount > 4 * 1024 * 1024) {
      await drainBuffer(dc);
    }
  }
}

function drainBuffer(dc) {
  return new Promise((resolve) => {
    dc.bufferedAmountLowThreshold = 1024 * 1024;
    dc.onbufferedamountlow = () => resolve();
  });
}

Приём зеркально прост: строка разбирается как описание, байты копятся в список, счётчик растёт с каждым фрагментом, и когда он сравнивается с заявленным размером, содержимое собирается в единый объект и предлагается к сохранению.

let meta = null;
let received = 0;
const parts = [];

dc.onmessage = (e) => {
  if (typeof e.data === "string") {
    meta = JSON.parse(e.data);
    return;
  }
  parts.push(e.data);
  received += e.data.byteLength;
  if (meta && received === meta.size) {
    offerDownload(new Blob(parts), meta.name);
  }
};

Порядок фрагментов гарантирует канал, поэтому пересборка тривиальна и не требует сортировки. Число шестнадцать килобайт не необычное, а проверенное практикой: большие фрагменты упираются в лимиты реализации, маленькие множат накладные расходы на служебные заголовки.

Контроль переполнения буфера bufferedAmount и честный прогресс передачи

Канал внутри браузера снабжён буфером отправки, и если заливать фрагменты быстрее, чем сеть уносит, буфер растёт без предела, вкладка съедает память, а система ложится. Спасает счётчик заполнения: он показывает, сколько байтов уже принято к отправке, и чтение этого счётчика между фрагментами превращает заливку в честный поток со скоростью сети.

Порог и событие опустошения делают контроль аккуратным: отправитель льёт фрагменты, пока заполнение ниже порога, замирает и ждёт события, когда буфер стечёт, и продолжает. Код из предыдущего раздела именно так и устроен, и этот рисунок повторяется почти в каждой серьёзной реализации прямого обмена.

Прогресс по отправке считается не по числу отправленных вызовов, а по подтверждённому потоку: сколько байтов ушло из буфера, столько честно передано. Разница между этими числами на быстрой машине и медленной сети составляет минуты, и интерфейс, который её игнорирует, показывает пользователю ложь в красивой полоске.

Приёмная сторона считает прогресс по собранным фрагментам, и на больших файлах полезно показывать обе полоски: сколько ушло у отправителя и сколько дошло до получателя. Расхождение полосок на живом сеансе само по себе сигналит о качестве сети между людьми.

Продвинутые реализации добавляют докачку после обрыва: при восстановлении соединения принимающая сторона сообщает, сколько байтов уже собрано, и отправитель продолжает с нужного смещения. Фрагменты фиксированного размера делают смещение тривиальным, и потеря сети превращается из катастрофы в паузу.

Ограничения NAT TURN лимиты канала и сценарии где прямой обмен не годится

Главное ограничение это одновременность: обмен живёт, пока живы обе стороны. Прервался браузер, уехал в сон ноутбук, оборвалась мобильная сеть, и передача останавливается без надежды на докачку, потому что продолжить сеанс с новыми кандидатами это отдельная и сложная задача, которую решают немногие сервисы.

Второе ограничение это скорость отдачи: домашние каналы асимметричны, и upload отправителя часто в разы уже download получателя, поэтому прямая передача через интернет упирается в самый медленный конец. Внутри одной локальной сети этого нет, и прямые каналы там показывают честные мегабайты в секунду.

Третье ограничение это ретрансляция в тяжёлых сетях: когда прямая дорога не находится, поток идёт через TURN, и там уже появляется цена за трафик и зависимость от чужого узла. Продукт обязан заранее понимать долю сессий с ретрансляцией и держать её в экономических рамках.

Четвёртое ограничение это мобильные браузеры: сворачивание вкладки замирает выполнение, таймеры засыпают, и долгая передача на телефоне требует либо экрана без затемнения, либо честного предупреждения. Продуктовый ответ это сегментация сценариев: короткие файлы на мобильных, длинные на настольных машинах.

Продуктовые сценарии от короткого кода комнаты до локальной сети устройств

Продукт прямого обмена собирается вокруг сцены знакомства: короткий код, кнопка выбрать файл, полоска прогресса и одна кнопка сохранить. Всё остальное это удержание соединения и честные состояния: ожидание второй стороны, передача, завершение, потеря связи. Продукт, который показывает эти состояния словами, воспринимается как надёжный даже в моменты слабой сети.

Локальный сценарий заслуживает отдельного внимания: устройства в одной сети обнаруживают друг друга быстрее, чем набирается код, и обмен между ноутбуком и телефоном в комнате укладывается в три касания. Сервисы такого жанра экономят людям флешки и кабели, и именно тут прямые каналы показывают максимум скорости.

Передача нескольких файлов оформляется как очередь: опись уходит целиком, файлы идут по одному, и интерфейс показывает общий прогресс по списку. Папки передаются как дерево описей либо собираются в единый архив на лету, и оба подхода умещаются в один и тот же протокол фрагментов.

Порядок сборки сервиса прямого обмена свёрнут в короткую лестницу:

  1. поднять сигналинг по коротким кодам комнат с восстановлением после обрыва;
  2. собрать пиринговое соединение и канал с надёжным упорядоченным режимом;
  3. договориться о протоколе: заголовок с именем и размером, фрагменты, маркер конца;
  4. встроить контроль буфера с порогом и событием опустошения;
  5. показать честный прогресс по подтверждённому потоку на обеих сторонах;
  6. обработать состояния связи как первый класс интерфейса;
  7. измерить долю сессий через ретрансляцию и заложить её в экономику.

Прямой обмен файлами через WebRTC возвращает интернету старую честную идею: соединить двух людей без надзирателя посередине. Технология не заменит облака для отложенных отправок, зато там, где важны приватность, скорость и отсутствие лишних хранений, она делает то, что облаку недоступно по построению, и потому занимает своё место в продуктовом арсенале.