Сигнализация и соединение с использованием шаблона «идеальное согласование»

В этой истории описана реализация очень простой веб-страницы текстового чата с использованием 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.

Спасибо за прочтение. Оставайтесь с нами, чтобы узнать больше.