АД по имени JSMPP

  • Tutorial
Видимо в жизни каждого программиста наступает момент, когда ему становится необходимо научиться отправлять SMS-сообщения. Вчера такой момент наступил и у меня. Сразу скажу, что эта необходимость никак не связана с рекламными рассылками и прочим спамом. SMS-ки понадобилось рассылать в сугубо мирных целях, в рамках реакции на события, обнаруженные в процессе мониторинга оборудования.

Важность наличия возможности такой рассылки сложно переоценить. Действительно, отослав уведомление об аварии на EMail контактного лица, мы не можем рассчитывать на немедленное реагирование. Неизвестно, когда адресат прочитает свою почту. SMS доставляется намного оперативнее.

В нашей компании давно и успешно используется собственная реализация SMPP-сервиса и мысль об использовании готового SMPP-клиента на Java показалась мне логичной. Отважно вбив в строку поиска google слова «java smpp client», я немедленно нашел нужную мне библиотеку. О том, что происходило дальше, рассказывает мой сегодняшний пост.

Поскольку для сборки проектов я использую maven, первым делом, я скачал с сайта последнюю версию библиотеки, загрузил ее в локальный репозиторий следующей командой:

mvn install:install-file -Dfile=jsmpp-2.1.0.jar -DgroupId=org.jsmpp -DartifactId=smpp -Dversion=2.1.0 -Dpackaging=jar

после чего создал pom-файл:

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

	<modelVersion>4.0.0</modelVersion>
	<groupId>com.acme.ae.tests.smpp</groupId>
	<artifactId>SMPPTest</artifactId>
	<version>1.0.0</version>
	<packaging>jar</packaging>
	<name>smppTest-${project.version}</name>
	<url>http://maven.apache.org</url>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<resource_dir>${project.basedir}/src/main/resources</resource_dir>
	</properties>

	<build>
		<finalName>${project.name}-${project.version}</finalName>
		<resources>
			<resource>
				<directory>${resource_dir}</directory>
			</resource>
		</resources>
	</build>

	<dependencies>
		<dependency>
			<groupId>org.jsmpp</groupId>
			<artifactId>smpp</artifactId>
			<version>2.1.0</version>
		</dependency>
	</dependencies>
</project>


Заготовка тестового приложения выглядела вполне стандартно:

Test.java
package com.acme.ae.tests.smpp;

public class Test {

	private void start() throws IOException {
	}

	private void stop() throws IOException {
	}

	private void test()  {
	}

	public static void main(String[] args) {
		Test t = new Test();
		try {
			try {
				t.start();
				t.test();
			} finally {
				t.stop();
			}
		} catch (Exception e) {
			System.out.println(e.toString());
		}
	}
}


Далее последовало изучение примера, любезно предоставленного разработчиками. В качестве второго источника вдохновения использовалась переводная спецификация.

Для соединения с сервером, в полном соответствии с упомянутым выше примером, использовался следующий код:

Соединение с сервером
	...
	private SMPPSession session = null;

	private void start() throws IOException {
		session = new SMPPSession();
		session.connectAndBind(SMPP_IP, SMPP_PORT, 
				new BindParameter(
						BindType.BIND_TX, 
						SMPP_LOGIN, 
						SMPP_PASS, 
						"cp", 
						TypeOfNumber.UNKNOWN, 
						NumberingPlanIndicator.UNKNOWN, 
						null));
	}
	
	private void stop() throws IOException {
		if (session != null) {
			session.unbindAndClose();
		}
	}
	...


Назначение части параметров здесь вполне понятно из контекста. Остальные параметры были взяты из кода примера без изменений. Поскольку мы собираемся только передавать сообщения, используем BindType.BIND_TX.

Код передачи сообщения (вернее задания кодировки), взятый из примера, компилироваться отказался:

...
new GeneralDataCoding(Alphabet.ALPHA_DEFAULT, MessageClass.CLASS1, false)
...

После сравнения исходников скачанной библиотеки (последней доступной версии 2.1.0) с исходниками на GitHub, выяснилось, что разработчики, по непонятной мне причине, изменили сигнатуру конструктора:

-    public GeneralDataCoding(boolean compressed, boolean containMessageClass,
-            MessageClass messageClass, Alphabet alphabet) {
+    public GeneralDataCoding(Alphabet alphabet, MessageClass messageClass,
+            boolean compressed) throws IllegalArgumentException {
            ...
    }

Поскольку мной использовался старый вариант, в код пришлось внести коррективы (адреса отправителя и получателя в коде изменены):

Передача короткого сообщения
	...
	private static TimeFormatter timeFormatter = new AbsoluteTimeFormatter();	

	private void test() throws PDUException, ResponseTimeoutException, InvalidResponseException, NegativeResponseException, IOException {
		String messageId = session.submitShortMessage(
				"CMT", 
				TypeOfNumber.ALPHANUMERIC, 
				NumberingPlanIndicator.UNKNOWN, 
				"ACME", 
				TypeOfNumber.INTERNATIONAL, 
				NumberingPlanIndicator.ISDN, 
				"7XXXXXXXXXX", 
				new ESMClass(), 
				(byte)0, 
				(byte)1,  
				timeFormatter.format(new Date()), 
				null, 
				new RegisteredDelivery(SMSCDeliveryReceipt.DEFAULT), 
				(byte)0,
				new GeneralDataCoding(
						false,
						false,
						MessageClass.CLASS1, 
						Alphabet.ALPHA_DEFAULT),
				(byte)0, 
				"jSMPP simplify SMPP on Java platform".getBytes());
		
		System.out.println("Message submitted, message_id is " + messageId);
	}
	...


Запуск кода на выполнение не увенчался успехом. При попытке создания SMPPSession, выбрасывалось исключение:

Exception in thread "main" java.lang.NoClassDefFoundError: org/slf4j/LoggerFactory
	at org.jsmpp.session.AbstractSession.<clinit>(AbstractSession.java:51)
	at com.amfitel.m2000.ae.tests.smpp.Test.start(Test.java:56)
	at com.amfitel.m2000.ae.tests.smpp.Test.main(Test.java:179)
Caused by: java.lang.ClassNotFoundException: org.slf4j.LoggerFactory
	at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:307)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:248)
	... 3 more

Действительно, в GettingStarted нашлось скупое упоминание об использовании SLF4J. Пришлось добавить зависимость в pom.xml:

		...
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
			<version>1.6.1</version>
		</dependency>
		...

Номер версии (1.6.1, а не 1.4.3, как было сказано в GettingStarted) был взят из pom-файла на GitHub. Теперь в лог стала писаться неприятная ошибка, но, по крайней мере, соединение с сервером устанавливалось:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

Ремарка
Проблема решилась добавлением еще одной зависимости, также взятой из исходников на GitHub:

		...
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-log4j12</artifactId>
			<version>1.6.1</version>
		</dependency>
		...

После ее добавления, стал формироваться нормальный лог, в соответствии с настройками заданными в log4j.properties. Спасибо Googolplex за совет.

Отправка сообщения также выполнялась успешно, но на телефон, вместо букв, приходили «квадратики». На помощь пришел WireShark:

image

Наш разработчик SMPP-сервера, утверждал, что, для нормальной отправки сообщений на латинице, в Data Coding должен отсылаться 0. Надо сказать, что процесс формирования различных числовых кодов, диктуемых спецификацией, при использовании JSMPP API не очевиден. После непродолжительной медитации на исходный код, проблема была решена:

				...
				new GeneralDataCoding(
						false,
						false,
-						MessageClass.CLASS1, 
+						MessageClass.CLASS0, 
						Alphabet.ALPHA_DEFAULT)
				...

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

Для передачи кириллицы, требовалось сменить кодировку и перевести сообщение в UCS-2. С практической точки зрения, последнее выливалось в представление текста в виде последовательности байт в кодировке UTF-16 (про разницу между UCS-2 и UTF-16 можно прочитать здесь). В результате, код отправки сообщений выглядел следующим образом:

Передача длинного кириллического сообщения
GeneralDataCoding coding = new GeneralDataCoding(
	false,
	false,
	MessageClass.CLASS0, 
	Alphabet.ALPHA_UCS2);
		
final int totalSegments = 3;
Random random = new Random();
OptionalParameter sarMsgRefNum = OptionalParameters.newSarMsgRefNum((short)random.nextInt());
OptionalParameter sarTotalSegments = OptionalParameters.newSarTotalSegments(totalSegments);

for (int i = 0; i < totalSegments; i++) {
	final int seqNum = i + 1;
	String message = "Сообщение " + seqNum + " of " + totalSegments + " ";
	OptionalParameter sarSegmentSeqnum = OptionalParameters.newSarSegmentSeqnum(seqNum);
	String messageId = session.submitShortMessage(
		"CMT", 
		TypeOfNumber.ALPHANUMERIC, 
		NumberingPlanIndicator.UNKNOWN, 
		"ACME", 
		TypeOfNumber.INTERNATIONAL, 
		NumberingPlanIndicator.ISDN, 
		"7XXXXXXXXXX", 
		new ESMClass(), 
		SMPP_PROTOCOL_ID,   // (byte)0
		SMPP_PRIORITY_FLAG, // (byte)1
		null, 
		null, 
		new RegisteredDelivery(SMSCDeliveryReceipt.DEFAULT), 
		SMPP_REP_IF_P_FLAG, // (byte)0
		coding, 
		(byte)0, 
		message.getBytes(Charset.forName("UTF-16")),
		sarMsgRefNum, 
		sarSegmentSeqnum, 
		sarTotalSegments);

	System.out.println("Message submitted, message_id is " + messageId);
}


Завершая эту статью, хочу подчеркнуть, что она писалась не с целью критики JSMPP. Спецификация SMPP сложна сама по себе, а разработчики библиотеки сделали все возможное чтобы максимально облегчить процесс ее использования. Мелкие огрехи, в подобном проекте, неизбежны.

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

Подробнее
Реклама
Комментарии 16
  • +1
    У меня работает шлюз по отправке СМС и получению отчетов на библиотеке opensmpp.org/ (которая повторяет API LOGICA)
    Могу написать статью по работе с ней, думал она не будет полезна.
    А JSMPP я пробовал но он не понравился из-за какой-то глючности
    • 0
      Спасибо за ссылку, но с JSMPP вроде бы все проблемы порешал, так что, скорее всего, она останется в проекте. Но про Ваш опыт по части ее багов было бы узнать интересно (на всякий случай, вдруг что-то важное упустил)
    • +5
      Насчёт ошибки slf4j — подключите библиотеку slf4j-log4j и автоматом получите логи изнутри jsmpp. Для этого slf4j и нужен)
      • 0
        Да, завтра хочу поковырять в этом направлении. Надо добиться нормального журналирования.
        • 0
          Насчет этого — достаточно использовать библиотеку из Maven Central, а не деплоить самому в локальный репозиторий с неполным POM-файлом. Заодно и бонусы в виде подключенных исходников библиотеки в IDE получите.
      • 0
        А старый бобрый kannel уже не в моде?
        • 0
          Я написал в статье, что у нас имеется собственная реализация SMPP шлюза. Также есть требование относительно его использования. Поскольку и то и другое писал не я, вопрос относительно моды не ко мне.
          • 0
            Виноват, пока не очень силен в терминологии. Шлюза HTTP -> SMPP у нас нет. Есть SMPP сервер.
            Честно говоря, я не вижу весомых причин добавлять в эту связку еще и «старый добрый kannel», но возможно Вы меня разубедите?

            Что это даст?
            • 0
              Каннел подключается к SMPP, вам доступен простой и понятный HTTP интерфейс с кучей функций.
              HTTP клиент в любом языке — в разы проще, чем SMPP с кучей особенностей и кривизны из всех щелей.

              Иначе говоря — поставив kannel вы получаете http=>smpp гейт, решать проблемы с которым проще некуда.
              Использовали в боевом режиме (апп как раз на java был) с реальными операторами.
              • 0
                Еще раз повторюсь, я понимаю все это, но установка дополнительного софта (не важно с какой лицензией) — вопрос не моей компетенции. Я разрабатываю продукт в соответсвии с тех. заданием и планом работ. Со своей стороны я также пока не вижу причин включать в решение kannel. Задача уже решена без него
                • +1
                  Что ж. Это плохое решение, но принятое руководством — вам с ним жить, раз вы не можете на это повлиять.
                  Всем, кому потребуется решение в будущем общаться с SMPP — советую использовать готовые отлаженные и испытанные в продакшене инструменты.
                  • 0
                    С этим советом я согласен и присоединяюсь к нему
        • 0
          1. Если вы хотите получать ещё и отчёты о доставке, то BIND_TX будет недостаточно — потребуется BIND_TRX и асинхронное получение отчётов о доставке.
          2. Поддержу предыдущий коммент — лучше используйте kannel в качестве шлюза SMPP <=> HTTP и подключаться по HTTP. Протокол SMPP простой, но нюансов в нём много, проще взять готовый продукт и использовать его.
          • 0
            Отчеты о доставке получать пока не планирую. Я понимаю Ваши доводы относительно kannel, но вопрос его использования находится вне моей компетенции. Мне дали доступ только по SMPP, это часть требований ТЗ на разработку продукта
          • 0
            Библиотека jsmpp предоставляет просто низкоуровневое api для работы с smpp. Это минус, когда требуется «просто отправить смску», но зато низкоуровневое api является гибким инструментом если уметь им пользоваться.

            Насчёт data coding, в стандарте GSM 03.38 (SMS Data Coding Scheme) под него выделено 4 бита, остальные биты служат для других целей, и Alphabet.ALPHA_DEFAULT — это и есть как-бы «нулевой» data coding.

            Bits 3 and 2 indicate the alphabet being used, as follows:
            Bit 3 Bit2 Alphabet:
            0 0 Default alphabet
            0 1 8 bit
            1 0 UCS2 (16bit) [10]
            1 1 Reserved

            Но в SMPP спецификации действительно SMSC Default Alphabet в случае, когда все биты = 0.
            В библиотеке используется кодирование согласно стандарту GSM 03.38, правильно или нет трудно сказать.

            P.S. грабли которые могут ещё встретиться: в случае латиницы, default alphabet не определяет кодировку, на практике может встречаться как latin1 (ISO-8859-1), так и кодировка GSM 03.38, в которой по другому кодируются некоторые символы (буквенные символы кодируются по прежнему, проблема со знаками). Также, в случае default alphabet, smsc может ожидать «упакованную» латиницу (т.е. 160 символов кодируются в 140 байт).

            А ещё, с этой библиотекой нужно следить, если соединение порвалось, то переустанавливать соединение.

            И вероятно, до сих пор не исправлен баг, когда входящий deliver_sm может начать обрабатываться одновременно с bind_*_resp (race condition), библиотека отвергнет этот deliver_sm из-за неправильного состояния сессии (ошибка редкая, но я сталкивался).

            • 0
              Спасибо за такой столь развернутый комментарий в части кодировок. Про race condition уже тоже успел услышать. Reconnect сессии будет безусловно.

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