Одноранговые соединения — это часть спецификации WebRTC, которая описывает соединение двух приложений на разных компьютерах для обмена данными по одноранговому протоколу. Обмен данными между одноранговыми узлами может осуществляться посредством видео, аудио или произвольных двоичных данных (для клиентов, поддерживающих API RTCDataChannel ). Чтобы определить, как два одноранговых узла могут соединиться, оба клиента должны предоставить конфигурацию сервера ICE. Это может быть STUN- или TURN-сервер, и их роль заключается в предоставлении каждому клиенту ICE-кандидатов, которые затем передаются удалённому одноранговому узлу. Такая передача ICE-кандидатов обычно называется сигнализацией.
Сигнализация
Спецификация WebRTC включает API для взаимодействия с сервером ICE (Internet Connectivity Establishment), но компонент сигнализации в неё не входит. Сигнализация необходима для того, чтобы два одноранговых узла могли сообщать друг другу, как им следует подключаться. Обычно это решается с помощью обычного веб-API на основе HTTP (например, REST-сервиса или другого механизма RPC), где веб-приложения могут передавать необходимую информацию до установления соединения между одноранговыми узлами.
Следующий фрагмент кода показывает, как можно использовать эту вымышленную сигнальную службу для асинхронной отправки и получения сообщений. Он будет использоваться в остальных примерах данного руководства по мере необходимости.
// Set up an asynchronous communication channel that will be
// used during the peer connection setup
const signalingChannel = new SignalingChannel(remoteClientId);
signalingChannel.addEventListener('message', message => {
// New message from remote client received
});
// Send an asynchronous message to the remote client
signalingChannel.send('Hello!');
Сигнализацию можно реализовать разными способами, и спецификация WebRTC не отдает предпочтения какому-либо конкретному решению.
Инициирование одноранговых соединений
Каждое одноранговое соединение обрабатывается объектом RTCPeerConnection . Конструктор этого класса принимает в качестве параметра один объект RTCConfiguration . Этот объект определяет настройку однорангового соединения и должен содержать информацию об используемых серверах ICE.
После создания RTCPeerConnection необходимо сформировать предложение или ответ SDP, в зависимости от того, являемся ли мы вызывающим или принимающим пиром. После создания предложения или ответа SDP его необходимо отправить удалённому пиру по другому каналу. Передача объектов SDP удалённым пирам называется сигнализацией и не рассматривается в спецификации WebRTC.
Чтобы инициировать установку соединения с одноранговым узлом с вызывающей стороны, мы создаём объект RTCPeerConnection , а затем вызываем createOffer() для создания объекта RTCSessionDescription . Это описание сеанса устанавливается как локальное с помощью setLocalDescription() и затем отправляется по нашему сигнальному каналу принимающей стороне. Мы также настраиваем прослушиватель нашего сигнального канала, который будет ожидать ответа от принимающей стороны на предложенное нами описание сеанса.
async function makeCall() {
const configuration = {'iceServers': [{'urls': 'stun:stun.l.google.com:19302'}]}
const peerConnection = new RTCPeerConnection(configuration);
signalingChannel.addEventListener('message', async message => {
if (message.answer) {
const remoteDesc = new RTCSessionDescription(message.answer);
await peerConnection.setRemoteDescription(remoteDesc);
}
});
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
signalingChannel.send({'offer': offer});
}
На принимающей стороне мы ожидаем входящего предложения, прежде чем создать экземпляр RTCPeerConnection . После этого мы устанавливаем полученное предложение с помощью setRemoteDescription() . Затем мы вызываем createAnswer() для создания ответа на полученное предложение. Этот ответ устанавливается как локальное описание с помощью setLocalDescription() и затем отправляется вызывающей стороне через наш сигнальный сервер.
const peerConnection = new RTCPeerConnection(configuration);
signalingChannel.addEventListener('message', async message => {
if (message.offer) {
peerConnection.setRemoteDescription(new RTCSessionDescription(message.offer));
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
signalingChannel.send({'answer': answer});
}
});
После того, как оба пира установили описания сеансов локального и удалённого, им известны возможности удалённого пира. Это не означает, что соединение между пирами готово. Для этого нам необходимо собрать ICE-кандидатов на каждом пире и передать их (по сигнальному каналу) другому пиру.
Кандидаты ICE
Прежде чем два одноранговых узла смогут взаимодействовать через WebRTC, им необходимо обменяться информацией о подключении. Поскольку сетевые условия могут меняться в зависимости от ряда факторов, для поиска возможных кандидатов на подключение к одноранговому узлу обычно используется внешняя служба. Эта служба называется ICE и использует сервер STUN или TURN. STUN означает Session Traversal Utilities for NAT и обычно используется косвенно в большинстве приложений WebRTC.
TURN (Traversal Using Relay NAT) — более продвинутое решение, включающее протоколы STUN, и большинство коммерческих сервисов на базе WebRTC используют TURN-сервер для установления соединений между одноранговыми узлами. API WebRTC напрямую поддерживает как STUN, так и TURN и объединяется под более общим термином «Установление подключения к Интернету». При создании WebRTC-подключения мы обычно указываем один или несколько ICE-серверов в конфигурации объекта RTCPeerConnection .
Капелька льда
После создания объекта RTCPeerConnection базовая платформа использует предоставленные серверы ICE для сбора кандидатов на установление соединения (ICE-кандидатов). Событие icegatheringstatechange объекта RTCPeerConnection сигнализирует о состоянии сбора ICE ( new , gathering или complete ).
Хотя одноранговый узел может дождаться завершения сбора ICE, обычно гораздо эффективнее использовать технику «постепенного получения данных» и передавать каждый ICE-кандидат удалённому одноранговому узлу по мере его обнаружения. Это значительно сократит время настройки соединения между одноранговыми узлами и позволит начать видеозвонок с меньшими задержками.
Чтобы собрать ICE-кандидатов, просто добавьте прослушиватель события icecandidate . Событие RTCPeerConnectionIceEvent отправленное этим прослушивателем, будет содержать свойство candidate , представляющее нового кандидата, которого следует отправить удалённому узлу (см. раздел «Сигнализация»).
// Listen for local ICE candidates on the local RTCPeerConnection
peerConnection.addEventListener('icecandidate', event => {
if (event.candidate) {
signalingChannel.send({'new-ice-candidate': event.candidate});
}
});
// Listen for remote ICE candidates and add them to the local RTCPeerConnection
signalingChannel.addEventListener('message', async message => {
if (message.iceCandidate) {
try {
await peerConnection.addIceCandidate(message.iceCandidate);
} catch (e) {
console.error('Error adding received ice candidate', e);
}
}
});
Соединение установлено
После получения ICE-кандидатов следует ожидать, что состояние нашего однорангового соединения в конечном итоге изменится на «подключено». Чтобы определить это, мы добавляем прослушиватель в наш RTCPeerConnection , который отслеживает события connectionstatechange .
// Listen for connectionstatechange on the local RTCPeerConnection
peerConnection.addEventListener('connectionstatechange', event => {
if (peerConnection.connectionState === 'connected') {
// Peers connected!
}
});