Version

Mobius SMPP Implementation

To demonstrate how our Mobius SMPP Gateway works, we’ve prepared a test case that mirrors a simplified version of a typical messaging exchange. This test walks through the core steps involved in establishing a connection, sending a message, and verifying the delivery process. While simplified, it reflects the same principles that apply in real-world deployments.

Let’s say you’ve just acquired the Mobius SMPP Gateway. The next step is to deploy it on your infrastructure. Once it’s up and running, you’ll integrate it with your existing systems, like message routing platform or billing engine. From there, your applications (External Short Messaging Entities (ESMEs)) will be able to connect to the Mobius SMPP server and start sending or receiving SMS messages in real time.

From the moment the ESME binds to the server, the session management, message flow control, delivery receipts, error handling, and optional TLVs (Tag-Length-Value) come into play, exactly as defined by the SMPP protocol.

The goal is to simulate this interaction: a client connects, authenticates, sends a message, and awaits confirmation.

Step 1: Prepare synchronization tools

Before you can send and receive messages, you need to establish internal synchronization points. These tools help coordinate different phases of the session - for example, making sure the bind is completed before a message is sent, or pausing execution until a delivery receipt arrives.

    public void testSingleMessaging() throws ClassNotFoundException, GeneralSecurityException, IOException, SmppChannelException
    {
        ConcurrentHashMap<String, Semaphore> connectSemaphores = new ConcurrentHashMap<String, Semaphore>();
        ConcurrentHashMap<String, Semaphore> messagesSemaphores = new ConcurrentHashMap<String, Semaphore>();
// Used to wait for events like "bind complete", "message received", etc.

In this case, we use Semaphore objects inside concurrent hash maps, keyed by a connection ID. This allows us to manage multiple connections independently and ensure thread-safe handling of events. Each Semaphore will be used to pause and resume specific parts of the flow, acting as a gatekeeper until the expected response is received.

Step 2: Generate a connection

Now that your synchronization tools are ready, it’s time to simulate a connection - just like your application would do in a real SMPP setup.
In this step, we generate a unique connection ID using ObjectId().toHexString(). It helps us track and manage the specific connection. In a live system, this might represent an individual client or a specific channel to your SMSC.
Next, we define credentials - a username and password. These data client will use to authenticate.

        String connectionID1 = (new ObjectId()).toHexString();
        String username1 = "username1";
        String password1 = "01020304";

Finally, we bind the connection ID to our synchronization semaphores. This means that whenever something happens on this specific connection (like a successful bind or a message being received) the corresponding semaphore will be used to unblock the next action.

        connectSemaphores.put(connectionID1, new Semaphore(0));
        messagesSemaphores.put(connectionID1, new Semaphore(0));

This way, each connection gets its own coordination tools, which is needed when you're testing multiple clients or message flows in parallel.

Now that we've created our connection and registered semaphores for tracking events, it's time to hook everything together with a connection listener.

        LocalConnectionListener connectionListener = new LocalConnectionListener = new LocalConnectionListener(
        connectSemaphores,     // Used to release a semaphore when a connection is successfully established (bind response). This signals that it's safe to proceed with sending messages.
        connectSemaphores,     // Reused for disconnect tracking. For example, if the connection drops or is intentionally closed, the listener will release this semaphore as well.
        null,                  // We're skipping heartbeat tracking in this scenario, so no semaphore is needed here. In a real-world application, heartbeats help detect inactive or stalled connections.
        messagesSemaphores,    // Triggered when a message is received by the client (deliver_sm). This allows your code to continue once an incoming message has been handled.
        messagesSemaphores,    // Used when a delivery status report (delivery receipt) is received. These are important for confirming whether the message reached its destination or failed.
        messagesSemaphores,    // Covers responses to submitted messages, such as submit_sm_resp. It indicates that the server has acknowledged the message and returned a message ID.
        null                   // We're not tracking timeouts in this particular flow, so this parameter is left unused.
        );
        setConnectionListener(connectionListener);

This listener monitors SMPP events coming from the server and unblocks the flow at the right moments. Without it, the flow wouldn't know when a connection has been established or when a message has arrived. It's essentially the bridge between what's happening in the background and what your logic is waiting for.

Finally, we call setConnectionListener(...) to register the listener, allowing it to begin catching and responding to SMPP events as they occur.

So in short, the listener helps catch every key event and signals to your code when it's time to move forward, whether that's after binding, sending, receiving, or getting a delivery update.

Step 4: Start the client

When the connection ID, semaphores, and event listener are ready it’s time to start the SMPP client and begin the session.

        startClient(
        connectionID1,        // The unique ID we generated earlier.
        false,            // TLS is disabled in this test flow. In production, you’d likely enable it for secure communication.
        false,            // epoll is disabled. It’s an optimization for high-throughput environments, not needed here.
        true,             // Indicates that the client should initiate a bind request immediately after starting.
        1, 1,            // Specifies the number of threads and channels the client will use. One of each is enough for our simple scenario.
        username1, password1    // Credentials used to authenticate the session.
);

Once the client is started, it sends a bind request to the SMPP server and waits for a response. But rather than blocking the entire process, we rely on the semaphore we created earlier to signal when the bind has completed:

        try
        {
            connectSemaphores.get(connectionID1).tryAcquire(1, (long) (bindTimeout * 1.5), TimeUnit.MILLISECONDS);
        }
        catch (InterruptedException ex)
        {
        // Ignored for simplicity
        }

Here, we wait for the semaphore to be released. This will happen once the server responds to the bind request. We give it a bit more time than the bindTimeout, just to be safe.
Finally, we confirm that the bind was successful by checking how many clients are currently marked as bound:

        assertEquals(getUsedClients(connectionID1), new Integer(1));
        
If all goes well, this confirms that the connection is live, authenticated, and ready to send or receive messages.

Step 5: Build a SubmitSm message

With the client fully connected and the session established, it’s time to prepare a message for delivery. In SMPP, the operation for sending an SMS is called SubmitSm.

We will start by setting up a few essentials:

        Long expirationDate = System.currentTimeMillis() + 24 * 60 * 60 * 1000; // +24h
        expirationDate = expirationDate - expirationDate % 100; // normalize timestamp
    
        String messageID = new ObjectId().toHexString();
        byte[] data = ("hello world ").getBytes();

        
We define a message expiration date, set to 24 hours from now. This tells the SMSC how long it should keep trying to deliver the message before giving up.
We generate a unique message ID, again using ObjectId, which helps track the message throughout its lifecycle.
We prepare the message body - in this case, just the string "hello world" encoded as bytes.
Now we use a helper method to build the actual SubmitSm object:

        SubmitSm message = generateSubmitSm(
            connectionID1,
            messageID,
            data,
            null, // no UDH (User Data Header). We are not dealing with concatenated messages or special formatting here.
            Encoding.OCTET_UNSPECIFIED_2, // We are using basic binary encoding (Encoding.OCTET_UNSPECIFIED_2), which fits our "hello world" content.
            "010203", NumberPlan.E164, TypeOfNetwork.INTERNATIONAL, // FROM with number plan and network type
            "010205", NumberPlan.E164, TypeOfNetwork.INTERNATIONAL, // TO with number plan and network type
            ReceiptRequested.REQUESTED, // We request a delivery receipt to get notified when the message reaches its destination.
            IntermediateNotificationRequested.NOT_REQUESTED, // We skip intermediate acknowledgment for simplicity.
            SmeAckRequested.NOT_REQUESTED, // We skip SME acknowledgments for simplicity.
            expirationDate, // When to stop retrying delivery
            Priority.NORMAL, // The message is assigned normal priority.
            connectionListener // Of course, we pass in our connectionListener to track delivery status and acknowledgments.
        );

We predefine what we expect in terms of results:
        MessageStatus status = MessageStatus.OK;
        DeliveryStatus delivery = DeliveryStatus.DELIVERED;
        Integer errorCode = 0;

These are the values we’ll later check to verify that the message was accepted and successfully delivered. 

To simulate real-world behavior more closely, we also give the system a second to stabilize:
        try
        {
            Thread.sleep(1000);
        }
        catch (Exception ex)
        {  
           
        }

This short delay ensures that everything on the network and thread level is fully synchronized before we proceed with sending.

Step 6: Send the message

With the SubmitSm fully built and ready, it’s time to send it through the active SMPP session.

        sendMessage(connectionID1, messageID, message);

This call hands off the message to the SMPP layer, where it’s encoded, wrapped in a protocol data unit (PDU), and pushed through the socket to the server. Your connection listener is now standing by, ready to catch the server’s response and release the appropriate semaphore once the message has been processed.

To keep the flow controlled and predictable, we pause execution here and wait until the message has been successfully received on the other side:

        try
        {
            messagesSemaphores.get(connectionID1).tryAcquire(1, (long) (requestTimeout * 1.5), TimeUnit.MILLISECONDS);
        }
        catch (InterruptedException ex)
        {  
   
        }

This semaphore tracks message-level events. By using tryAcquire(), we ensure that the program only continues once an acknowledgment comes back, or the timeout is hit.

This mechanism is similar to what happens in production: your application sends a message, then waits (or listens asynchronously) for confirmation that the server received it and either accepted or rejected it.

Step 7: Server response

Once the message is sent, the server would normally pick it up, process it, and send back a response. It can be an acknowledgment or an error. Since we are running a self-contained scenario, we manually simulate that part.

        ConcurrentLinkedQueue<MessageRequestWrapper> requests = getRequests(connectionID1);
        MessageRequestWrapper currRequest = requests.poll();

Here we access the internal request queue for the given connection. This queue holds all messages that were sent and are waiting to be processed on the server side. We take the first message from the queue using poll(), simulating the server receiving the message. The poll() method removes the message from the queue and returns it.

Before moving forward, we validate two things and we simulate the server generating a successful response:

        assertNotNull(currRequest);                // we make sure the request was actually received (it’s not null).
        String remoteMessageID = new ObjectId().toHexString();    // we compare the original message payload ("hello world") with the content that arrived. This ensures that the message wasn’t corrupted during transmission.
        assertArrayEquals(data, currRequest.getData());
        RequestProcessingResult result = new RequestProcessingResult(Arrays.asList(new String[] { remoteMessageID }), status);

A new remote message ID is generated. This is what a real SMSC would send back to help track the message later.
We wrap the message ID and MessageStatus.OK into a RequestProcessingResult, representing a successful operation.
Then we trigger the response manually, as if the server were pushing it back to the client:

        currRequest.getResponse().onResult(result, null);
This callback simulates the server replying to the submit_sm request with a positive response, effectively acknowledging receipt and providing a message ID.

Finally, just like before, we wait for the client to process this result:

        try
        {
            messagesSemaphores.get(connectionID1).tryAcquire(1, (long) (requestTimeout * 1.5), TimeUnit.MILLISECONDS);
        }
        catch (InterruptedException ex)
        {  
  
        }

This ensures the flow doesn't move forward until the response has been fully handled. It is similar to how a production system would wait for delivery confirmation or response parsing before continuing.

Step 8: Validate the response

At this stage, the server has sent a response, and the client has processed it. Now it’s time to validate that everything happened as expected.

        ConcurrentLinkedQueue<ResponseWrapper> responses = getResponses(connectionID1);
        ResponseWrapper currResponseWrapper = responses.poll();

Just like we did with requests, we access the queue holding the received responses for this connection. We then extract the first response using poll(), simulating the final step where your application picks up and processes the delivery result.

Next comes the validation:

        assertNotNull(currResponseWrapper);

First, we check that a response was actually received. It should never be null at this point.

        assertEquals(currResponseWrapper.getOriginalMessageID(), messageID);

This confirms that the response matches the message we originally sent. It ensures traceability from request to acknowledgment.

        assertEquals(currResponseWrapper.getRemoteMessageID(), remoteMessageID);

This checks that the server returned the correct message ID — the one it generated when processing our request.

        assertEquals(currResponseWrapper.getStatus(), status);

Finally, we validate that the status returned (MessageStatus.OK) is what we expected. In a real-world case, this status could indicate success, failure, or a variety of delivery states.
At this point, the entire flow from connection, through message submission, to response handling has been successfully completed and verified.

Step 9: Send delivery report (DLR)

Once the message has been processed and acknowledged, the next step is to send a Delivery Report (DLR). This informs the client whether the message was successfully delivered to the recipient’s device or if it failed somewhere along the way.
In this step, we simulate the server sending a DLR back to the client.
First, we generate the DeliverSm object - this is the SMPP PDU used to carry the delivery status.

        DeliverSm dlr = generateDeliverSm(
        connectionID1,
        remoteMessageID,
        "Message Delivered".getBytes(),     // DLR message content
        null,                               // no UDH
        errorCode,                          // error code, 0 for success
        delivery,                           // delivery status, e.g., DELIVERED
        Encoding.OCTET_UNSPECIFIED_2,       // encoding type
        message.getSourceAddress().getAddress(),                          // FROM
        NumberPlan.fromInt(message.getSourceAddress().getNpi()),
        TypeOfNetwork.fromInt(message.getSourceAddress().getTon()),
        message.getDestAddress().getAddress(),                            // TO
        NumberPlan.fromInt(message.getDestAddress().getNpi()),
        TypeOfNetwork.fromInt(message.getDestAddress().getTon()),
        connectionListener
        );

Next, we send this delivery report back to the client:

                sendDelivery(connectionID1, remoteMessageID, dlr);

This simulates the server pushing the DLR to notify the client about the final status of their message.
As before, we pause the flow briefly to wait for the client to process the delivery report:        

        try
        {
            messagesSemaphores.get(connectionID1).tryAcquire(1, (long) (requestTimeout * 1.5), TimeUnit.MILLISECONDS);
        }
        catch (InterruptedException ex)
        {  
  
        }

This ensures everything runs in sync - the client won’t continue until it’s received and acknowledged the delivery report.

Step 10: Validate delivery report

Delivery report has been sent and we need to validate that it was correctly received and interpreted on the client side. This step ensures that all details in the delivery report match what was expected.
First, we retrieve the latest request from the queue:

        requests = getRequests(connectionID1);
        currRequest = requests.poll();

This gives us the request that represents the delivery report just sent. Now we extract and inspect the report data. We validate it directly instead of storing the report data in a separate variable:

        assertNotNull(currRequest.getReportData());                                // The report data exists (not null).
        assertEquals(currRequest.getReportData().getMessageID(), remoteMessageID); // The message ID matches the one we sent and tracked earlier.
        assertEquals(currRequest.getReportData().getErrorCode(), errorCode);       // The error code is correct (0, indicating no issues).
        assertTrue(Arrays.equals(currRequest.getReportData().getData(), "Message Delivered".getBytes()));    // The content of the report is exactly "Message Delivered".
        assertEquals(currRequest.getReportData().getEncoding(), Encoding.OCTET_UNSPECIFIED_2); // The encoding of the message is accurate.
        assertEquals(currRequest.getReportData().getDeliveryStatus(), delivery);                // The delivery status confirms that the message was successfully delivered.

After validating the details, we need to let the system know we’ve processed the delivery report. We simulate this by creating a result and sending a response from the client:

        result = new RequestProcessingResult(Arrays.asList(new String[] { remoteMessageID }), status);
        
        currRequest.getResponse().onResult(result, null);

The response handler is triggered to let the system know the delivery report was successfully processed.

Next, we give the system time to process everything and synchronize, just like we did before:

        try
        {
            messagesSemaphores.get(connectionID1).tryAcquire(1, (long) (requestTimeout * 1.5), TimeUnit.MILLISECONDS);
        }
        catch (InterruptedException ex)
        {
        }

This ensures that the flow doesn’t proceed until all parts of the delivery report cycle are finished.

Step 11: Final response check

At this point, we've processed the delivery report and acknowledged it. Now, we perform one last check to make sure the final response from the system is as expected.
We start by retrieving the latest response from the queue:

        responses = getResponses(connectionID1);
        currResponseWrapper = responses.poll();

This gives us the response generated after handling the delivery report.
Now, we validate the key elements of this final response:

        assertNotNull(currResponseWrapper); // Make sure that a response exists (not null)
        assertEquals(currResponseWrapper.getOriginalMessageID(), remoteMessageID); // We check that the original message ID in this response matches the remote message ID from the delivery report. To be sure that we're still tracking the same message throughout.
        assertNull(currResponseWrapper.getRemoteMessageID()); // There’s no new message ID to return, it should be null. (because it is the final acknowledgment)
        assertEquals(currResponseWrapper.getStatus(), status); // The status is as expected (MessageStatus.OK), indicating that everything completed successfully.

Step 12: Cleanup

After successfully running through the entire SMPP message lifecycle, it’s important to clean up and release any resources that were used during the process. This mirrors what you would typically do in a real application - making sure everything shuts down gracefully.

        resetConnectionListener();
        stopClient(connectionID1);
    }

resetConnectionListener():
This disables the listener we set up earlier, ensuring that no late event handlers remain active. It’s a good practice to reset or clear listeners after the job is done, especially if you plan to reuse the same environment for other tasks.

stopClient(connectionID1):
This gracefully shuts down the client session we started. The connection is closed, resources like threads and sockets are released, and everything tied to this specific session is properly finalized.

Start innovating with Mobius

What's next? Let's talk!

Mobius Software

As a company you'll get:

  • Get started quickly

  • Support any business model

  • Join millions of businesses

Questions? websupport@mobius.com