📜 ⬆️ ⬇️

SMS sending using jSMPP methods UDH, SAR, Payload

Our company is engaged in email and sms mailings. At the initial stages for SMS mailings, we used the intermediary API. The company is growing and there are more and more clients, we have decided to write our software to send SMS using the smpp protocol. This allowed us to send a set of bytes to the provider, and it has already distributed traffic across countries and internal operators.

After reading the available and free libraries to send SMS, the choice fell on jsmpp . Information on use, except this one , was mainly analyzed from google by jsmpp . Description of the protocol SMPP in Russian tyts .

I hope that with this article I will make life easier for many when writing the SMPP service.

Introduction


Let's start with a phased analysis of sending SMS. The logic of sending SMS is as follows:
')
1. The client sends you an SMS that he wants to send (in the form of json):

{ "sms_id": "test_sms_123", "sender": "  ", "phone": "380959999900", "text_body": "!   1000000$!", "flash": 0 } 

The flash parameter says that this is not a flash sms .

2. When we receive data, we begin to prepare them for the way in which we will send it to the provider, namely: UDH, SAR, Payload . It all depends on what method your provider supports.

3. You transfer the data to the provider and he gives you back a line that identifies the SMS receiving by the provider, I called it transaction_id .

4. Sending SMS leaves the provider the right to return you the final status (delivered, undelivered, etc.) within 48 hours. Therefore, you will have to save your sms_id and received transaction_id .

5. When the SMS is delivered to the recipient, the provider will send you transaction_id and status (DELIVERED).

Statuses


According to statuses I will advise 2 articles: only 10 statuses are described and given here , and here is the full list of status codes and their full description.

Maven dependency for jSmpp API:

  <dependency> <groupId>com.googlecode.jsmpp</groupId> <artifactId>jsmpp</artifactId> <version>2.1.0-RELEASE</version> </dependency> 

I will describe the main classes with which we will work:


Dispatch method Payload


The easiest way to send is payload . Regardless of what length of SMS you send, you send SMS with one data packet. The provider takes care of splitting the sms into parts. Gluing the parts is already happening on the recipient's phone. We will start the review of SMS sending implementation from it.

First we need to connect to the provider. To do this, we need to create a session and its listener, as well as a listener who responds to receiving the statuses of sent SMS. Below is an example of the createSmppSession method of connecting to an ISP whose data is stored in the SmppServer class I created. It contains the following data: username, password, ip, port, etc.

The method creates an SMPP connection
 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(); } } 


From this example, we can see that we use the obsms of the SmsReceiverListenerImpl and SessionStateListenerImpl classes to create a session. The first is responsible for receiving the statuses sent by SMS, the second - the session listener.

The SessionStateListenerImpl class in the onStateChange method gets the class of the old session state and the new one. In this example, if the session is not connected, a reconnection attempt is made.

SMPP session listener
  /** * 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 example. You will have to override 3 methods: onAcceptDeliverSm , onAcceptAlertNotification , onAcceptDataSm . We only need the first to send SMS. He will receive from the provider transaction_id , under which the provider registered our SMS and status. In this example, you will encounter two classes: SmppErrorStatus and StatusType are enum classes that store error status and send status (sent to the provider, not sent to the provider, etc.), respectively.

SMS status listener
 /*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; } } 


Well, finally, the most important method - the method of sending SMS. I desserialized the above described JSON into the SMSMessage object, therefore, meeting an object of this class, be aware that it contains all the necessary information about the SMS sent.

The sendSmsMessage method, described below, returns an object of the class SingleSmppTransactionMessage , which contains the data about the sent SMS with the transaction_id , which was assigned by the provider.

Class Gsm0338 helps to determine the content of Cyrillic characters in SMS. This is important, as we must inform the provider of this. This class was built from a document .

The Enum class SmppResponseError was built on the basis of errors that the provider’s SMPP server can return, link here .

SMS sending method
 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; } 


In the next article I will describe the method of sending SMS using UDH, the link will be here This option obliges you to translate the message into bytes, then divide them into sub-messages and indicate their numbering and number in the first bits. It will be fun.

Github link I hope my article will simplify the development of your SMPP service. Thanks for reading.

Source: https://habr.com/ru/post/310828/


All Articles