Наша компания занимается рассылками email и sms. На начальных этапах для рассылок смс мы использовали API посредника. Компания растет и клиентов становится все больше, мы приняли решение написать свой софт для отправки смс по протоколу smpp. Это нам позволило отправлять провайдеру набор байтов, а он уже распределял трафик по странам и внутренним операторам.
После ознакомления с доступными и бесплатными библиотеками для отправки смс выбор пал на jsmpp. Информацию по использованию, кроме этой, в основном разбирал из google по jsmpp. Описание самого протокола SMPP на русском языке тыц.
Надеюсь, что этой статьей я многим облегчу жизнь при написании SMPP сервиса.
Давайте начнем с поэтапного анализа отправки смс. Логика отправки смс выглядит следующим образом:
1. Клиент передает Вам смс которую хочет отправить (в виде json):
Параметр flash говорит, о том что это не flash смс.
2. Когда мы получаем данные, мы начинаем их готовить под тот способ, каким мы будете его отправлять провайдеру, а именно: UDH, SAR, Payload. Все зависит от того какой способ поддерживает Ваш провайдер.
3. Вы передаете данные провайдеру и он Вам отдает в ответ строку, которая идентифицирует прием смс провайдером, я назвал ее transaction_id.
4. Отправка смс оставляет за провайдером право на возвращения Вам окончательного статуса (доставлено, недоставлено и т.д.) на протяжении 48 часов. Поэтому Вам прийдется сохранять Ваш sms_id и полученный transaction_id.
5. Когда смс доставилась получателю, провайдер Вам передает transaction_id и статус (DELIVERED).
По статусам посоветую 2 статьи: здесь описывается и дается название только 10 статусам, а здесь уже полный перечень кодов статусов и их полное описание.
Maven зависимость для API jSmpp:
Опишу основные классы, с которыми мы будем работать:
Самый простой способ отправки это payload. Вне зависимости от того, какой длины смс Вы отправляете, Вы отправляете смс одним пакетом данных. Провайдер сам заботится о разбиении смс на части. Склейка частей уже происходит на телефоне получателя. С него и начнем обзор реализации отправки смс.
Для начала нам нужно подключиться к провайдеру. Для этого нам необходимо создать сессию и ее слушателя, а также слушателя, который реагирует на прием статусов отправленных смс. Ниже приведен пример метода createSmppSession подключения к провайдеру, данные которого хранятся в мною созданном классе SmppServer. Он содержит такие данные: логин, пароль, ip, порт и т.д.
Из этого примера видно, что мы используем объеты классов SmsReceiverListenerImpl и SessionStateListenerImpl для создания сессии. Первый отвечает за прием статусов отправленных смс, второй — слушатель сессии.
Класс SessionStateListenerImpl в методе onStateChange получает класс старого состояния сессии и нового. В данном примере, если сессия не подключена, происходит попытка переподключения.
Пример SmsReceiverListenerImpl. Вам придется переопределить 3 метода: onAcceptDeliverSm, onAcceptAlertNotification, onAcceptDataSm. Нам для отправки смс нужен только первый. Он будет получать от провайдера transaction_id, под которой зарегистрировал провайдер нашу смс и статус. В этом примере Вы встретите два класса: SmppErrorStatus и StatusType — это enum-классы, которые хранят статусы с ошибками и статусы отправки (отправлено провайдеру, не отправлено провайдеру и т.д.) соответственно.
Ну и наконец-то самый главный метод — метод отправки смс. Выше описанный JSON я дессериализировал в объект SMSMessage, поэтому, встречая объект этого класса, знайте, что он содержит всю нужную информацию про отправляемую смс.
Метод sendSmsMessage, описанный ниже, возвращает объект класса SingleSmppTransactionMessage, который содержит в себе данные об отправленной смс с transaction_id, который был присвоен провайдером.
Класс Gsm0338 помогает определить содержание кириллических символов в смс. Это важно, так как мы должны сообщать провайдеру об этом. Этот класс был построен на основе документа.
Enum класс SmppResponseError был построен на основе ошибок, которые может возвращать SMPP сервер провайдера, ссылка тут.
В следующей статье я опишу метод отправки смс использующий UDH, ссылка будет здесь. Этот вариант обязывает Вас переводит сообщение в байты, после чего делить их на подсообщения и в первых битах указывать их нумерацию и количество. Будет весело.
Github-ссылка. Надеюсь моя статья упростит Вам разработку SMPP сервиса. Спасибо за прочтение.
После ознакомления с доступными и бесплатными библиотеками для отправки смс выбор пал на jsmpp. Информацию по использованию, кроме этой, в основном разбирал из google по jsmpp. Описание самого протокола SMPP на русском языке тыц.
Надеюсь, что этой статьей я многим облегчу жизнь при написании SMPP сервиса.
Введение
Давайте начнем с поэтапного анализа отправки смс. Логика отправки смс выглядит следующим образом:
1. Клиент передает Вам смс которую хочет отправить (в виде json):
{
"sms_id": "test_sms_123",
"sender": "Ваше альфа имя",
"phone": "380959999900",
"text_body": "Привет! Ты выиграл 1000000$!",
"flash": 0
}
Параметр flash говорит, о том что это не flash смс.
2. Когда мы получаем данные, мы начинаем их готовить под тот способ, каким мы будете его отправлять провайдеру, а именно: UDH, SAR, Payload. Все зависит от того какой способ поддерживает Ваш провайдер.
3. Вы передаете данные провайдеру и он Вам отдает в ответ строку, которая идентифицирует прием смс провайдером, я назвал ее transaction_id.
4. Отправка смс оставляет за провайдером право на возвращения Вам окончательного статуса (доставлено, недоставлено и т.д.) на протяжении 48 часов. Поэтому Вам прийдется сохранять Ваш sms_id и полученный transaction_id.
5. Когда смс доставилась получателю, провайдер Вам передает transaction_id и статус (DELIVERED).
Статусы
По статусам посоветую 2 статьи: здесь описывается и дается название только 10 статусам, а здесь уже полный перечень кодов статусов и их полное описание.
Maven зависимость для API jSmpp:
<dependency>
<groupId>com.googlecode.jsmpp</groupId>
<artifactId>jsmpp</artifactId>
<version>2.1.0-RELEASE</version>
</dependency>
Опишу основные классы, с которыми мы будем работать:
- SMPPSession — сессия, которая создается для пользователя, которого Вы зарегистрировали у провайдера и со счета которого снимаются деньги;
- SessionStateListener — слушатель извещает Вас о том, что статус Вашей сессии был изменен, например сессия закрылась;
- MessageReceiverListener — слушатель возвращает объект DeliverSm, в котором хранится transaction_id, статус и мобильный номер;
Способ отправки Payload
Самый простой способ отправки это payload. Вне зависимости от того, какой длины смс Вы отправляете, Вы отправляете смс одним пакетом данных. Провайдер сам заботится о разбиении смс на части. Склейка частей уже происходит на телефоне получателя. С него и начнем обзор реализации отправки смс.
Для начала нам нужно подключиться к провайдеру. Для этого нам необходимо создать сессию и ее слушателя, а также слушателя, который реагирует на прием статусов отправленных смс. Ниже приведен пример метода createSmppSession подключения к провайдеру, данные которого хранятся в мною созданном классе SmppServer. Он содержит такие данные: логин, пароль, ip, порт и т.д.
Метод создающий SMPP подключение
protected SMPPSession session;
protected SmppServer server;
private ExecutorService receiveTask;
private SessionStateListener stateListener;
... // some of your code
public void createSmppSession() {
StopWatchHires watchHires = new StopWatchHires();
watchHires.start();
log.info("Start to create SMPPSession {}", sessionName);
try {
session = new SMPPSession();
session.connectAndBind(server.getHostname(),
server.getPort(),
new BindParameter(
BindType.BIND_TRX,
server.getSystemId(),
server.getPassword(),
server.getSystemType(),
TypeOfNumber.UNKNOWN,
NumberingPlanIndicator.UNKNOWN,
null));
stateListener = new SessionStateListenerImpl();
session.addSessionStateListener(stateListener);
session.setMessageReceiverListener(new SmsReceiverListenerImpl());
session.setTransactionTimer(TRANSACTION_TIMER);
if (Objects.isNull(receiveTask)
|| receiveTask.isShutdown()
|| receiveTask.isTerminated()) {
this.receiveTask = Executors.newCachedThreadPool();
}
watchHires.stop();
log.info("Open smpp session id {}, state {}, duration {} for {}",
session.getSessionId(), session.getSessionState().name(),
watchHires.toHiresString(), sessionName);
} catch (IOException e) {
watchHires.stop();
if (SmppServerConnectResponse.contains(e.getMessage())) {
log.error("Exception while SMPP session creating. Reason: {}. Duration {}, {}",
e.getMessage(), watchHires.toHiresString(), sessionName);
close();
return;
} else if (e instanceof UnknownHostException) {
log.error("Exception while SMPP session creating. Unknown hostname {}, duration {}, {}",
e.getMessage(), watchHires.toHiresString(), sessionName);
close();
return;
} else {
log.error("Failed to connect SMPP session for {}, duration {}, Because {}",
sessionName, watchHires.toHiresString(), e.getMessage());
}
}
if (!isConnected()) {
reconnect();
}
}
Из этого примера видно, что мы используем объеты классов SmsReceiverListenerImpl и SessionStateListenerImpl для создания сессии. Первый отвечает за прием статусов отправленных смс, второй — слушатель сессии.
Класс SessionStateListenerImpl в методе onStateChange получает класс старого состояния сессии и нового. В данном примере, если сессия не подключена, происходит попытка переподключения.
Слушатель SMPP сессии
/**
* This class will receive the notification from {@link SMPPSession} for the
* state changes. It will schedule to re-initialize session.
*/
class SessionStateListenerImpl implements SessionStateListener {
@Override
public void onStateChange(SessionState newState, SessionState oldState, Object source) {
if (!newState.isBound()) {
log.warn("SmppSession changed status from {} to {}. {}",
oldState, newState, sessionName);
reconnect();
}
}
}
Пример SmsReceiverListenerImpl. Вам придется переопределить 3 метода: onAcceptDeliverSm, onAcceptAlertNotification, onAcceptDataSm. Нам для отправки смс нужен только первый. Он будет получать от провайдера transaction_id, под которой зарегистрировал провайдер нашу смс и статус. В этом примере Вы встретите два класса: SmppErrorStatus и StatusType — это enum-классы, которые хранят статусы с ошибками и статусы отправки (отправлено провайдеру, не отправлено провайдеру и т.д.) соответственно.
Слушатель смс статусов
/*The logic on this listener should be accomplish in a short time,
because the deliver_sm_resp will be processed after the logic executed.*/
class SmsReceiverListenerImpl implements MessageReceiverListener {
@Override
public void onAcceptDeliverSm(DeliverSm deliverSm) throws ProcessRequestException {
if (Objects.isNull(deliverSm)) {
log.error("Smpp server return NULL delivery answer");
return;
}
try {
// this message is delivery receipt
DeliveryReceipt delReceipt = deliverSm.getShortMessageAsDeliveryReceipt();
//delReceipt.getId() must be equals transactionId from SMPPServer
String transactionId = delReceipt.getId();
StatusType statusType;
String subStatus;
if (MessageType.SMSC_DEL_RECEIPT.containedIn(deliverSm.getEsmClass())) {
// && delReceipt.getDelivered() == 1
statusType = getDeliveryStatusType(delReceipt.getFinalStatus());
SmppErrorStatus smppErrorStatus =
SmppErrorStatus.contains(delReceipt.getError());
if (smppErrorStatus != null)
subStatus = smppErrorStatus.name();
else
subStatus = delReceipt.getError();
} else {
statusType = StatusType.SMS_UNDELIVERED;
// this message is regular short message
log.error("Delivery SMS event has wrong receipt. Message: {}", deliverSm.getShortMessage());
subStatus = SmppErrorStatus.INVALID_FORMAT.name();
}
// some providers return phone number in deliverSm.getSourceAddr()
String phoneNumber = deliverSm.getDestAddress();
saveDeliveryStatus(transactionId, statusType, subStatus, phoneNumber));
log.info("Receiving delivery receipt from {} to {}, transaction id {}, status {}, subStatus {}",
deliverSm.getSourceAddr(), deliverSm.getDestAddress(),
transactionId, statusType, subStatus);
} catch (InvalidDeliveryReceiptException e) {
log.error("Exception while SMS is sending, destination address {}, {}",
deliverSm.getDestAddress(), e.getMessage(), e);
}
}
@Override
public void onAcceptAlertNotification(AlertNotification alertNotification) {
log.error("Error on sending SMS message: {}", alertNotification.toString());
}
@Override
public DataSmResult onAcceptDataSm(DataSm dataSm, Session source) throws ProcessRequestException {
log.debug("Event in SmsReceiverListenerImpl.onAcceptDataSm!");
return null;
}
private StatusType getDeliveryStatusType(DeliveryReceiptState state) {<cut />
if (state.equals(DeliveryReceiptState.DELIVRD))
return StatusType.SMS_DELIVERED;
else if (state.equals(DeliveryReceiptState.ACCEPTD))
return StatusType.ACCEPTED;
else if (state.equals(DeliveryReceiptState.DELETED))
return StatusType.DELETED;
else if (state.equals(DeliveryReceiptState.EXPIRED))
return StatusType.EXPIRED;
else if (state.equals(DeliveryReceiptState.REJECTD))
return StatusType.REJECTED;
else if (state.equals(DeliveryReceiptState.UNKNOWN))
return StatusType.UNKNOWN;
else
return StatusType.SMS_UNDELIVERED;
}
}
Ну и наконец-то самый главный метод — метод отправки смс. Выше описанный JSON я дессериализировал в объект SMSMessage, поэтому, встречая объект этого класса, знайте, что он содержит всю нужную информацию про отправляемую смс.
Метод sendSmsMessage, описанный ниже, возвращает объект класса SingleSmppTransactionMessage, который содержит в себе данные об отправленной смс с transaction_id, который был присвоен провайдером.
Класс Gsm0338 помогает определить содержание кириллических символов в смс. Это важно, так как мы должны сообщать провайдеру об этом. Этот класс был построен на основе документа.
Enum класс SmppResponseError был построен на основе ошибок, которые может возвращать SMPP сервер провайдера, ссылка тут.
Метод отправки смс
public SingleSmppTransactionMessage sendSmsMessage(final SMSMessage message) {
final String bodyText = message.getTextBody();
final int smsLength = bodyText.length();
OptionalParameter messagePayloadParameter;
String transportId = null;
String error = null;
boolean isUSC2 = false;
boolean isFlashSms = message.isFlash();
StopWatchHires watchHires = new StopWatchHires();
watchHires.start();
log.debug("Start to send sms id {} length {}",
message.getSmsId(), smsLength);
try {
byte[] encoded;
if ((encoded = Gsm0338.encodeInGsm0338(bodyText)) != null) {
messagePayloadParameter =
new OptionalParameter.OctetString(
OptionalParameter.Tag.MESSAGE_PAYLOAD.code(),
encoded);
log.debug("Found Latin symbols in sms id {} message", message.getSmsId());
} else {
isUSC2 = true;
messagePayloadParameter =
new OptionalParameter.OctetString(
OptionalParameter.Tag.MESSAGE_PAYLOAD.code(),
bodyText,
"UTF-16BE");
log.debug("Found Cyrillic symbols in sms id {} message", message.getSmsId());
}
GeneralDataCoding dataCoding = getDataCodingForServer(isUSC2, isFlashSms);
log.debug("Selected data_coding: {}, value: {}, SMPP server type: {}",
dataCoding.getAlphabet(),
dataCoding.toByte(),
server.getServerType());
transportId = session.submitShortMessage(
"CMT",
TypeOfNumber.ALPHANUMERIC,
NumberingPlanIndicator.UNKNOWN,
message.getSender(),
TypeOfNumber.INTERNATIONAL,
NumberingPlanIndicator.ISDN,
message.getPhone(),
ESM_CLASS,
ZERO_BYTE,
ONE_BYTE,
null,
null,
rd,
ZERO_BYTE,
dataCoding,
ZERO_BYTE,
EMPTY_ARRAY,
messagePayloadParameter);
} catch (PDUException e) {
error = e.getMessage();
// Invalid PDU parameter
log.error("SMS id:{}. Invalid PDU parameter {}",
message.getSmsId(), error);
log.debug("Session id {}, state {}. {}", session.getSessionId(), session.getSessionState().name(), e);
} catch (ResponseTimeoutException e) {
error = analyseExceptionMessage(e.getMessage());
// Response timeout
log.error("SMS id:{}. Response timeout: {}",
message.getSmsId(), e.getMessage());
log.debug("Session id {}, state {}. {}", session.getSessionId(), session.getSessionState().name(), e);
} catch (InvalidResponseException e) {
error = e.getMessage();
// Invalid response
log.error("SMS id:{}. Receive invalid response: {}",
message.getSmsId(), error);
log.debug("Session id {}, state {}. {}", session.getSessionId(), session.getSessionState().name(), e);
} catch (NegativeResponseException e) {
// get smpp error codes
error = String.valueOf(e.getCommandStatus());
// Receiving negative response (non-zero command_status)
log.error("SMS id:{}, {}. Receive negative response: {}",
message.getSmsId(), message.getPhone(), e.getMessage());
log.debug("Session id {}, state {}. {}", session.getSessionId(), session.getSessionState().name(), e);
} catch (IOException e) {
error = analyseExceptionMessage(e.getMessage());
log.error("SMS id:{}. IO error occur {}",
message.getSmsId(), e.getMessage());
log.debug("Session id {}, state {}. {}", session.getSessionId(), session.getSessionState().name(), e);
} catch (Exception e) {
error = e.getMessage();
log.error("SMS id:{}. Unexpected exception error occur {}",
message.getSmsId(), error);
log.debug("Session id {}, state {}. {}", session.getSessionId(), session.getSessionState().name(), e);
}
watchHires.stop();
log.info("Sms id:{} length {} sent with transaction id:{} from {} to {}, duration {}",
message.getSmsId(), smsLength,
transportId, message.getSender(),
message.getPhone(), watchHires.toHiresString());
return new SingleSmppTransactionMessage(message, server.getId(), error, transportId);
}
private GeneralDataCoding getDataCodingForServer (boolean isUCS2Coding, boolean isFlashSms){
GeneralDataCoding coding;
if (isFlashSms) {
coding = isUCS2Coding ? UCS2_CODING : DEFAULT_CODING;
} else {
coding = isUCS2Coding ? UCS2_CODING_WITHOUT_CLASS : DEFAULT_CODING_WITHOUT_CLASS;
}
return coding;
}
/**
* Analyze exception message for our problem with session
* While schedule reconnecting session sms didn't send and didn't put to resend
*/
private String analyseExceptionMessage(String exMessage){
if(Objects.isNull(exMessage))
return exMessage;
if (exMessage.contains("No response after waiting for"))
return SmppResponseError.RECONNECT_RSPCTIMEOUT.getErrCode();
else if (exMessage.contains("Cannot submitShortMessage while"))
return SmppResponseError.RECONNECT_CANNTSUBMIT.getErrCode();
else if (exMessage.contains("Failed sending submit_sm command"))
return SmppResponseError.RECONNECT_FAILEDSUBMIT.getErrCode();
return exMessage;
}
В следующей статье я опишу метод отправки смс использующий UDH, ссылка будет здесь. Этот вариант обязывает Вас переводит сообщение в байты, после чего делить их на подсообщения и в первых битах указывать их нумерацию и количество. Будет весело.
Github-ссылка. Надеюсь моя статья упростит Вам разработку SMPP сервиса. Спасибо за прочтение.
Поделиться с друзьями
Комментарии (3)
ihostage
14.12.2016 00:03А почему не воспользовались шлюзом? Jasmine или Kannel? На мой взгляд работать с REST API куда приятнее и проще, чем разбираться во всех тонкостях SMPP. Пусть и используя при этом уже готовую java-библиотеку.
Этот вариант обязывает Вас переводит сообщение в байты, после чего делить их на подсообщения и в первых битах указывать их нумерацию и количество. Будет весело.
Я вот ровно об избавлении от всего этого веселья. Так как шлюз на себя берет реализацию всех этих нюансов
- Разбиение на части длинных сообщений
- Борьба с кодировками
- Дозирование нагрузки на SMPP канал за счёт очереди
- Асинхронные callback'и при ответах от SMPP. Например отчёты о доставке SMS.
- Сохранение истории отправки SMS в каком-нибудь хранилище.
NateF
19.12.2016 09:20Каннел скок лет уже не обновлялся? Там косяков неочевидных в дефолтной версии предостаточно, сидеть потом разбираться в них, править)
afanasiy_nikitin
забавно :)
а вообще — почему не OpenSmpp или что-нибудь профессиональное, типа Twitter Cloudhopper?