Сотрудник открывает страницу личного кабинета и нажимает кнопку вызова прямо в интерфейсе. Через секунду раздаётся гудок, рядом с карточкой клиента появляется таймер разговора и поле для заметок, никакого отдельного приложения не нужно. Так выглядит телефония, встроенная в веб-интерфейс, и за этим сценарием стоит связка из двух технологий с очень разной историей.
SIP остаётся главным протоколом сигнализации для интернет-телефонии
Протокол установления сеанса, известный под сокращением SIP, появился в 1996 году как решение для мультимедийных сеансов в исследовательской сети. Стандартом он стал с документом RFC 2543, а в июне 2002 года был пересмотрен в RFC 3261, версию, которая считается основной.
SIP относится к прикладному уровню и работает поверх разных транспортов. Он может идти по дейтаграммам, по потоку с подтверждением доставки, а для защищённой передачи шифруется средствами транспортного уровня. Стандартные порты легко запомнить: 5060 для обычной сигнализации и 5061 для защищённой.
По устройству это текстовый протокол, вобравший элементы гипертекстового и почтового. Сообщения читаются глазами, запросы и ответы похожи на привычные методы и коды состояния, а адрес абонента записывается в форме, напоминающей электронную почту.
Текстовая природа объясняет живучесть протокола: отладка ведётся обычным чтением логов, расширения добавляются без ломки базовой логики. За двадцать с лишним лет SIP оброс десятками расширений и стал основой не только интернет-телефонии, но и мобильной голосовой связи в сетях четвёртого поколения.
WebRTC отвечает за передачу голоса и не содержит сигнализации
Вторая половина связки выросла из покупки, которую крупная интернет-компания совершила в мае 2010 года: вместе с разработчиком софта для видеосвязи достались кодеки и алгоритмы эхоподавления. Технологию открыли, передали в организации по стандартизации и в мае 2011 года представили публичный проект связи в реальном времени из браузера.
Проект получил название WebRTC. Спецификация шла к статусу рекомендации десятилетие: черновик консорциума всемирной паутины вышел в октябре 2011 года, статус кандидата присвоен в ноябре 2017 года, окончательная рекомендация появилась в январе 2021 года.
Для разработчика важны три программных интерфейса: первый даёт доступ к микрофону и камере, второй устанавливает соединение между участниками, беря на себя обработку звука, выбор кодека, защиту и управление полосой, третий открывает двусторонний канал произвольных данных поверх защищённого транспорта.
Ключевая особенность в том, чего в WebRTC нет: спецификация не описывает сигнализацию, механизм, которым участники находят друг друга и договариваются о параметрах соединения. Обнаружение путей через преобразователи адресов решается набором приёмов, а обмен управляющими сообщениями целиком ложится на приложение. Именно в это пустое место встраивается SIP.
SIP over WebSockets соединяет телефонную сигнализацию с веб-страницей
Чтобы SIP заработал в браузере, нужен транспорт, доступный из сценария. Обычный сокет веб-странице недоступен, зато протокол WebSocket умеет держать постоянное двустороннее соединение. Расширение SIP для работы через WebSocket описано в RFC 7118.
Схема получается такой. Браузер открывает защищённое соединение с сервером телефонии и обменивается по нему текстовыми сообщениями SIP. Медиапоток идёт отдельно, напрямую между участниками или через промежуточный узел. Сигнализация и голос живут в разных каналах, и путать их нельзя.
Типовая последовательность установления вызова выглядит так:
- браузер регистрируется на сервере, отправляя запрос с учётными данными;
- при исходящем вызове отправляется приглашение с описанием предлагаемых параметров медиа;
- сервер отвечает промежуточными и окончательными откликами о ходе вызова;
- после ответа абонента стороны подтверждают установление сеанса;
- по завершении разговора любая из сторон отправляет запрос на разрыв;
- регистрация периодически обновляется, чтобы сервер знал о доступности абонента.
Регистрация требует внимания. Срок её действия задаётся сервером, и клиент обязан повторять запрос заранее, иначе абонент станет недоступен для входящих вызовов при открытой странице. Обновление удобно выполнять примерно за треть срока до истечения.
Адрес абонента выглядит как электронная почта и разрешается через службу имён
Адрес записывается в форме пользователь и домен, разделённые символом @. Пример записи выглядит так:
sip:Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.
Такая строка предписывает клиенту найти сервер, обслуживающий домен, и передать ему запрос на соединение с абонентом. Поиск ведётся через службу доменных имён специальными записями, указывающими узел и порт сигнализации; без них клиент обратится на стандартный порт 5060 с дейтаграммным транспортом.
Защищённый вариант отличается схемой и портом по умолчанию и требует шифрования транспортного уровня на каждом участке пути. Сквозной защиты он не даёт: последний участок от прокси до абонентского устройства обеспечивается по местным правилам, и это стоит помнить при проектировании.
Подключение библиотеки в браузере требует защищённого контекста и аккуратной обработки событий
Практическая работа начинается с клиентской библиотеки, реализующей SIP поверх WebSocket: она берёт на себя разбор сообщений, управление транзакциями, поддержку диалогов и работу с медиа.
Скелет инициализации выглядит примерно так:
import { UserAgent, Registerer, Inviter, SessionState } from "sip-library";
const socket = new WebSocket("wss://sip.example.net/ws");
const userAgent = new UserAgent({
uri: UserAgent.makeURI("sip:Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript. "),
transportOptions: { sockets: [socket] },
authorizationUsername: "operator",
authorizationPassword: "секрет",
});
await userAgent.start();
const registerer = new Registerer(userAgent);
await registerer.register();
Регистратор стоит держать отдельным объектом. При потере сети его состояние нужно отслеживать и перезапускать регистрацию, иначе интерфейс будет показывать готовность к работе, а вызовы перестанут доходить.
Исходящий вызов оформляется созданием объекта приглашения и последующим запуском:
async function call(target) {
const uri = UserAgent.makeURI("sip:" + target + "@example.net");
const inviter = new Inviter(userAgent, uri, {
sessionDescriptionHandlerOptions: {
constraints: { audio: true, video: false },
},
});
inviter.stateChange.addListener((state) => {
if (state === SessionState.Established) console.log("Разговор начался");
if (state === SessionState.Terminated) console.log("Разговор завершён");
});
await inviter.invite();
return inviter;
}
Обработчик изменения состояния это необходимость: интерфейс должен различать состояния набора, дозвона, разговора и завершения, иначе пользователь начнёт нажимать кнопку повторно.
Входящий вызов приходит через событие у пользовательского агента. Ответ требует подготовки медиасредств, поэтому между уведомлением и откликом выполняется согласование параметров соединения. Ошибка на этом шаге даёт разговор без звука при успешном ответе.
Медиапоток и сигнализация идут разными путями и требуют разной диагностики
Понимание разделения каналов спасает часы отладки. Когда вызов не устанавливается, виновата сигнализация. Когда вызов устанавливается, но звука нет, виноват медиапуть.
В первом случае проверяют соединение с сервером, учётные данные, состояние регистрации и доступность порта сигнализации. Типичные симптомы: страница не переходит в готовность, входящие не приходят, исходящие обрываются сразу после набора.
Во втором случае смотрят на согласование кодеков и прохождение медиатрафика. Симптомы другие: таймер разговора идёт, собеседник не слышит или слышит обрывками. Причины обычно в сетевой топологии: преобразование адресов, ограничивающие правила на корпоративном шлюзе, несовпадение кодеков.
Полезный инструмент это сбор статистики соединения. Браузерный интерфейс позволяет получить данные о потерях пакетов, задержке, дрожании и использованных кодеках. Эти цифры превращают жалобу "плохо слышно" в конкретную задачу:
async function report(connection) {
const stats = await connection.getStats();
stats.forEach((item) => {
if (item.type === "inbound-rtp" && item.kind === "audio") {
console.log("Потеряно пакетов:", item.packetsLost, "Дрожание:", item.jitter);
}
});
}
Такой отчёт стоит писать в журнал вместе с идентификатором вызова: при разборе инцидента через сутки сохранённые цифры дадут ответ сразу.
Отдельная тонкость связана с аудиоустройствами: список гарнитур открывается только после разрешения на доступ к микрофону, поэтому порядок в интерфейсе должен быть сначала запрос доступа, потом выбор.
Безопасность встроенной телефонии строится вокруг учётных данных и защищённых схем
Интеграция телефонии в веб-интерфейс создаёт соблазн хранить учётные данные SIP в сценарии страницы. Так делать нельзя. Любой, кто откроет исходники, получит доступ к телефонии организации, а счёт за междугородние звонки придёт владельцу.
Разумная схема выдавать браузеру временные учётные данные: приложение запрашивает их у своего сервера, тот проверяет права пользователя и выдаёт короткоживущий пароль для регистрации, а постоянный секрет остаётся на серверной стороне.
Сигнализация идёт по защищённому варианту WebSocket, медиа шифруется встроенными средствами. Открытые схемы применимы только в локальной сети на этапе разработки и в рабочий контур не переезжают.
Имя и номер вызывающего передаются отправителем и не подтверждаются автоматически, поэтому подмена номера возможна. Полагаться на отображаемое имя при принятии решений нельзя, для подтверждения личности нужны отдельные механизмы.
Публичный узел сигнализации регулярно получает попытки регистрации с подбором учётных данных и запросы на соединение с внешними номерами. Наблюдение за журналами, ограничение попыток и запрет междугородних направлений по умолчанию давно стали обязательным минимумом.
Кодеки и качество связи определяют впечатление сильнее интерфейса
Пользователь судит о телефонии по звуку, а не по красоте кнопок. Качество определяется кодеками и состоянием сети.
Современный стандарт для голоса это кодек Opus, обеспечивающий хорошее качество при умеренной полосе и умеющий подстраиваться под условия. Для совместимости со старым оборудованием применяются более простые кодеки, работающие на низких скоростях. Обязательный минимум для совместимости включает пару узкополосных кодеков и передачу сигналов нажатия клавиш в виде отдельных сообщений, описанных в RFC 4733. Без последнего не будут работать голосовые меню, где требуется ввод добавочного номера.
Полоса под голос невелика: один разговор в узкополосном режиме занимает десятки килобит в секунду, в широкополосном заметно больше. Сложности начинаются не от объёма, а от стабильности. Задержка свыше трёхсот миллисекунд превращает диалог в перебивание, потери пакетов выше нескольких процентов делают речь прерывистой, а дрожание задержки ломает воспроизведение даже при отсутствии потерь.
Отсюда практический вывод для офиса. Проводное подключение предпочтительнее беспроводного, приоритет голосового трафика настраивается на сетевом оборудовании, а для удалённых сотрудников стоит предусмотреть промежуточный узел, если прямое соединение недоступно.
Выбор промежуточного узла это компромисс: прямое соединение даёт минимальную задержку, но не всегда устанавливается, релейный узел работает почти всегда, зато добавляет задержку и требует ресурсов. В библиотеках эти режимы различаются строковыми настройками политики, и менять их стоит осознанно, а не копируя пример из документации.
Интеграция телефонии в веб-приложения получилась историей про стык двух миров. Один вырос из исследовательских сетей и остался текстовым, расширяемым и предсказуемым. Второй вырос из покупки и стандартизации и взял на себя всю тяжёлую работу с медиа. Вместе они дают то, чего не было ни у одного по отдельности: разговор, встроенный в рабочее место, где рядом лежат данные этого разговора.