SMS sending using jSMPP methods UDH, SAR, Payload

    Our company is engaged in email and sms. In the initial stages, we used the intermediary API to send SMS. The company is growing and there are more and more customers, we decided to write our own software for sending SMS via smpp protocol. This allowed us to send a set of bytes to the provider, and it already distributed traffic among countries and domestic operators.

    After reviewing the available and free libraries for sending SMS, the choice fell on jsmpp . Information on the use, in addition to this , was mainly sorted from google by jsmpp . Description of the SMPP protocol itself in Russian tyts .

    I hope that with this article I will make life easier when writing an SMPP service.

    Introduction


    Let's start with a phased analysis of SMS sending. The logic of sending SMS is as follows:

    1. The client sends you the 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 flash sms .

    2. When we receive the data, we begin to prepare it for the way 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 returns a line to you that identifies the SMS reception by the provider, I called it transaction_id .

    4. Sending SMS leaves the provider with the right to return to 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 transmits transaction_id and status (DELIVERED) to you.

    Statuses


    I will advise 2 articles on statuses: only 10 statuses are described and given a name, and here is a complete list of status codes and their full description.

    Maven dependency for jSmpp API:

    com.googlecode.jsmppjsmpp2.1.0-RELEASE

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

    • SMPPSession - a session that is created for the user whom you registered with the provider and from whose account money is withdrawn;
    • SessionStateListener - the listener notifies you that the status of your session has been changed, for example, the session has closed;
    • MessageReceiverListener - the listener returns a DeliverSm object , which stores transaction_id , status and mobile number ;

    Method of sending Payload


    The easiest way to send this is payload . Regardless of how long SMS you send, you send SMS with one data packet. The provider itself takes care of breaking SMS into parts. Parts are already glued together on the recipient's phone. From it we will begin the review of implementation of sending SMS.

    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 status of sent SMS. The following is an example of the createSmppSession method for connecting to a provider whose data is stored in the SmppServer class that I created . It contains the following data: login, password, ip, port, etc.

    Method creating 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();
            }
        }
    


    As you can see from this example, we use the SmsReceiverListenerImpl and SessionStateListenerImpl class objects to create a session. The first is responsible for receiving the status of sent SMS, the second is the session listener.

    Class SessionStateListenerImpl the method onStateChange gets the class of the old state and the new session. In this example, if the session is not connected, an attempt is made to reconnect.

    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 transaction_id from the provider , under which the provider has registered our SMS and status. In this example, you will find two classes: SmppErrorStatus and StatusType are enum classes that store error statuses and send statuses (sent to the provider, not sent to the provider, etc.), respectively.

    Listener sms status
    /*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) {
                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 is the method of sending SMS. I deserialized the above JSON into an SMSMessage object , therefore, when 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 SingleSmppTransactionMessage class , which contains data about the sent SMS with transaction_id , which was assigned by the provider.

    The Gsm0338 class 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 on the basis of the document .

    Enum class SmppResponseErrorwas built on the basis of errors that the provider's SMPP server may 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.

    Also popular now: