- PVSM.RU - https://www.pvsm.ru -

Назад в 90-е или как отправить сообщение на пейджер через Java

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

Уже прошли те времена, когда для отправки текста на пейджер надо было сначала пообщаться с сонной девушкой с телефонного узла. Теперь достаточно дозвониться до станции и набрать номер абонента и сообщение в тоновом режиме. Арсенал при этом сильно ограничен: можно отправлять только цифры, символы * и #, иногда буквы ABCD. Но для передачи, скажем, номера комнаты или кода ошибки должно хватить. Это довольно сильно упрощает задачу и роднит её с другими — с дозвоном в общую переговорную комнату, например.

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

image

Шаг 1 — INVITE

Первый этап — дозвон на пейджинговую станцию — был реализован через протокол 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), в которой прописываются используемые кодеки (в нашем случае на отправителе они могут быть не установлены) и порт для принятия ответов по теме.

Шаг 2 — аутентификация

Если нам удалось отправить правильный инвайт, то в лучшем случае сервер-получатель пришлет нам желанное 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 - текущий диалог
Шаг 3 — SIP INFO

Дальше начинается самое интересное — отправка 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, а кому-то их не хватило, так как они не производят звукового сигнала и потому остаются незамеченными. Естественно, по закону подлости моей целью был сервер второго типа. Поэтому…

Шаг 4. формирование RTP-пакета

Вообще когда я осознала, что одним 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-пакетов * длительность пакета)
Шаг 5. RTP-канал

Можно было создать только один соккет для отправления и принятия сообщений. Но в любом случае никакой отправки, естественно, не случится, если не знать хоста и порта назначения. Это уже совсем не те данные, через которые проходила 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.

Шаг 6. RTP-коммуникация

На то, чтобы понять, в каком направлении двигаться дальше, ушло много времени. Я вложила много усилий в то, чтобы перечитать все имеющиеся в наличии спецификации и сто раз проверить свои пакеты на правильность. На седьмой же день Зоркий Глаз в моем лице заметил, что стандартная 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/