Взаимодействие с сервером 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, либо слушать события на сервере. Наша последовательность включает и то и другое.
Опишем план действий:
- Открываем конекшен к серверу;
- Описываем сценарий работы;
- Слушаем события;
- Закрываем конекшен.
Соответственно, соединение инициализируется во время создания объекта 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)
agic
я просто оставлю ссылку и скажу, что пользоваться AMI в таком режиме не всегда есть гуд.
helezpopova Автор
так расскажите о недостатках) мне интересно ) Из прямых недостатков, которые я заметила, могу обозначить только то, что соединение может периодические падать и приходится ждать рекконекта.
agic
не так давно на «asterconf», было несколько докладов на эту тему, в том числе и мой доклад — «ARI или почему ami зло». если кратко ami — это телнет подключение которые управляет примитивами астериска, и самый минус, вам приходится полностью парсить все события которые валятся туда, ну не вам а либе… И при нагруженных системах это проблема, и таких минусов не мало в ami (хотя по сути он не плох). ARI это все таки RestFull интерфейс, с возможностью подписки на определенные события.
pvsur
ami — прошлый век. ari гораздо проще и понятнее. можно вообще без Астерикса, с помощью pjsip :) во всех случаях Ява — самый ненужный компонент ;)
yaremchuk
С ari4java имел проблемы из-за того, что она отказалась работать с некотрыми минорными версиями asterisk 13. Ругаясь на наличие/отсутствие какого-либо поля в сообщениях от сервера.
Но так как ari хорошо документирован и с ним легко интегрироваться простыми http и websocket клиентами, мире оказалось проще и эффективней обойтись без ari4java.