smpp34 1.1.1

Pure-Rust SMPP 3.4 codec with an async (tokio) client and server
Documentation

smpp34

crates.io docs.rs CI license

A pure-Rust implementation of the SMPP 3.4 protocol (Short Message Peer-to-Peer): a PDU codec plus an asynchronous (tokio) client and server built on a listener-trait model.

  • Client — bind to an SMSC as an ESME (transmitter, receiver or transceiver), send submit_sm / data_sm, and receive deliver_sm and session events through your own SmppClientListener.
  • Server — accept ESME binds and dispatch submit_sm, data_sm, cancel_sm, … to your SmppServerListener; push MT traffic back out with deliver_sm.
  • Codec — encode/decode every SMPP 3.4 PDU, including optional TLV parameters.

Sessions handle the SMPP timers (session_init, enquire_link, inactivity, response), sequence-number windowing, and optional TLS.

Supported PDUs

bind_transmitter · bind_receiver · bind_transceiver (+ resps) · outbind · unbind · enquire_link · submit_sm · submit_sm_multi · deliver_sm · data_sm · query_sm · cancel_sm · replace_sm · alert_notification · generic_nack, plus arbitrary TLV (optional parameter) encode/decode.

Install

cargo add smpp34

Quick start — server

Implement SmppServerListener and hand it to an SmppServer. Every trait method has a default, so you override only the PDUs you care about — typically on_bind_transceiver and on_submit_sm.

use std::sync::Arc;
use std::net::{IpAddr, Ipv4Addr};
use async_trait::async_trait;
use smpp34::{
    SmppServer, SmppServerListener, SmppConnectionInformation, SmppError,
    bind_transceiver, bind_transceiver_resp, submit_sm, submit_sm_resp,
};

struct MyEsmeHandler;

#[async_trait]
impl SmppServerListener for MyEsmeHandler {
    async fn on_bind_transceiver(
        &self, req: bind_transceiver, _c: &SmppConnectionInformation, _s: &String,
    ) -> bind_transceiver_resp {
        if req.system_id == "esme1" && req.password == "secret" {
            req.accept("MySMSC".to_string(), Some(0x34))   // 0x34 = SMPP v3.4
        } else {
            req.reject(SmppError::ESME_RINVPASWD)
        }
    }

    async fn on_submit_sm(
        &self, req: submit_sm, _c: &SmppConnectionInformation, _s: &String,
    ) -> submit_sm_resp {
        // ... route the message ...
        req.accept("message-id-1".to_string())
    }

    // Every other method has a sensible default (binds reject, on_data_sm /
    // on_cancel_sm reject, on_unbind acks, notifications no-op), so you override
    // only what you need.
}

#[tokio::main]
async fn main() {
    let listener = Arc::new(MyEsmeHandler);
    let mut server = SmppServer::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 2775, listener);
    server.start().await;          // spawns the accept loop
    tokio::signal::ctrl_c().await.unwrap();
    server.stop().await;
}

Quick start — client

Bind to an SMSC, then submit once the bind completes (the on_smsc_bound callback hands you an SMSC you can submit through).

use std::sync::Arc;
use smpp34::client::{SmppClient, BIND_TYPE};

// `listener` implements SmppClientListener; its on_smsc_bound callback hands
// you the bound `SMSC` handle to submit through.
let listener = Arc::new(MyClientHandler::new());
let mut client = SmppClient::new(
    "smsc.example.net".to_string(), 2775, /* tls = */ false,
    BIND_TYPE::TRX,
    "esme1".to_string(), "secret".to_string(), "GATEWAY".to_string(),
    /* addr_ton */ 0, /* addr_npi */ 0, /* address_range */ String::new(),
    listener.clone(),
    /* window_size */ 20,
);
client.start().await;
// inside on_smsc_bound(smsc, _): use the fluent builder (only set what you need)
//   smsc.submit_sm()
//       .source_addr("12345")
//       .destination_addr("31600000000")
//       .short_message(b"hello")
//       .send().await?;

TLS

Pass tls = true to SmppClient::new to bind over TLS. The TLS transport is backed by tokio-native-tls (system OpenSSL).

Timers & windowing

SmppServer::new / SmppClient::new use sensible default timers. Use the *_with_default_timers constructors to set session_init, enquire_link, inactivity and response timers (milliseconds) and the buffer / window size explicitly.

Performance

Measured on a 24-core AMD Ryzen AI 9 HX 370 (single box — the SMSC and ESME are co-located, so the load generator competes for cores; separate hosts scale further). Reproduce with cargo bench and perf/docker-compose.yml.

Codec — single core, criterion (cargo bench):

Operation Throughput
submit_sm decode ~6.6 M PDU/s
submit_sm encode ~12.3 M PDU/s
deliver_sm decode / encode ~6.8 M / ~12.6 M PDU/s
TLV decode / encode ~20 M / ~14 M /s

End-to-end — real TCP, full submit_sm round-trip (send → submit_sm_resp):

Load Throughput Peak RSS
1 ESME, window 64 ~180k submit_sm/s 6 MB
4 ESMEs, window 64 ~635k submit_sm/s 7 MB

The SMSC server sustains 625k+ submit_sm/s even pinned to half the cores; past a few sessions the single-box benchmark is bound by the co-located load generator, not by smpp34.

Profiling

A CPU flamegraph of the loopback under load (scripts/flamegraph.sh) puts ~99.98% of samples in the kernel (the socket/syscall path) — the user-space codec barely appears. smpp34 is syscall-bound, not CPU-bound: at these rates the cost is the ~600k+ read/write syscalls per second plus the runtime's wakeups, not parsing (the codec runs at millions of PDUs/sec). The remaining headroom is therefore I/O batching — coalescing writes and cutting wakeups — not faster code.

Memory

scripts/mem_leak_test.sh installs a counting global allocator and asserts live bytes stay flat (exact — unlike noisy RSS) across both codec round-trips and bind/unbind churn. RSS holds at single-digit MB while pushing millions of PDUs. (The bind/unbind check found and now guards a real session-teardown leak — see the changelog.)

SMPP 3.4 compliance

The PDU / TLV / session support matrix is in docs/COMPLIANCE.md; docs/COMPARISON.md maps smpp34 against other SMPP stacks.

MSRV

Rust 1.80.

Stability

The SMPP 3.4 codec and the client/server session handling are used in production, and the crate follows Semantic Versioning: the public Rust API is the contract. The 17-argument send_submit_sm / send_deliver_sm remain available, but the submit_sm() / deliver_sm() builders are the recommended way to send.

License

MIT © Real Time Telecom B.V.

Maintained by Real Time Telecom B.V. — carrier-grade telecom infrastructure in Rust.