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

Фрагменты кода из этой статьи построены поверх фрагментов из Части 1.

Обмен сообщениями

В предыдущей статье я просто описал основы настройки соединения WebRTC. Я решил добавить в эту статью часть об обмене сообщениями, чтобы постепенно наращивать сложность.

В этой демонстрации вы увидите дополнительно в консоли несколько текстовых сообщений, которые отправляются с одной вкладки на другую.

Вот каталог в моем репозитории GitHub для кода в этом разделе статьи.

Вызывающий узел

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

После того, как ответ SDP был получен от другого узла, и после того, как соединение было установлено, мы почти готовы отправить сообщение. Но сначала требуется, чтобы dataChannel имел состояние «открыто». Поэтому нам нужно дождаться этого асинхронного события, иначе мы получим ошибку.

Вызываемый узел

На стороне вызываемого объекта после установления соединения, прежде чем мы сможем отправить сообщение, нам нужно получить указатель на экземпляр dataChannel, созданный RTCPeerConnection. Мы извлекаем этот экземпляр, ожидая события, а также регистрируем для него обработчик событий для новых сообщений.

В отличие от вызывающего узла, нам не нужно ждать его открытия, так как он уже создан в «открытом» состоянии. Теперь мы свободны, чтобы отправить любое сообщение, которое мы хотим.

И, наконец, небольшой рефакторинг файла common.js.

Отступление. Обмен SDP без кандидатов ICE

Одна вещь, которая меня смущала некоторое время, была схема обмена кандидатами ICE (информация, которая сообщает другому пиру, как можно связаться с текущим).

До сих пор я показывал вам фрагменты кода, которые устанавливают localDescription, затем ждут, пока будут собраны кандидаты ICE, и только после этого отправляют предложение другому узлу.

В этом примере я поделюсь с вами тем, что обмен кандидатами ICE также может происходить без проблем после обмена SDP. И вы увидите, что достаточно просто обменяться с одного пира на другой, чтобы установить соединение хотя бы локально.

Почему вы рассматриваете обмен кандидатами SDP и ICE по отдельности?

Ну, насколько я понимаю, это всего лишь метод оптимизации. Если вы хотите включить кандидатов ICE в SDP, вам придется подождать несколько секунд, иначе обмен кандидатами ICE будет выполняться асинхронно.

Вот каталог в моем репозитории GitHub для кода в этом разделе статьи.

Вызывающий узел

В этом фрагменте вы можете видеть, что я удалил строку кода, ожидающую кандидатов ICE, и предложение, отправляемое другому узлу, — это просто SDP, возвращаемый createOffer .

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

Вызываемый узел

Здесь я добавил новую функцию для десериализации и сохранения кандидатов ICE от вызывающего узла.

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

Ниже вы увидите новую функцию, добавленную в common.js для сбора кандидатов ICE.

Объединение файлов вызывающего и вызываемого абонентов

До сих пор мы хранили два одноранговых узла в отдельных файлах. Один из естественных способов упростить код — объединить две ветки в один файл.

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

Вот каталог в моем репозитории GitHub для кода в этом разделе статьи.

Здесь не так много логики. Это почти тот же код из предыдущих частей, за исключением того, что теперь каждый пир может генерировать предложение.

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

async function start() {
    const [peerConnection, dataChannel] = initializeBeforeCreatingOffer()
    await prepareOfferSDP(peerConnection)
const remoteOfferString = prompt("Peer offer (skip if caller peer)");
    if (remoteOfferString) {
        await beCallee(remoteOfferString, peerConnection, dataChannel)
    } else {
        await beCaller(peerConnection, dataChannel)
    }
}

Выводы

В этой статье я постепенно добавлял сложности, добавляя обмен сообщениями, обмен кандидатами ICE, и я объединил два одноранговых узла в одном файле, но с двумя ветвями.

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