smpp34
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 receivedeliver_smand session events through your ownSmppClientListener. - Server — accept ESME binds and dispatch
submit_sm,data_sm,cancel_sm, … to yourSmppServerListener; push MT traffic back out withdeliver_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
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 Arc;
use ;
use async_trait;
use ;
;
async
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 Arc;
use ;
// `listener` implements SmppClientListener; its on_smsc_bound callback hands
// you the bound `SMSC` handle to submit through.
let listener = new;
let mut client = new;
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.