Сигнализация и соединение с использованием шаблона «идеальное согласование»
В этой истории описана реализация очень простой веб-страницы текстового чата с использованием WebRTC для обработки обмена данными между двумя (и только двумя) одноранговыми узлами.
WebRTC — это веб-API, позволяющий осуществлять одноранговый обмен данными без участия стороннего сервера (в процессе обмена данными). Следует отметить, что, насколько я понимаю, требуются следующие сторонние серверы: (а) сервер STUN, позволяющий одноранговым узлам определять, как они могут общаться друг с другом, (б) сигнальный сервер, позволяющий одноранговым узлам обмениваться информация до установления однорангового соединения и, возможно, (c) сервер TURN для поддержки однорангового соединения в сети с ограничениями.
Эта история посвящена:
- Реализация базового сигнального сервера — необходимость обмена информацией, необходимой для установления соединения WebRTC
- Реализация паттерна «Perfect Negotiation» для установления WebRTC-соединения.
Моя мотивация для выполнения работы, представленной здесь, состоит в том, чтобы понять из первых рук некоторые сложности с WebRTC, например, как настроить WebRTC или узнать, когда может потребоваться сервер TURN, а также бросить себе вызов, выполнив еще одну задачу. Демонстрация доступна здесь.
Исходный код выложен под лицензией MIT в моем репозитории GitHub.
Взгляды/мнения, выраженные в этой истории, являются моими собственными. Эта история связана с моим личным опытом и выбором. Рассказ, демонстрации и исходный код предоставляются в надежде на то, что они будут полезны, но без каких-либо гарантий.
Сигнальный сервер
Сигнальный сервер позволяет обоим пирам обмениваться информацией, необходимой для установления соединения WebRTC, а именно (насколько я понимаю) Описанием сеанса (SDP) и Кандидатами ICE. Уже доступны лучшие объяснения по теме, например, здесь или здесь.
Моя цель здесь — представить очень простой сигнальный сервер, который использует четырехсимвольный код для идентификации предложений и ответов, использует протокол http (не веб-сокет), развернут на AWS lambda (т. е. без сервера) и написан на Rust. Описанный здесь сигнальный сервер доступен в репозитории проекта GitHub.
На следующей диаграмме показана последовательность взаимодействия между обоими одноранговыми узлами и сигнальным сервером. В приведенной ниже последовательности узел A делает предложение, которое сервер сигнализации сохраняет и идентифицирует четырехзначным кодом abcd
. Этот код должен быть предоставлен узлу B другим способом, чтобы информация, отправленная узлом B на сигнальный сервер, могла быть связана с предложением узла A. В обычных одноранговых коммуникационных приложениях такая связь, как я предполагаю, устанавливается через имя пользователя или другие идентификаторы.
В приведенной выше последовательности взаимодействия существует три типа информации, которой обменивается и которую хранит сигнальный сервер: (а) предложение, (б) ответ и (в) сообщения.
Структура данных для каждого типа информации показана ниже. Каждая часть информации имеет временную метку, связанную с сохранением времени ее создания. Сигнальный сервер использует метку времени, чтобы понять, когда информация устарела (определяется как 10 минут в настоящее время), поэтому, если сообщение (или ответ) запрашивается через 10 минут, сервер отвечает статусом 404 (не найдено).
Структура данных сообщения хранит описание сеанса и кандидатов ICE в виде строки JSON в поле message
.
Поскольку каждый тип информации отображается и хранится одинаково, реализация использует процедурные макросы Rust, чтобы избежать дублирования кода.
Макросы деривов используются для расширения структур данных возможностями сериализации и десериализации с помощью serde
, а также чтения и записи в формат SQLite с помощью макроса derive-sql
(обсуждается в этой истории).
В следующем фрагменте кода показано объявление структуры данных answer
, дополненное макросами получения и функциями десериализации по умолчанию для полей id
, polite
и timestamp
.
Был разработан набор функциональных макросов, которые преобразуют общие типы запросов (список, получение, публикация и т. д.) в запросы к базе данных, которые сами определяются макросами derive-sql
. Эти макросы, похожие на функции, упрощают реализацию этого типа запроса для сопоставления запросов к базе данных, связанных с каждой структурой данных. Фрагмент кода ниже показывает это сопоставление для структуры данных answer
вместе с реализацией макроса post!
для справки.
Определен набор функциональных макросов для сопоставления лямбда-контекста AWS (из ящиков AWS Rust lambda_http
) с общими типами запросов, определенными выше, с использованием промежуточного перечисления Request
, определенного в файле lambda
rust. Аналогичная карта используется для actix-web
запросов к серверу, так что можно использовать локальный сервер, что особенно полезно во время разработки. Реализация позволяет активировать AWS lambda или actix-web
в качестве дополнительной функции крейта signalling
. Выдержка ниже показывает, что для структуры данных ответа доступны только типы запросов list, post и get, поскольку функции обновления и удаления не требуются.
Особо следует отметить в приведенном выше коде, что вызов list
в строке 14 использует преобразование Request:List
query
в информационную структуру данных, подходящую для использования в запросе к базе данных, чтобы никто не мог получить список всех текущих предложений. Это делается с помощью структуры Info
со следующей реализацией, специфичной для структуры данных ответа — это заставило меня осознать необходимость опций Filter::None
и Filter:All
в derive-sql
:
В приведенном выше примере использование процедурных макросов Rust позволило написать большинство алгоритмов один раз и применить их ко всем структурам данных.
Основная функция лямбда-реализации показана ниже и состоит в выборе на основе пути запроса структуры данных, подходящей для обработки запроса.
Например, запросы с путем, начинающимся с /api/answer
, делегируются структуре данных ответа, описанной выше.
Сигнальный сервер насчитывает около 800 строк кода. Я попытался дать обзор того, как структуры данных и алгоритмы объединяются для обеспечения функциональных возможностей сервера сигнализации. Но не стесняйтесь оставлять комментарии, если что-то неясно, и я постараюсь уточнить/расширить по мере необходимости.
WebRTC
Этот раздел относится к веб-странице, на которой сервер сигнализации используется для обмена информацией, необходимой для установления однорангового соединения с использованием WebRTC.
Цель здесь состоит в том, чтобы реализовать класс Connection
, который (а) инициирует и обрабатывает соединение с использованием WebRTC и (б) отправляет и получает сообщения.
В следующем примере кода описывается интерфейс этого класса Connection
и класса ConnectionBuilder
— шаблон построителя, предложенный для установки методов обратного вызова для обработки ошибок, изменения состояния соединения, закрытия соединения и получения однорангового сообщения. Интерфейс класса Connection
ограничен методами получения соединения id
— этот id
идентифицирует предложение и должен быть предоставлен узлу, отвечающему на соединение — отправке сообщения и закрытию соединения.
Реализация — см. здесь — шаблона построителя включает в себя реализацию методов обратного вызова по умолчанию. Это просто и, следовательно, не обсуждается далее за одним исключением. Построитель имеет статические методы offer
и answer
, которые вызываются для предоставления либо (а) offer
, либо (б) answer
предложения.
В последнем случае метод назначает параметр аргумента id
, который соответствует id
из offer
. Этот id
, также известный как peer_id
, используется повсюду как индикатор того, соответствует ли сторона однорангового соединения offer
— когда peer_id
является null
— или answer
— когда peer_id
не является null
.
Шаблон построителя используется для настройки и создания объекта класса Connection
. Во время создания объекта создается экземпляр RTCPeerConnection
, который соответствует соединению WebRTC между локальным узлом и удаленным узлом.
Также создается экземпляр SignalingChannel
, который обрабатывает связь с сигнальным сервером, описанную выше, для обмена Описанием сеанса (SDP) и Кандидатами ICE между двумя одноранговыми узлами.
Шаблон Perfect Negotiation, предложенный в веб-документах MDN, используется для обработки обмена описаниями сеансов. Этот шаблон предназначен для корректной обработки ситуации, когда оба одноранговых узла отправляют offer
описаний сеансов. В такой ситуации вежливый партнер уступает место offer
описанию сеанса от невежливого партнера.
Реализация шаблона показана ниже:
Три переменные используются для хранения данных и состояний, которые взаимосвязаны с состоянием однорангового соединения.
Переменная making_offer
относится к пограничному случаю, когда партнер запросил описание сеанса offer
, но еще не получил и не обработал его.
Переменная send_icecandidates
указывает, должны ли кандидаты ICE храниться в массиве icecandidates
— когда send_icecandidates
равно false
— или должны отправляться непосредственно при получении. Этот подход позволяет игнорировать кандидатов ICE, сгенерированных вежливым узлом, в случае коллизии предложений — кандидаты ICE генерируются повторно после разрешения коллизии, т. е. когда вежливый узел сгенерировал описание сеанса answer
.
Шаблон Perfect Negotiation эффективен, когда существует риск того, что оба одноранговых узла попытаются открыть каналы данных и отправить конфликтующее offer
описание. Текущий рабочий процесс имеет четко определенные одноранговые узлы, предлагающие и отвечающие, и, следовательно, риск конфликта устранен, но, поскольку это может измениться в будущем, а идеальный шаблон согласования является гибким, эта реализация сохраняется.
Поскольку изображение говорит тысячу слов, на видео ниже показано соединение со стороны инициатора (peer1) и получателя (peer2) с использованием демо, развернутого на https://webrtc.charentenay.me/.
Peer2 подключается, назначая код предложения, возвращенный Peer1 сигнальным сервером, который идентифицирует предложение Peer1.
Хотя этой демонстрации достаточно, чтобы понять, как работает WebRTC, у нее есть несколько особенностей — отсутствие кнопки завершения вызова, обработка ошибок, возможно, нуждается в улучшении — которые можно было бы улучшить со временем и усилиями…
В развернутой демонстрации используется Open Relay Free WebRTC Turn Server.
Спасибо за прочтение. Оставайтесь с нами, чтобы узнать больше.