Взаимодействие с Asterisk сервером из Java-приложения

Взаимодействие с сервером Asterisk из java-приложения через Asterisk Managment Interface (AMI)

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

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

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

1) Что выбрать, чтобы было удобно работать?

Определенно — Asterisk Managment Interface (AMI): данный интерфейс обладает полным набором функций, позволяющим совершать звонок, слушать события в реальном времени с сервера, получать статус звонка и прерывать его по необходимости.

2) Какую библиотеку подключать?

Вот эту:

<dependency>             <groupId>org.asteriskjava</groupId>             <artifactId>asterisk-java</artifactId>             <version>2.0.4</version> </dependency> 

3) Какие конфиги необходимо посмотреть на сервере?

extensions.conf — конфиг, который описывает диаплан. Вы к нему будете постоянно обращаться. Если более понятным языком, то там содержаться сценарии того, что будет делать сервер, при поступлении на него звонка на определенный номер. Сначала в диаплане ищется конкретный контекст — он записывается в квадратных скобочках, после этого под тегом этого контекста ищется номер, по которому вы обращаетесь.

manager.conf — конфиг с юзером и паролем к вашему серверу Asterisk

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

[user_name]     secret = password     read = all     write = all     deny=0.0.0.0/0.0.0.0     permit=0.0.0.0/255.255.255.0 

  • user_name — имя пользователя
  • secret — пароль для него
  • deny — ip адреса, доступ которым запрещен под данным пользователем
  • permit — доступ которым разрешен. Обязательно указывайте ip, с которого обращаетесь, в permit, так как астер может отбить ваш запрос.

sip.conf — тут прописаны все транки. Транк — это телефон, с которого будем звонить клиенту.

4) С чего начинать писать код?

Тут два варианта: вам либо нужно совершать какие-то действия на сервере Asterisk, либо слушать события на сервере. Наша последовательность включает и то и другое.

Опишем план действий:

  1. Открываем конекшен к серверу;
  2. Описываем сценарий работы;
  3. Слушаем события;
  4. Закрываем конекшен.

Соответственно, соединение инициализируется во время создания объекта DefaultAsteriskServer:

import org.asteriskjava.live.AsteriskServer; import org.asteriskjava.live.DefaultAsteriskServer; 

AsteriskServer asteriskServer = new DefaultAsteriskServer(HOSTNAME, USERNAME, PASSWORD); asteriskServer.initialize(); 

После того как открыли соединение, нам нужно позвонить пользователю. Назовем это сценарием действий. Описание сценария работы будет в отдельном классе:

/**  * Задается сценарий прозвона  */ public class ScenarioCall extends OriginateAction {      private final Logger log = LoggerFactory.getLogger(ScenarioCall.class);      private String TRUNK;     private final String PHONE_FOR_RINGING;     private final String EXTEN_FOR_APP;     private final String CONTEXT_FOR_APP;          public ScenarioCall(String trunk, String phoneForRinging, String extension, String context) {         this.TRUNK = trunk;         this.PHONE_FOR_RINGING = phoneForRinging;         this.EXTEN_FOR_APP = extension;         this.CONTEXT_FOR_APP = context;         this.init();     }      /**      * инициализируем сценарий и уже в конструкторе получаем готовый OriginateAction      */     private void init() {         //номер абонента         String callId = ValidValues.getValidCallId(this.PHONE_FOR_RINGING);         //канал с которого звоним         String channelAsterisk = ValidValues.getValidChannel(this.TRUNK, this.PHONE_FOR_RINGING);         this.setContext(CONTEXT_FOR_APP);         this.setExten(EXTEN_FOR_APP);         this.setPriority(1);         this.setAsync(true);         this.setCallerId(callId);         this.setChannel(channelAsterisk);         log.info("Create Scenario Call: phone '{}',chanel '{}',context '{}',extension '{}'",             callId,             channelAsterisk,             CONTEXT_FOR_APP,             EXTEN_FOR_APP);     }  } 

Что нам нужно понимать в этом сценарии? Сначала создается соединение с транком. Транк — это номер с которого вы будете звонить абоненту. После этого создается соединение между транком и абонентом, уже после этого соединение между абонентом и еще кем там Вам нужно.

Именно в такой последовательности.

this.setContext(CONTEXT_FOR_APP) 

Передаваемое значение: контекст, в котором будем искать телефонный номер, с которым вы хотите связать абонента (из extensions.conf).

this.setExten(EXTEN_FOR_APP) 

Передаваемое значение: сценарий, который выполнится после того, как вы связались с абонентом (из extensions.conf).

this.setCallerId(callId) 

Передаваемое значение: номер нашего абонента

this.setChannel(channelAsterisk) 

Передаваемое значение: устанавливаемый канал связи, обычно выглядит так: trunk_name/phone_user.

Где искать trunk_name? На сервере астериск есть конфиг sip.conf — там прописаны все транки.

Создадим звонок:

if (asteriskServer .getManagerConnection().getState().equals(ManagerConnectionState.CONNECTED)                     || asteriskServer .getManagerConnection().getState().equals(ManagerConnectionState.CONNECTING)                     || asteriskServer .getManagerConnection().getState().equals(ManagerConnectionState.INITIAL)) {                     try { 						ScenarioCall scenarioCall = new ScenarioCall(trank, phone, extension, context);                         CallBack callBackForScenarioCall = new CallBack();                         asteriskServer.originateAsync(scenarioCall, callBackForScenarioCall);                     } catch (ManagerCommunicationException e) {                         //при падении канала связи, StateConnection  может быть в RECONNECTING, а может вообще отвалиться                      }                 } 

Мы создали звонок, но как за ним следить динамически?

Для этого делается две вещи: в методе originateAsync передается экземпляр класса CallBack
и на сервер вешается слушатель, который будет сливать нам все происходящее.

Слушатель нужен, потому что класс CallBack не оповестит вас о окончании звонка, когда пользователь уже поговорил, а так же не оповестит вас о том, что пользователь мог еще куда бы то ни было перевестись.

/**  * После того как вы передали в метод asteriskConnection.originateAsync экземпляр  * класса CallBack - начнет исполняться сценарий на обзвон, переданный во втором параметре   * в originateAsync. CallBack будет служить своеобразным слушателем исполнения звонка,  * то если если пользователь не возьмет трубку, будет вызван метод  onNoAnswer , если  * линия будет занята то onBusy, если канал связи будет недоступен, то onFailure, и тд.  * Там вы пишете обработку событий призошедших со звонком. Важно, что данный класс не оповестит  * вас об успешном окончании звонка ( то есть когда пользователь взял трубку, поговорил и завершил звонок)  */ public class CallBack implements OriginateCallback {          /**      * Поставим первоначальный статус в PRERING, потом при исполнении переопределенных методов класса       * OriginateCallback - можете его менять      */     private ChannelState resultCall = ChannelState.PRERING;      /**      * когда мы звоним абоненту, устанавливается этот статус. Сценарий на обзвон еще не закончил свое выполнение      */     @Override     public void onDialing(AsteriskChannel asteriskChannel) {         // канал связи создан, переустанавливаете resultCall,          // важно что asteriskChannel будет скорее всего null,          // так что устанавливать resultCall придется хардкодом        // обработка события     }      /**      * Абонент взял трубку. Сценарий на обзвон закончил исполнение      * устанавливаем статус данного сценария в 6 - setStatus      */     @Override     public void onSuccess(AsteriskChannel asteriskChannel) {         // пользователь поднял трубку, asteriskChannel уже не null,         // asteriskChannel.getState() будет скорее всего в значении ChannelState.UP        // обработка события     }      /**      * Аббонент не ответил или сбросил звонок, не подняв трубку      * устанавливаем статус данного сценария в 7 - setStatus (рекомендуется)      */     @Override     public void onNoAnswer(AsteriskChannel asteriskChannel) {         // пользователь не ответил,          // важно что asteriskChannel будет скорее всего null,          // так что устанавливать resultCall придется хардкодом        // обработка события     }      /**      * Линия занята      * устанавливаем статус данного сценария в 7 - setStatus (рекомендуется)      */     @Override     public void onBusy(AsteriskChannel asteriskChannel) {         // телефонная линия занята,          // важно что asteriskChannel будет скорее всего null,         // так что устанавливать resultCall придется хардкодом        // обработка события     }      /**      * Произошла ошибка во время обзвона      */     @Override     public void onFailure(LiveException e) {         // обязательно проводите обработку данного события,          // потому что как показала практика,          // onFailure будет у вас очень часто      }  }

Как повесить слушателя на Астериск?

Для этого нужно создать класс имплементирующий AsteriskServerListener, PropertyChangeListener.
Для созданного соединения посредством экземпляра класса AsteriskConnection осуществляем:

 this.asteriskConnection.addAsteriskServerListener(this.callBackEventListener);

this.callBackEventListener — экземпляр класса нашего слушателя, рождается из:

**  * Слушатель для сервера Asterisk  * имплементация PropertyChangeListener  нужна для того, чтобы слушать события с сервера.  * имплементация AsteriskServerListener нужна для того, чтобы повесить слушателя на AsteriskConnection.  */ public class CallBackEventListener implements AsteriskServerListener, PropertyChangeListener {       public void onNewAsteriskChannel(AsteriskChannel channel) {         channel.addPropertyChangeListener(this);     }      public void onNewMeetMeUser(MeetMeUser user) {         user.addPropertyChangeListener(this);     }      public void onNewQueueEntry(AsteriskQueueEntry user) {         user.addPropertyChangeListener(this);     }      public void onNewAgent(AsteriskAgent asteriskAgent) {         asteriskAgent.addPropertyChangeListener(this);     }      /**      * Ловит событие окончания звонка. С помощью {@link PropertyChangeEvent}      * можно отслеживать любые события,      * но в данном контексте необходимы только события окончания,      * так как события начала звонка слушает класс CallBack      *      * @param propertyChangeEvent событие происходящее в течении звонка      */     public void propertyChange(PropertyChangeEvent propertyChangeEvent) {         findEventEndCall(propertyChangeEvent);     }      private void findEventEndCall(PropertyChangeEvent event) {         if (event.getSource() instanceof AsteriskChannel) {             AsteriskChannel callBackChannel = (AsteriskChannel) event.getSource();             String callId = getStringWithOnlyDigits(callBackChannel.getCallerId().toString());             callId = ValidValues.getValidCallId(callId);             if (callBackChannel.getState().toString().equals("HUNGUP")                 && event.getOldValue().toString().contains("RINGING")) {                 //пользователь не поднял трубку или сбросил                 callBackChannel.removePropertyChangeListener(this);                // пишете обработку окончания звонка              } else if (callBackChannel.getState().toString().equals("HUNGUP")                 && event.getOldValue().toString().contains("UP")) {                 //пользователь поднял трубку и поговорил                 callBackChannel.removePropertyChangeListener(this);                 // пишете обработку окончания звонка              } else if (callBackChannel.getState().toString().equals("HUNGUP")) {                 // завершение звонка по другой причине                 callBackChannel.removePropertyChangeListener(this);                 // пишете обработку окончания звонка             }         }     }      private String getStringWithOnlyDigits(String strForParse) {         String result = "";         if (strForParse != null && !strForParse.isEmpty()) {             CharMatcher ASCII_DIGITS = CharMatcher.anyOf("<>").precomputed();             result = ASCII_DIGITS.removeFrom(strForParse.replaceAll("[^0-9?!]", ""));         }         return result;     } } 

Советую в самом начале просто залогировать, то что приходит в propertyChange и посмотреть на PropertyChangeEvent, это будет адская портянка всего, что происходит на сервере. Он вообще никак не фильтрует информацию. Поэтому вывод: вешать слушателя надо как можно реже. Не на каждый звонок, потому что это можно сделать даже в классе OriginateCallback, насколько я находила. Это ни к чему. Посмотрите, какие вам приходят объекты PropertyChangeEvent, посмотрите какого типа там поля и какие из них вам нужны. Дальше — welcome в мир обработки информации.

Немного о валидации данных.

В OriginateAction.setChannel — передается trunk_name/phone_user
phone_user — если российский, то должен начинаться с восьмерки, если международный номер — с плюса.

В OriginateAction.setCallerId — передается номер телефона клиента,
потом в CallBackEventListener он придет в callBackChannel.getCallerId().

Будет брать его так:

String callId = getStringWithOnlyDigits(callBackChannel.getCallerId().toString()); 

В итоге не забываем про:

    asteriskServer.shutdown(); 

Если вам нужно прервать какой-либо звонок, то либо в классе CallBackEventListener
на существующий канал связи выполняем:

callBackChannel.hangup(); 

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

Успехов вам в работе с серверами Asterisk!

Дополнительная литература:

1) Asterisk-Java tutorial

2) Asterisk Managment Interface (AMI)

FavoriteLoadingДобавить в избранное
Posted in Без рубрики

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *