Коллеги, здарова!
Меня зову Храмов Максим и я с командой делаем сервис для онлайн звонков и видео конференций с ИИ, приложение по итогам звонка составляет summary где отражаются какие были проблемы или темы озвучены, к каким результатам пришли, и все такое, в общем помогает фиксировать ключевые решения live-call. И мы хотели бы рассказать подробнее о технической части этого проекта, как он работает, как мы его деплоим, какая инфраструктура используется и как мы стараемся обеспечить отказоустойчивость, чтобы вы всегда могли проводить созвоны без проблем. Мы хотим начать с самого низкоуровневого решения и рассказать о том, как работает WebRTC, какие механизмы используются для создания соединения и как это все работает.
Технология довольно обширная и рассказывать можно бесконечно, но для написания этой статьи мы ставим цель, чтобы вы поняли все необходимое за наименьшее время.
Развертывание и поддержание работы инфраструктуры для WebRTC в режиме реального времени задача не самая простая. Звонки и видеоконференции требуют минимальных сетевых задержек, а также возможности прохождения UDP пакетов через NAT разных видов. Сегодня сделаем небольшое введение в механизм работы LiveKit сервера и поговорим о том, как работают современные онлайн звонки, видеоконференции и войс чаты в играх.
Обзор WebRTC:
Технология, благодаря которой появляется возможность одноранговой передачи видео и аудио называется WebRTC, а расшифровывается как Web Real-Time Communication. Для начала давайте разберемся что значит одноранговой. Обычно WebRTC реализует только Peer to Peer соединение между двумя клиентами, ему важно выбрать наиболее кротчайший и быстрый путь.

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

Сами STUN серверы очень легковесные. Можно использовать публичные, например stun.l.google.com, их много. Либо можно поднимать свой. Стоит ли запускать свой собственный STUN? Сложный вопрос, для большинства случаев достаточно общедоступных серверов. STUN не хранит состояние и не требует больших ресурсов. Клиент формирует запрос размером в 20 байт, а в ответ получает примерно 48 байт. Таким образом можно легко обрабатывать миллионы запрос в секунду, хоть на одном ядре.
TURN:
TURN это сервер-посредник для WebRTC, если не получилось соединится через NAT. Полное название: Traversal Using Relays around NAT.
Такое может произойти если используется symmetric nat.
Обычный NAT работает следующим образом: На маршрутизаторе он составляет таблицу соответствий
192.168.1.11:5000 <-> 93.184.10.20:62000192.168.1.12:5001 <-> 93.184.10.20:62001
И при обычном соединении алгоритм выглядит следующим образом
Допустим, ПК открывает сайт:
пк: 192.168.1.11:5000Сайт: 142.250.185.14:443
Роутер создаёт запись:
192.168.1.11:5000 -> 93.184.10.20:62000
И отправляет пакет наружу уже так:
93.184.10.20:62000 -> 142.250.185.14:443
Сайт отвечает:
142.250.185.14:443 -> 93.184.10.20:62000
Роутер смотрит таблицу и понимает:
93.184.10.20:62000 = 192.168.1.11:5000
И отдает пакет ПК.
Симметричный NAT не позволяет устанавливать p2p соединения, потому что создает разный внешний порт для каждого удаленного адрес. (с оговорками)
Допустим ПК открывает сайт Google
ПК: 192.168.1.11:5000Сайт: 142.250.185.14:443Роутер: 93.184.10.20:62000
Роутер создаёт NAT-запись:
192.168.1.11:5000 -> 142.250.185.14:443
То есть наружу пакет уходит так:
93.184.10.20:62000 -> 142.250.185.14:443
Сайт отвечает:
142.250.185.14:443 -> 93.184.10.20:62000
Роутер смотрит таблицу:
93.184.10.20:62000разрешён только для 142.250.185.14:443и ведёт внутрь к 192.168.1.11:5000
И отдаёт пакет ПК. Пока все нормально.
Теперь ПК с того же адреса:
ПК: 192.168.1.11:5000Другой сайт: 1.1.1.1:443
При обычном NAT внешний порт мог бы остаться тем же.
Но симметричный NAT создаёт новую запись, потому что внешний адрес другой:
192.168.1.11:5000 -> 1.1.1.1:443=93.184.10.20:62001
Пакет наружу:
93.184.10.20:62001 -> 1.1.1.1:443
Ответ:
1.1.1.1:443 -> 93.184.10.20:62001
Роутер отдаёт внутрь:
93.184.10.20:62001 = 192.168.1.11:5000
В итоге у роутера таблица не просто такая:
192.168.1.11:5000 -> 93.184.10.20:62000
А такая:
192.168.1.11:5000 + 142.250.185.14:443=93.184.10.20:62000192.168.1.11:5000 + 1.1.1.1:443=93.184.10.20:62001192.168.1.11:5000 + 8.8.8.8:443=93.184.10.20:62002
То есть внешний порт зависит от того, куда именно ПК отправляет пакет.
Теперь давайте рассмотрим пример с WebRTC:
ПК спрашивает STUN:
ПК: 192.168.1.11:5000STUN: 74.125.140.127:19302
Симметричный NAT создаёт запись:
192.168.1.11:5000 -> 74.125.140.127:19302=93.184.10.20:62000
STUN отвечает ПК:
Ты снаружи выглядишь как:93.184.10.20:62000
ПК передаёт этот адрес другому клиенту2:
Мой адрес: 93.184.10.20:62000
Клиент2 пытается отправить пакет:
Клиент2 -> 93.184.10.20:62000
Но роутер Клиент1 смотрит таблицу и говорит:
62000 разрешён только для STUN-сервера:74.125.140.127:19302Клиент2 — это другой IP:port.Не пускаю.
Пакет Клиента2 дропаетcz, спасибо NAT.
Для того чтобы этого избежать оба участника начинают отправлять свои медиа пакетики на TURN-сервер. А он уже распределяет эти пакеты между ними. Естественно, это приводит к задержкам и большим нагрузкам, вопрос отказоустойчивости тоже стоит ребром, потому что вывести на обслуживание TURN сервер уже так просто не получится, а в случае аварий все пользователи, использующие его, завершают диалог. Но тем не менее это гарантирует, что соединение будет работать даже в условиях ограничения.
Таким образом TURN реализует fallback в случае проблем с STUN.
Далее нам нужно познакомится с процессом ICE:
ICE (Interactive Connectivity Establishment) отвечает за поиск всех возможных способов соединения двух одноранговых узлов. На самом деле его задача крайне проста, он должен собрать приватные и публичные адреса для клиентов. Эти адреса могут быть обнаружены с помощью STUN или TURN серверов, а после сбора этих адресов он должен выбрать оптимальный путь для установки соединения.
Что значит оптимальный путь?
Клиент собирает ICE candidates:
host = локальный адресsrflx = внешний адрес через STUNrelay = адрес через TURN
Например
host: 192.168.1.11:5000srflx: 93.184.10.20:62000relay: 203.0.113.5:55000
И после этого проверяет пары адресов между двумя клиентами, чтобы проверить сетевую доступность. Например
у Alice есть:
A1 = 192.168.1.11:5000A2 = 93.184.10.20:62000A3 = TURN:203.0.113.5:55000
У Bob:
B1 = 10.0.0.5:5000B2 = 88.77.66.55:41000B3 = TURN:203.0.113.5:56000
ICE пробует разные пары:
A1 <-> B1A1 <-> B2A2 <-> B2A3 <-> B3...
И проверяет проходят пакеты или нет. Он отправляет проверочные STUN пакеты от одного клиента к другому, и если ответы приходят, то путь рабочий. Далее ICE выбирает лучший маршрут, у него есть свой приоритет
1. Локальное соединение (если два клиента находятся в одной локальной сети и смогли достучаться друг до друга)
2. Прямое соединение через NAT
3. TURN релей.
Сам ICE не может напрямую отправить информацию о маршрутах клиентам и передает их через Signaling Server.
Signaling Server и SDP:
Но одного STUN мыло чтобы обеспечить соединение между двумя клиентами. Тут у нас в топологии появляется сигнальный сервер. Signal Server это обычный посредник между клиентами, он нужен чтобы два пира нашли друг друга. WebRTC не определяет каким именно должен быть сигнальный сервер, по этому можно использовать разные протоколы, например WebSocket или HTTPS. Конечно, часто используют вебсокеты, потому что важно уметь крайне быстро передавать события.
Теперь давайте рассмотрим, как выглядят эти события. Само событие представляет текстовое описание соединения, для его передачи используется сетевой протокол прикладного уровня SDP (Session Description Protocol). Он предназначен для того, чтобы два клиента могли договорится о том какие кодеки использовать, будет ли аудио, будет ли видео, будет ли шифрование и т.д. Щас рассмотрим подробнее.
Изначально SDP пакет содержит следующие данные:
v=0o=- 4611733057961207892 2 IN IP4 127.0.0.1s=-t=0 0m=audio 9 UDP/TLS/RTP/SAVPF 111 63 0 8c=IN IP4 0.0.0.0a=rtpmap:111 opus/48000/2a=sendrecva=ice-ufrag:abc123a=ice-pwd:def456a=fingerprint:sha-256 AA:BB:CC:DD...m=video 9 UDP/TLS/RTP/SAVPF 96 97a=rtpmap:96 VP8/90000a=rtpmap:97 H264/90000a=sendrecv
Описание всех полей можно прочитать в RFC 4566
Либо под катом
Скрытый текст
Session description
v= (protocol version)
o= (originator and session identifier)
s= (session name)
i=* (session information)
u=* (URI of description)
e=* (email address)
p=* (phone number)
c=* (connection information — not required if included in media)
b=* (zero or more bandwidth information lines)
One or more time descriptions («t=» and «r=» lines; see below)
z=* (time zone adjustments)
k=* (encryption key)
a=* (zero or more session attribute lines)
Zero or more media descriptions
Time description
t= (time the session is active)
r=* (zero or more repeat times)
Media description, if present
m= (media name and transport address)
i=* (media title)
c=* (connection information — optional if included at
session level)
b=* (zero or more bandwidth information lines)
k=* (encryption key)
a=* (zero or more media attribute lines)
Рассмотрим простую аналогию. Клиент 1 составляет OFFER запрос.
Здарова, я начинаю звонок.Я умею только ogg аудиоВидео могу VP8/H264.Вот мои адреса.Вот ключи для шифрования.Ответь, что из этого ты поддерживаешь.
После этого вторая сторона отвечает ANSWER ответом:
И тебе не хворатьЯ тоже умею opusВидео H264Вот мои адресИ вот мои параметры шифрования
Резюме:
Теперь давайте подобьем алгоритм установки соединения. У нас будет два клиента Алиса и Боб, как же меня уже тошнит от них.
1. Алиса начинает звонок, и сама того не подозревая подключается к Сигнальному серверу.
2. Боб тоже подключается к сигнальному серверу.
3. Анастасия и Боб отправляют запросы к STUN серверу.
4. Ангелина и Боб начинают собирать ICE Candidates:
— host
— srflx
— relay
5. Амина составляет SDP Offer и отправляет Бобу через Сигнальный сервер
6. Боб получает и применяет Offer. Дальше отправляет SDP Answer назад.
7. Боб так же собирает ICE Candidates. И отправляет Алине ICE Candidates через Сигнальный сервер
8. Александра применяет SDP Answer от Боба и ICE Candidates
9. ICE проверяет пары адрес
10. Пробует открывать прямые пути
11. Если прямой путь работает:
Алиса <==== UDP ====> Боб
12. Если прямой путь не работает:
Алиса <==== TURN ====> Боб
13. Дальше происходит выбор шифрования, обмен ключами, упаковка и передача медиа пакетов. И общались они долго и счастливо.
На этом Механизм WebRTC в двух словах объяснил.
И все бы было хорошо, если бы мы могли общаться только тет-а-тет. Но когда появляются третий, четвертый, десятый клиент эта схема трещит по швам. Проблема в том, что каждый участник должен соединится со всеми остальными.
Если в комнате N участников, количество P2P-связей:
N * (N — 1) / 2
Например:
|
Участников |
P2P-соединений |
|
2 |
1 |
|
4 |
6 |
|
10 |
45 |
|
50 |
1225 |
И это мы не берем в расчет видео, кодеки, реконнекты и все такое.
Для этого используется Сервер LiveKit.
LiveKit — это не замена WebRTC. LiveKit использует WebRTC, но меняет топологию. Вместо каждый с каждым он ставит между участниками SFU-сервер — Selective Forwarding Unit. LiveKit официально описывает свой сервер как WebRTC SFU; он принимает медиа-треки и пересылает их нужным подписчикам.

Теперь Каждый клиент устанавливает одно WebRTC-соединение с LiveKit, а не с каждым участником напрямую. Клиенты 1,2,3,4 тоже публикуют свои треки в LiveKit. Клиенты подписываются только на нужные треки: камера, микрофон, и т.д. В модели LiveKit участник публикует tracks, а другие участники подписываются на опубликованные tracks.
При этом важно понимать, что WebRTC в данной технологии никто не отменял, он остается основным транспортом медиа потоков, а LiveKit реализует серверную инфраструктуру поверх WebRTC и дает SDK для работы.
Тут я думаю стоит откланяться, мы пробежались по механизму работы WebRTC, даже немного сам LiveKit зацепили. Если вам понравилось и стало интересно, то в следующей статье мы расскажем как деплоим инфраструктуру для нашего приложения, или расскажешь что-нибудь интересное из нашего опыта. За сим прощаюсь
Не содержит ИИ
Зарисовки краха
https://www.figma.com/design/nBNfWwxkebyvflqQyKlqaW/livekit?node-id=0-1&t=MxZyIsVThnkvNfhM-1
ссылка на оригинал статьи https://habr.com/ru/articles/1044650/