Видимо в жизни каждого программиста наступает момент, когда ему становится необходимо научиться отправлять 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-файл:
<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>
Заготовка тестового приложения выглядела вполне стандартно:
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. С учетом использования log4j в остальных частях моего проекта, это оказалось неприятным сюрпризом.
Но надо, значит надо. Пришлось добавить зависимость в 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.
Отправка сообщения также выполнялась успешно, но на телефон, вместо букв, приходили «квадратики». На помощь пришел WireShark:
Наш разработчик 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 сложна сама по себе, а разработчики библиотеки сделали все возможное чтобы максимально облегчить процесс ее использования. Мелкие огрехи, в подобном проекте, неизбежны.
Я писал этот пост не с целью кого-нибудь поругать, а лишь из желания облегчить другим людям хождение по тем граблям, по которым прошел сам. Прошу не воспринимать мое брюзжание излишне серьезно.
Автор: GlukKazan