Наша компания занимается рассылками 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 сервиса. Спасибо за прочтение.