История одного проекта или как я 7 лет создавал АТС на базе Asterisk и Php

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

История одного проекта или как я 7 лет создавал АТС на базе Asterisk и Php

Идея и ключевые требования

А началось всё банально с любви к Asterisk (framework для построения коммуникационный приложений), автоматизации телефонии и установок FreePBX (веб интерфейс для Asterisk). Если потребности компании были без особенностей и укладывались в возможности FreePBX – всё супер. Вся установка проходила за сутки, компания получала настроенную АТС, удобный интерфейс и краткое обучение плюс сопровождение по желанию.

Но самые интересные задачи были нестандартными и тогда было не так сказочно. Asterisk может многое, но чтобы сохранить в рабочем виде веб-интерфейс, приходилось потратить в разы больше времени. Так небольшая мелочь могла занять времени гораздо больше, чем установка всей остальной АТС. И дело не в том, что писать веб интерфейс долго, а скорее дело в особенностях архитектуры FreePBX. Подходы и методы архитектуры FreePBX закладывалась во времена php4, а в тот момент уже был php5.6 на котором всё можно было сделать проще и удобнее.

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

Ключевыми требованиями стали:

  • простая настройка, интуитивно доступная даже начинающему администратору. Тем самым компаниям не требуется обслуживание АТС на нашей стороне,
  • легкая доработка, чтобы задачи решались за адекватное время,
  • удобство интеграции с АТС. У FreePBX не было API для изменения настроек, т.е. нельзя, например, создавать группы или голосовые меню из стороннего приложения, только API самого Asterisk,
  • opensource – для программистов это крайне важно для доработок под клиента.

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

Следующее, что замедляло время доработки и чего стоило избежать — это дублирование. Если есть модуль ответственный за дозвон до сотрудника, то все остальные модули, которым нужно отправить звонок сотруднику, должны использовать именно его, а не создавать свои собственные копии. Так, если нужно что-нибудь поменять, то менять придется только в одном месте и поиск «как это работает» проводить одного места, а не осуществлять поиск по всему проекту.

Первая версия и первые ошибки

Первый прототип был готов уже через год. Вся АТС, как и планировалось, была модульная, и модули могли не только добавлять новый функционал для обработки звонков, но и менять сам веб-интерфейс.

История одного проекта или как я 7 лет создавал АТС на базе Asterisk и Php
Да, идея построения диалплана в виде такой схемы не моя, но она весьма удобная и я сделал тоже самое для Asterisk.

История одного проекта или как я 7 лет создавал АТС на базе Asterisk и Php

С помощью написания модуля, программисты уже могли:

  • создавать для обработки звонка собственный функционал, который можно было разместить на схеме, а также в меню элементов слева,
  • создавать собственные страницы для веб-интерфейса и добавлять свои шаблоны на существующие страницы (если разработчик страницы это предусмотрел),
  • добавлять свои настройки на вкладку основных настроек или создавать собственную вкладку с настройками,
  • программист может пронаследоваться от существующего модуля, изменить часть функционала и зарегистрировать под новым именем или заменить оригинальный модуль.

Например, вот так можно создать своё голосовое меню:

......
class CPBX_MYIVR extends CPBX_IVR
{
 function __construct()
 {
 parent::__construct();
 $this->_module = "myivr";
 }
}
.....
$myIvrModule = new CPBX_MYIVR();
CPBXEngine::getInstance()->registerModule($myIvrModule,__DIR__); //Зарегистрировать новый модуль
CPBXEngine::getInstance()->registerModuleExtension($myIvrModule,'ivr',__DIR__); //Подменить существующий модуль

Первые сложные внедрения принесли первую гордость и первые разочарования. Радовало то, что оно работало, что я уже смог воспроизвести основные возможности FreePBX. Радовало, что людям идея схемы пришлась по душе. Было ещё много вариантов упростить разработку, но и на тот момент часть задач уже делалась проще.

Разочарованием стало API для изменения конфигурации АТС — получилось совсем не то, что хотелось. Я взял тот же принцип, что и во FreePBX, по нажатию кнопки Apply пересоздается вся конфигурация и перезапускаются модули.

Выглядит это так:

История одного проекта или как я 7 лет создавал АТС на базе Asterisk и Php
*Диалплан — правило(алгоритм), по которому обрабатывается звонок.

Но при таком варианте невозможно написать нормальное API для изменения настроек АТС. Во-первых, операция применения изменений к Asterisk слишком долгая и ресурсоемкая.
Во-вторых, нельзя вызвать две функции одновременно, т.к. обе будут создавать конфигурацию.
В-третьих, применяет все настройки в том числе сделанные администратором.

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

Вторая версия. Нос вытащил хвост увяз

Идеей для решения проблемы стало не пересоздавать конфигурацию и диалплан для Asterisk, а сохранять информацию в базу и читать из базы прямо во время обработки звонка. Asterisk уже умел читать конфигурации из базы, достаточно поменять значение в базе и следующий звонок уже будет обрабатываться с учетом изменений, а для чтения параметров диалплана отлично подошла функция REALTIME_HASH.

В итоге не понадобилось даже перезапускать Asterisk при изменении настроек и все настройки стали применяться сразу к Asterisk.

История одного проекта или как я 7 лет создавал АТС на базе Asterisk и Php

Единственные изменения диалплана – это добавления внутренних номеров и hints. Но это были маленькие точечные изменения

exten=>101,1,GoSub(‘sub-callusers’,s,1(1)); - точечное изменение, добавляется/изменяется через ami

; sub-callusers – универсальная функция генерится при установке модуля.
[sub-callusers]
exten =>s,1,Noop()
exten =>s,n,Set(LOCAL(TOUSERID)=${ARG1})
exten =>s,n,ClearHash(TOUSERPARAM)
exten =>s,n,Set(HASH(TOUSERPARAM)=${REALTIME_HASH(rl_users,id,${LOCAL(TOUSERID)})})
exten =>s,n,GotoIf($["${HASH(TOUSERPARAM,id)}"=""]?return)
...

Добавить или поменять строку в диалплане легко можно через Ami (интерфейс управления Asterisk) и перезагрузки всего диалплана не требуется.

Так была решена проблема с API для конфигурации. Можно было даже напрямую зайти в базу и добавить новую группу или поменять, например, время дозвона в поле “dialtime” у группы и следующий звонок уже будет длиться указанное время (Это не рекомендация к действию, т.к. для некоторых API операций требуются Ami вызовы).

Первые сложные внедрения опять принесли первую гордость и разочарование. Радовало то, что это работает. База данных стала критически важным звеном, выросла зависимость от диска, рисков больше, но всё работало стабильно и без проблем. А главное теперь всё, что можно было сделать через веб-интерфейс, можно было сделать и через API и при этом использовались одни и те же методы. Дополнительно, веб-интерфейс избавился от кнопки «применить настройки к АТС», о которой администраторы часто забывали.

Разочарованием стало усложнение разработки. Ещё с первой версии язык php генерит диалплан на языке Asterisk и выглядит это совершенно нечитаемо, плюс сам язык Asterisk для написания диалплана крайне примитивен.

Как это выглядело:

$usersInitSection = $dialplan->createExtSection('usersinit-sub','s');
$usersInitSection
 ->add('',new Dialplanext_gotoif('$["${G_USERINIT}"="1"]','exit'))
 ->add('',new Dialplanext_set('G_USERINIT','1'))
 ->add('',new Dialplanext_gosub('1','s','sub-AddOnAnswerSub','usersconnected-sub'))
 ->add('',new Dialplanext_gosub('1','s','sub-AddOnPredoDialSub','usersinitondial-sub'))
 ->add('',new Dialplanext_set('LOCAL(TECH)','${CUT(CHANNEL(name),/,1)}'))
 ->add('',new Dialplanext_gotoif('$["${LOCAL(TECH)}"="SIP"]','sipdev'))
 ->add('',new Dialplanext_gotoif('$["${LOCAL(TECH)}"="PJSIP"]','pjsipdev'))

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

Третья версия

Идеей для решения проблемы стало не генерить Asterisk диалплан из php, а использовать FastAGI и все правила обработки писать уже на самом php. FastAGI позволяет Asterisk, для обработки звонка, подключиться к сокету. Получать оттуда команды и отправлять результаты. Таким образом логика диалплана находится уже за границами Asterisk и может быть написана на любом языке, в моём случае на php.

Тут было много проб и ошибок. Главной проблемой являлось, что у меня уже было много классов/файлов. На создание объектов, инициализацию и взаимную регистрацию между ними уходило около 1,5 секунд, и эта задержка на каждый звонок не то, что можно игнорировать.

Инициализация должна была быть только 1 раз и поэтому поиски решения начались с написания сервиса на php с использованием Pthreads. Спустя неделю экспериментов этот вариант был отложен из-за тонкостей работы этого расширения. От асинхронного программирования на php после месяца тестов тоже пришлось отказаться, нужно было что-то простое, знакомое любому новичку php, да и многие расширения для php синхронные.

Решением стал свой многопоточный сервис на ‘си’, который компилировался с PHPLIB. Он подгружает все php файлы АТС, ждёт, когда все модули инициализируются, добавят коллбэк друг к другу и когда всё готово – кэширует. При запросе по FastAGI создаётся поток, в нём воспроизводится копия из кэша всех классов и данных и запрос передается в php функцию.

При таком решении время от отправки звонка в наш сервис до первой команды Asterisk сократилось с 1,5с до 0,05с и это время слабо зависит от размера проекта.

История одного проекта или как я 7 лет создавал АТС на базе Asterisk и Php

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

Для обработки диалплана в классе модуля нужно реализовать функцию dialplanDynamicCall и аргумент pbxCallRequest будет содержать объект для взаимодействия с Asterisk.

История одного проекта или как я 7 лет создавал АТС на базе Asterisk и Php

В дополнении появилась возможность отлаживать диалплан (в php есть xdebug и для нашего сервиса оно работает), можно двигаться по шагам просматривая значения переменных.

Данные по звонкам

Для любой аналитики и отчётов нужны правильно собранные данные и этот блок АТС тоже проходил много проб и ошибок с первой по третью версию. Зачастую данные по звонкам – это табличка. Один звонок = одна запись: кто звонил, кто ответил, сколько проговорили. В более интересных вариантах есть ещё дополнительная табличка, кого из сотрудников АТС вызывала во время звонка. Но всё это закрывает лишь часть потребностей.

Первоначальными требованиями стали:

  • сохранять не только кому звонила АТС, но и кто ответил, т.к. существуют перехваты и при анализе звонков это нужно будет учитывать,
  • время до соединения с сотрудником. Во FreePBX и некоторых других АТС, звонок считается отвеченным, как только АТС поднимет трубку. Но для голосового меню уже нужно поднять трубку, таким образом все звонки становятся отвеченными и время ожидания ответа становится 0-1 секунду. Поэтому решено было сохранять не только время до ответа, но время до соединения с ключевыми модулями (модуль сам устанавливает у себя это флаг. Сейчас это «Сотрудник», «Внешняя линия»),
  • для более сложного диалплана, когда звонок гуляет между разными группами, нужна была возможность каждый элемент исследовать по отдельности.

Лучшим вариантом оказался вариант, когда модули АТС сами о себе отправляют информацию по звонкам и в итоге сохранять информацию в виде дерева.

Выглядит это следующим образом:

Для начала общая информация о звонке(как у всех — ничего особенного).

История одного проекта или как я 7 лет создавал АТС на базе Asterisk и Php

  1. Поступил звонок по внешней линии «Для теста» в 05:55:52 с номера 89295671458 на номер 89999999999, в итоге на него ответил сотрудник «Секретарь2» с номером 104. Клиент прождал 60 секунд и разговаривал 36 секунд.
  2. Сотрудник «Секретарь2» делает звонок на номер 112 и на него отвечает сотрудник «Менеджер1» спустя 8 секунд. Разговаривают 14 секунд.
  3. Клиента переводят на Сотрудника «менеджер1» где они продолжают разговаривать ещё 13 секунд

Но это вершина айсберга, по каждой записи можно получить подробное прохождение звонка по АТС.

История одного проекта или как я 7 лет создавал АТС на базе Asterisk и Php

Вся информация представляется в виде вложенности вызовов:

  1. Поступил звонок по внешней линии «Для теста» в 05:55:52 с номера 89295671458 на номер 89999999999.
  2. В 05:55:53 внешняя линия отправляет звонок на Входящую схему «test»
  3. Во время обработки звонка по схеме вызывается модуль «вызов менеджера», в котором звонок находится 16 секунд. Это разработанный под клиента модуль.
  4. Модуль «вызов менеджера» отправляет звонок на ответственного за номер (клиента) сотрудника «Менеджер1» и ожидает ответа 5 секунд. Менеджер не ответил.
  5. Модуль «вызов менеджера» отправляет звонок на группу «Менеджеры КОРП». Это другие менеджеры такого же направления (сидят в одной комнате) и ожидает ответа 11 секунд.
  6. Группа «Менеджеры КОРП» вызывает сотрудников «Менеджер1, Менеджер2, Менеджер3» одновременно по 11 секунд. Ответа нет.
  7. Вызов менеджера завершается. И схема звонок отправляет на модуль «Выбор маршрута из 1с». Тоже написанный под клиента модуль. Тут звонок обрабатывался 0 секунд.
  8. Схема отправляет звонок на голосовое меню «Осн с донабором». Клиент в нём прождал 31 секунду, донабора не было.
  9. Схема отправляет звонок на Группу «Секретари», где клиент прождал 12 секунд.
  10. В группе вызывается одновременно 2 сотрудника «Секретарь1» и «Секретарь2» и спустя 12 секунд отвечает сотрудник «Секретарь2». Ответ на вызов дублируется в родительские вызовы. Получается и в группе ответил «Секретарь2», при вызове схемы ответил «Секретарь2» и на звонок по внешней линии ответил «Секретарь2».

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

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

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

Что в итоге?

Для обслуживания АТС не требуется специалист, с этим справляется самый обычный администратор – проверено на практике.

Для доработок не нужны специалисты с серьёзной квалификацией достаточно знаний php, т.к. уже написаны модули и для sip протокола, и для очереди, и для вызова сотрудника и другие. Есть класс обёртка для Asterisk. Программист для разработки модуля может (и по-хорошему должен) вызывать уже готовые модули. И знания Asterisk совершенно не нужны, если клиент просит добавить страницу с каким-нибудь новым отчётом. Но практика показывает, что сторонние программисты хоть и справляются, но без документации и нормального покрытия комментариями чувствуют себя неуверенно, поэтому ещё есть куда двигаться.

Модули могут:

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

Настройки АТС через API. Как описано выше, все настройки хранятся в базе и читаются в момент вызова, поэтому через API можно менять все настройки АТС. При вызове API не пересоздаётся конфигурация и не перезапускаются модули, следовательно, не важно насколько много у вас настроек и сотрудников. API запросы выполняются быстро и не блокируют друг друга.

АТС сохраняет все ключевые операции со звонками с длительностями (ожидания/разговора), вложенностями и в терминах АТС (сотрудник, группа, внешняя линия, а не канал, номер). Это позволяет строить различные отчёты под конкретных клиентов и большая часть работы – сделать удобный интерфейс.

Что будет дальше покажет время. Есть ещё много нюансов, которые стоит переделать, есть ещё много планов, но от создания 3-ей версии прошёл уже год и уже можно сказать, что идея работает. Основной минус 3-й версии – это аппаратные ресурсы, но за удобство разработки обычно всегда именно так и приходится платить.

Источник: habr.com