- PVSM.RU - https://www.pvsm.ru -
Прочитав заголовок, вы, наверное, немного удивились необычности задачи, которую я перед собой поставила. Однако, как ни странно, пейджеры до сих пор иногда могут пригодиться в жизни, даже несмотря на появившееся в последние 15 лет обилие других средств коммуникации. Один из частных случаев их применения — (около)медицинское учреждение, расположенное в железо-бетонном здании, глушащем WiFi и сигнал мобильного телефона. Обслуживающий персонал, тем не менее, должен каким-то образом получать сообщения о том, куда им надо срочно переместиться в случае чего. Для решения этой проблемы руководство учреждения в нашем случае поставило себе дорогую станцию и раздало всем сотрудникам пейджерА пейджеры, которые должны были среди прочих принимать наши сигналы. Соответственно, нашей (меня и моих коллег) задачей являлась их отправка.
Уже прошли те времена, когда для отправки текста на пейджер надо было сначала пообщаться с сонной девушкой с телефонного узла. Теперь достаточно дозвониться до станции и набрать номер абонента и сообщение в тоновом режиме. Арсенал при этом сильно ограничен: можно отправлять только цифры, символы * и #, иногда буквы ABCD. Но для передачи, скажем, номера комнаты или кода ошибки должно хватить. Это довольно сильно упрощает задачу и роднит её с другими — с дозвоном в общую переговорную комнату, например.
Несмотря на кажущуюся прозрачность решения и вторичность моего опыта, я решила подробно описать свои действия, потому что информации в интернете по теме представлено не очень много: на форумах на вопросы отвечают редко и неметко. Кому-то этот текст, возможно, сильно сэкономит время.
Первый этап — дозвон на пейджинговую станцию — был реализован через протокол SIP [1] и с помощью соответствующей Java-библиотеки jain-sip [2]. Самое лучшее описание принципов работы протокола я нашла на Хабре в публикациях «Взаимодействие клиентов SIP. Часть 1» [3] и «Взаимодействие клиентов SIP. Часть 2» [4], а самый удобоваримый туториал по джейн — вот здесь [5] (но коллекция примеров отсюда [6] отказалась получше).
В качестве предпоготовки я создала класс:
public class SipNotificator implements SipListener
c необходимыми полями, которые вначале должны быть инициализированы так, как указано в туториале:
private SipProvider sipProvider;
private SipFactory sipFactory;
private SdpFactory sdpFactory;//пригодится позже
private AddressFactory addressFactory;
private HeaderFactory headerFactory;
//private MessageFactory messageFactory; //не пригодилась
Как предписывают нам правила, сначала необходимо отправить INVITE-сообщение на телефон. Обратите внимание на то, что адресат в To- и Request-хедерах записывается по-разному. В первом случае заголовок просто собирается из глобального телефонного номера:
Address toNameAddress = addressFactory.createAddress( addressFactory.createTelURL(adresseenumber));
ToHeader toHeader = headerFactory.createToHeader(toNameAddress, null);
Во втором случае необходимо указать хост, с которого производится отправка сообщения, инициирующего общение:
URI requestURI = addressFactory.createAddress("sip:"+adresseenumber+"@"+host+";user=phone").getURI();
Другим интересным элементом является, собственно, тело SDP [7]-сообщения, представляющее собой описание того, что понадобится для успешной коммуникации. В нашем случае оно выглядело примерно так:
String sdpData = "v=0rn"
+ "o=4444 123456 789054 IN IP4 "+InetAddress.getLocalHost().getHostAddress() +"rn"
+ "s=phone callrn"
+ "p="+phoneusername+"rn"
+ "c=IN IP4 "+InetAddress.getLocalHost().getHostAddress() +"rn"
+ "t=0 0rn"
+ "m=audio "+localUdpPort+" RTP/AVP 0 8 18 101rn"
+ "a=rtpmap:0 PCMU/8000rn"
+ "a=rtpmap:8 PCMA/8000rn"
+ "a=rtpmap:18 G729A/8000rn"
+ "a=fmtp:18 annexb=norn"
+ "a=rtpmap:101 telephone-event/8000rn"
+ "a=fmtp:101 0-16rn"
+ "a=ptime:20rn"
+ "a=sendrecvrn";
Атрибуты «o» и «s» не являются особо важными, в «p» пишем свой телефон. Основная часть — «m» (media), в которой прописываются используемые кодеки (в нашем случае на отправителе они могут быть не установлены) и порт для принятия ответов по теме.
Если нам удалось отправить правильный инвайт, то в лучшем случае сервер-получатель пришлет нам желанное OK-сообщение со статусом 200, а в худшем — решит еще немного помучить идентификацией. Во втором случае ответый статус будет 401 или 407. Вот код, с помощью которого посылается ответ. Для его поддержки понадобится одна из последних версий jain-sip (например, 1.2.228). Его надо поместить в метод processResponse(), получающий в качестве аргумента ResponseEvent responseEvt.
if (status == 401|| status == 407){
AuthenticationHelper authenticationHelper = ((SipStackExt) sipProvider.getSipStack()).getAuthenticationHelper(
new AccountManagerImpl(this.phoneusername, this.password), headerFactory);
transaction = authenticationHelper.handleChallenge(responceEvt.getResponse(), responceEvt.getClientTransaction(), sipProvider, 15, true);
dialog = transaction.getDialog();
transaction.sendRequest();
}
Обратите внимание на четверый аргумент метода handleChallenge(), без него формат сообщения изменится, станет неподходящим и ваша аутентификация провалится.
Классы AccountManagerImpl и тоже неоходимый UserCredentialsImpl должны быть дописаны вами, я их писала по модели тех, что представлены здесь [8].
После отправки своих регистрационных данных мы можем смело ожидать искомый 200 OK, на который надо не забыть отправить ACK. Такой тип сообщения изготавливается крайне просто:
Request ackRequest = dialog.createAck( ((CSeqHeader) responseEvt.getResponse().getHeader(CSeqHeader.NAME)).getSeqNumber() );
//dialog - текущий диалог
Дальше начинается самое интересное — отправка DTMF-сигналов (те самые нажатия в тоновом режиме). Глобально это можно сделать через два разных протокола: через SIP [9] и через RTP [10]. Естественно, поначалу было решено пойти по пути наименьшего сопротивления. Для каждого символа формировался вот такой запрос, который потом нужно было отправить на сервер:
Request info = dialog.createRequest(Request.INFO);
String sdpData = "Signal="+digit+"rn" + "Duration=200";
byte[] contents = sdpData.getBytes();
ContentTypeHeader contentTypeHeader = headerFactory.createContentTypeHeader("application", "dtmf-relay");
info.setContent(contents, contentTypeHeader);
ClientTransaction transaction = sipProvider.getNewClientTransaction(info);
Dialog dialog = transaction.getDialog();
dialog.sendRequest(transaction);
Сама процедура отправки выглядит немного странно (кажется, построенной по модели «через Жмеринку в Париж»), но иначе у меня ничего не работало. Вообще библиотека мне показалась немного глючной: очень часто одно из нескольких решений, выглядевших по большому счету одинаково, не срабатывало.
Что я могу сказать? После реализации этого шага оказалось, что не все VoIP-сервера одинаково дружелюбны: некоторым достаточно было сигналов, передаваемых через SIP, а кому-то их не хватило, так как они не производят звукового сигнала и потому остаются незамеченными. Естественно, по закону подлости моей целью был сервер второго типа. Поэтому…
Вообще когда я осознала, что одним SIP-ом проблему не решить, я надеялась, что хотя бы смогу воспользоваться другой библиотекой, которая умеет ненапряжно отправлять DTMF-сигналы. Но не тут-то было. Обычно, если мы говорим «RTP через джаву», то подразумеваем JMF [11]. Но, во-первых, она уже старенькая и не особо поддерживается. Во-вторых, она больше подходит для передачи более сложных медиа. В-третьих, туториалы, которые мне удалось найти, были не очень толковыми. Вот [12] один из примеров из документации, в середине которого всплывает некая rtpSession, следов которой в первые сколько-то минут поиска мне найти вообще не удалось.
Другим вариантом была библиотека libjitsi [13], представляющая из себя целый коммуникатор. Из неё ничего позаимствовать тоже не удалось, хотя там есть милая метода sendDTMF или что-то в этом духе. Структура кода такова, что он берётся или целиком, или никак. В итоге было решено по-нормальному сделать человеческий пакет и отправить его через UDP-соккет.
Итак, вот значимый фрагмент класса RtpPacket: его основные поля и конструктор со значениями, подходящими для передачи DTMF. Что значат все эти вещи, написано много где, поэтому повторяться не буду. Отмечу только, что значение параметра ssrc в принципе роли не играет, но у всех отправляемых в одной сессии пакетов оно должно совпадать. Номер формата полезной нагрузки у DTMF-пакетов (payload type) — 101 (его мы прописали, когда инициировали SIP-коммуникацию).
private int version;
private boolean padding;
private boolean extension;
private int csrcCount;
private boolean marker;
private int payloadType;
private int sequenceNumber;
private long timestamp;
private long ssrc;
private long[] csrcList;
private byte[] data;
public RtpPacket(){
this.setVersion(2);
this.setPadding(false);
this.setExtension(false);
this.setCsrcCount(0);
long[] list = {};
this.setCsrcList(list);
}
Самый важный этап создания пакета — заполнение байтового массива данных. У DTMF, естественно свой формат: первый байт — это, собственно, значение передаваемого сигнала (от 0 до 16), первая половина второго байта — различные маркеты (обычно 0), вторая половина второго байта- громкость (стандартное значение — 10), остальные два — это длительность (стандартное значение — 160).
Для каждого сигнала создается около 10 пакетов (число может варьироваться):
— первый, начальный, имеет marker = 1, остальные — 0;
— последние три — конечные, marker = 0, зато первый бит второго байта блока данных = 1. Блок данных в неконечном пакете для передачи сигнала 1 будет выглядеть так:
0000 0001 0000 1010 0000 0000 1010 0000
значение громкость длительность
А в конечном вот так:
0000 0001 1000 1010 0000 0000 1010 0000
значение end громкость длительность
Метка времени у всех DTMF-пакетов, относящихся к одному сигналу, может оставаться одинаковой (предположим, T). Зато время следующего пакета должно быть:
T+(количество DTMF-пакетов * длительность пакета)
Можно было создать только один соккет для отправления и принятия сообщений. Но в любом случае никакой отправки, естественно, не случится, если не знать хоста и порта назначения. Это уже совсем не те данные, через которые проходила SIP-коммуникация. Свои координаты телефонный сервер присылает нам в ответных SIP сообщениях во время нашего дозвона. Их можно получить, вставив в метод processResponce() вот такой код (тут можно увидеть, зачем мы ранее инициализаровали sdpFactory):
Response resp = responceEvent.getResponse();
int remoteHost;
int remotePort;
if (resp.getRawContent()!=null){
String sdpContent =
new String(resp.getRawContent());
SessionDescription requestSDP = sdpFactory.createSessionDescription(sdpContent);
remoteHost = requestSDP.getConnection().getAddress();//хост запрятан в Connection Information
Vector<MediaDescription> media = requestSDP.getMediaDescriptions(false);
for (MediaDescription m:media){
if (m.getMedia()!=null )
remotePort =m.getMedia().getMediaPort();//порт можно найти среди данных media
}
}
Дальше, как я наивно полагала, мне оставалось только понаделать из моих байтов DatagramPacket'ов, засунуть их в сокет и запулить в сервер. Но не тут-то было. В ответ сервер продолжал обрывать коммуникацию на полуслове, как будто ничего и не получал. А Wireshark в принципе не принимал мои сообщения за RTP, отображая из как простые UDP.
На то, чтобы понять, в каком направлении двигаться дальше, ушло много времени. Я вложила много усилий в то, чтобы перечитать все имеющиеся в наличии спецификации и сто раз проверить свои пакеты на правильность. На седьмой же день Зоркий Глаз в моем лице заметил, что стандартная RTP-коммуникация не начинается сразу же с отправки DTMF-данных, а что ей предшествует непродолжительный обмен пакетами с сервером, которые выглядят несколько иначе.
Формат полезной нагрузки, объявленный в заголовке, равен 0, данных нет, зато есть собственно сама полезная нагрузка (payload), которая занимает 160 байт. Этот набор байтов различается во всех приходящих и уходящих сообщениях и выглядит составленным довольно случайно. Так или иначе, я не смогла найти информации о том, как именно он должен формироваться, поэтому каждый раз забивала его рандомами.
После того, как я стала отправлять эти вспомогательные пакеты перед каждым DTMF-сигналом, Wireshark наконец-то признал RTP-формат. Всё выглядело лучше, но коммуникация по-прежнему прерывалась, хотя сервер теперь от радости тоже стал меня забрасывать «пейлодными» пакетами.
Я уже и не знала, что еще бы могла сделать, но тут вспомнила, что RTP есть брат-неразлучник — RTCP [14]. Проблема, по всей видимости, действительно была в нем: сервер пытался мне что-то отправить, но ему от меня постоянно приходили сообщения о том, что соответствующий порт закрыт. Поскольку я не хотела заморачиваться отправкой еще и RTCP-пакетов, я начала просто с открытия чакры порта:
DatagramSocket socket = new DatagramSocket(localUdpPort, InetAddress.getLocalHost());
DatagramSocket controlSocket = new DatagramSocket(localUdpPort+1, InetAddress.getLocalHost());//порт должен быть на 1 больше, чем у RTP
Это оказало решающее воздействие: абонент получил моё сообщение «305*1*66» на пейджер!
В последних строках моей телеги хотелось бы подчеркнуть, что это мой первый пост на Хабре, так что не судите меня строго. Я совершенно не считаю себя гуру телематики или чего-либо ещё. Просто при написании исходного кода очень много времени ушло на поиск информации. Что-то я находила в спецификациях, которые от начала до конца в один присест осилить было сложновато, что-то было описано нормальным языком, но как-то неярко мелким шрифтом на полях, что-то я делала наугад. Так что в какой-то момент просто решила, что если у меня всё получится, я опишу все свои действия в одном месте и оставлю это индексироваться куда-нибудь в интернет.
Так что очень надеюсь, что хоть кому-то моя статья пригодится или хотя бы покажется интересной.
Автор: fainalex
Источник [15]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/82235
Ссылки в тексте:
[1] протокол SIP: http://en.wikipedia.org/wiki/Session_Initiation_Protocol
[2] jain-sip: https://jsip.java.net/
[3] «Взаимодействие клиентов SIP. Часть 1»: http://habrahabr.ru/post/188352/
[4] «Взаимодействие клиентов SIP. Часть 2»: http://habrahabr.ru/post/189332/
[5] здесь: http://www.oracle.com/au/products/database/introduction-jain-sip-090386.html
[6] отсюда : https://gitorious.org/jain-sip/mainline/source/af49d1bd2bcbd948114789e03c6e2f0f5c82fdfe:src/examples/
[7] SDP: http://en.wikipedia.org/wiki/Session_Description_Protocol
[8] здесь: https://gitorious.org/jain-sip/mainline/source/af49d1bd2bcbd948114789e03c6e2f0f5c82fdfe:src/examples/authorization
[9] через SIP: http://www.voip-info.org/wiki/view/SIP+DTMF+Signalling
[10] RTP : http://en.wikipedia.org/wiki/Real-time_Transport_Protocol
[11] JMF: http://www.oracle.com/technetwork/java/javase/tech/index-jsp-140239.html
[12] Вот: http://docs.oracle.com/cd/E17802_01/j2se/javase/technologies/desktop/media/jmf/2.1.1/apidocs/javax/media/rtp/RTPManager.html
[13] libjitsi: https://download.jitsi.org/libjitsi/
[14] RTCP: https://ru.wikipedia.org/wiki/RTCP
[15] Источник: http://habrahabr.ru/post/250185/
Нажмите здесь для печати.