Реалізація WebRTC для P2P-обміну даними на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.
Розробка та обслуговування будь-яких видів сайтів:
Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація WebRTC для P2P-обміну даними на сайті
Складна
~5 робочих днів
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація WebRTC для P2P-обміну даними на сайті

WebRTC не обмежується відео. RTCDataChannel — це низкоуровневий канал даних між браузерами з задержками порядку 20–50 мс, що працює поверх SCTP/DTLS без серверного посередника для передачи даних. Файлообменник, спільний редактор, ігровий бекенд, mesh-синхронізація — все це будується на DataChannel.

RTCDataChannel проти WebSocket

Параметр WebSocket RTCDataChannel
Маршрут даних Клієнт → Сервер → Клієнт Клієнт → Клієнт (P2P)
Задержка 50–200 мс (через сервер) 20–60 мс (прямий канал)
Надійність TCP (упорядкований, надійний) Настраиваемая
Шифрування TLS DTLS (обов'язково)
Навантаження на сервер Весь трафік Лише сигнализація

Головна перевага — дані не проходять через ваші сервери. Критично для файлообміну, E2E-шифрованих чатів, приватних ігрових сесій.

Створення DataChannel

// Інціатор (offerer)
const pc = new RTCPeerConnection({ iceServers: [/* ... */] });

const channel = pc.createDataChannel('files', {
  ordered: true,      // TCP-семантика
  // maxRetransmits: 0,  // UDP-семантика
  // maxPacketLifeTime: 100,
});

channel.binaryType = 'arraybuffer';
channel.bufferedAmountLowThreshold = 65536; // 64 KB

channel.onopen = () => console.log('DataChannel open');
channel.onclose = () => console.log('DataChannel closed');
channel.onmessage = (e) => handleMessage(e.data);

// Отримувач (answerer)
pc.ondatachannel = (e) => {
  const remoteChannel = e.channel;
  remoteChannel.onmessage = (e) => handleMessage(e.data);
};

Режими надійності

SCTP під DataChannel дозволяє настройки семантики доставки:

  • ordered + reliable (за замовчуванням) — гарантований порядок, ретрансмісія. Для файлів, чатів.
  • unordered + unreliable (maxRetransmits: 0) — UDP-like. Для ігрових позицій, курсорів.
  • ordered + maxPacketLifeTime — пакет живе N мс, потім виключається. Для голосових команд, введення з клавіатури.

Передача файлів через DataChannel

Браузерний DataChannel обмежений розміром повідомлення ~256 KB. Файли потрібно чанковать:

const CHUNK_SIZE = 64 * 1024; // 64 KB

async function sendFile(channel, file) {
  const metadata = JSON.stringify({
    name: file.name,
    size: file.size,
    type: file.type,
    chunks: Math.ceil(file.size / CHUNK_SIZE),
  });

  channel.send(metadata);

  const buffer = await file.arrayBuffer();
  let offset = 0;

  function sendNextChunk() {
    while (offset < buffer.byteLength) {
      // Контроль перегрузки буфера
      if (channel.bufferedAmount > channel.bufferedAmountLowThreshold * 2) {
        channel.onbufferedamountlow = () => {
          channel.onbufferedamountlow = null;
          sendNextChunk();
        };
        return;
      }

      const chunk = buffer.slice(offset, offset + CHUNK_SIZE);
      channel.send(chunk);
      offset += CHUNK_SIZE;
    }
    channel.send(JSON.stringify({ type: 'transfer-complete' }));
  }

  sendNextChunk();
}

Отримувач збирає чанки:

let receivedSize = 0;
let receivedChunks = [];
let fileMetadata = null;

channel.onmessage = (e) => {
  if (typeof e.data === 'string') {
    const msg = JSON.parse(e.data);
    if (msg.name) {
      fileMetadata = msg;
    } else if (msg.type === 'transfer-complete') {
      const blob = new Blob(receivedChunks);
      triggerDownload(blob, fileMetadata.name);
    }
  } else {
    receivedChunks.push(e.data);
    receivedSize += e.data.byteLength;
    updateProgress(receivedSize / fileMetadata.size);
  }
};

Прогрес та швидкість

const startTime = Date.now();
let lastSize = 0;

function updateProgress(ratio) {
  const elapsed = (Date.now() - startTime) / 1000;
  const speed = (receivedSize - lastSize) / 1024; // KB/s за останній тік
  lastSize = receivedSize;

  progressBar.style.width = `${ratio * 100}%`;
  speedLabel.textContent = `${(receivedSize / elapsed / 1024).toFixed(1)} KB/s`;
  etaLabel.textContent = `${((fileMetadata.size - receivedSize) / (receivedSize / elapsed)).toFixed(0)} сек`;
}

Многоканальна архітектура

Для різних типів даних — різні канали з різними настройками надійності:

const channels = {
  control: pc.createDataChannel('control', { ordered: true }),
  files:   pc.createDataChannel('files',   { ordered: true }),
  cursor:  pc.createDataChannel('cursor',  { ordered: false, maxRetransmits: 0 }),
  chat:    pc.createDataChannel('chat',    { ordered: true }),
};

E2E-шифрування поверх DTLS

DataChannel вже шифрований DTLS. Для додаткового E2E (де ключи не видні навіть серверу) використовують Web Crypto API:

// Обмін ключами через ECDH у процесі сигнализації
const keyPair = await crypto.subtle.generateKey(
  { name: 'ECDH', namedCurve: 'P-256' },
  false, ['deriveKey']
);

// Публічний ключ відправляється через сигнальний сервер
const publicKeyExported = await crypto.subtle.exportKey('raw', keyPair.publicKey);

// Після отримання публічного ключа партнера
const sharedKey = await crypto.subtle.deriveKey(
  { name: 'ECDH', public: partnerPublicKey },
  keyPair.privateKey,
  { name: 'AES-GCM', length: 256 },
  false, ['encrypt', 'decrypt']
);

Mesh P2P для кількох учасників

До 4–6 учасників допустима full-mesh топологія (кожен з кожним):

class MeshNetwork {
  constructor(signalSocket) {
    this.peers = new Map(); // userId -> RTCPeerConnection
    this.channels = new Map();
    this.signal = signalSocket;
  }

  async connectTo(userId) {
    const pc = new RTCPeerConnection(ICE_CONFIG);
    this.peers.set(userId, pc);

    const channel = pc.createDataChannel('mesh');
    this.channels.set(userId, channel);
    channel.onmessage = (e) => this.onData(userId, e.data);

    const offer = await pc.createOffer();
    await pc.setLocalDescription(offer);
    this.signal.emit('offer', { to: userId, offer });
  }

  broadcast(data) {
    const message = JSON.stringify(data);
    this.channels.forEach(ch => {
      if (ch.readyState === 'open') ch.send(message);
    });
  }
}

Типічні застосування

Спільний whiteboard — позиції курсорів через unreliable канал (~20 мс задержка), операції рисування через reliable. Дельта-синхронізація раз у 100 мс.

Ігровий матч 1v1 — стан гравця (позиція, дія) через unordered канал 60 разів на секунду. Критичні события (попадання, смерть) через ordered reliable.

Peer-to-peer чат з файлами — текст через ordered reliable, файли чанками з контролем буфера, превью зображень як base64 у JSON.

Screenshare + annotation — відеопоток через RTCPeerConnection, анотації (координати кліків) через DataChannel.

Обмеження та підводні камені

  • Safari не підтримує bufferedAmountLowThreshold у старих версіях — потрібен polling через setInterval
  • Firefox максимальний розмір повідомлення 256 KB; Chrome варіюється залежно від версії
  • Мобільні браузери можуть закривати DataChannel при уходе додатку в фон — потрібна логіка переподключення
  • При втраті з'єднання iceConnectionState === 'failed' — автоматичного восстановлення нема, потрібен явний reconnect

Терміни

  • Базовий файлообменник P2P (2 учасники) — 3–4 дні
  • Многопользовательський mesh з кількома каналами — 1–2 тижні
  • E2E-шифрування + ключевий обмін — плюс 3–4 дні
  • Спільний редактор / whiteboard поверх DataChannel — 2–4 тижні (включаючи логіку CRDT/OT)