SMPPEX is a framework for building SMPP servers and clients (which are often referred to as MC and ESME entities respectevely).
The major features exposed by the library are:
SMPPEX.ESME
module and behaviour for implementing ESME entities;SMPPEX.MC
module and behaviour for implementing MC entities;SMPPEX.ESME.Sync
module representing simple ready to use SMPP client.
Also one of the core features of the library is simplicity: both code simplicity and simplicity of use.
- The library does not have much TCP handling or session management functionality,
it is based on great
ranch
library. - SMPP session is symmetric(used both in ESME and MC) and is implemented as
ranch_protocol
behaviour. - The library includes an easy and ready to use SMPP client (
SMPP.ESME.Sync
) which has capabilities of synchronous SMS sending and do not require implementing ESME behavior. There is also an SMPP testing toolsmppsend
based on this client.
SMPPEX.ESME.Sync
is the most straightforward way to interact with an SMSC. Example:
{:ok, esme} = SMPPEX.ESME.Sync.start_link(host, port)
bind = SMPPEX.Pdu.Factory.bind_transmitter("system_id", "password")
{:ok, _bind_resp} = SMPPEX.ESME.Sync.request(esme, bind)
# We are bound, let's send a message
submit_sm = SMPPEX.Pdu.Factory.submit_sm({"from", 1, 1}, {"to", 1, 1}, "hello!")
{:ok, submit_sm_resp} = SMPPEX.ESME.Sync.request(esme, submit_sm)
# Message is sent, let's get the obtained id:
message_id = SMPPEX.Pdu.field(submit_sm_resp, :message_id)
# Now let's wait for a delivery report:
delivery_report? = fn(pdu) ->
SMPPEX.Pdu.command_name(pdu) == :deliver_sm and
SMPPEX.Pdu.field(pdu, :receipted_message_id) == message_id
end
delivery_reports = case SMPPEX.ESME.Sync.wait_for_pdus(esme, 60000) do
:stop ->
Logger.info("Ooops, ESME stopped")
[]
:timeout ->
Logger.info("No DLR in 60 seconds")
[]
received_items ->
# Let's filter out DLRs for the previously submitted message
for {:pdu, pdu} <- received_items, delivery_report?.(pdu), do: pdu
end
SMPPEX.ESME
can be used when more complicated client logic is needed, for example
custom immediate reactions to all incoming PDUs, rps/window control, etc.
SMPPEX.Session
provides "empty" defaults for all required callbacks, so minimal ESME
could be very simple:
defmodule DummyESME do
use SMPPEX.Session
def start_link(host, port) do
SMPPEX.ESME.start_link(host, port, {__MODULE__, []})
end
end
It is still completely functional:
{:ok, esme} = DummyESME.start_link(host, port)
SMPPEX.Session.send_pdu(esme, SMPPEX.Pdu.Factory.bind_transmitter("system_id", "password"))
Here's a more complicated example of ESME, which does the following:
-
Receives port number and three arguments:
waiting_pid
— a pid of the process which will be informed when ESME stops;count
— count of PDUs to send;window
— window size, the maximum number of sent PDU's without resps.
-
Connects to the specified port on localhost and issues a bind command.
-
Starts to send predefined PDUs after bind at maximum possible rate but regarding window size.
-
Stops after all PDUs are sent and notifies the waiting process.
defmodule SMPPBenchmarks.ESME do
use SMPPEX.Session
require Logger
@from {"from", 1, 1}
@to {"to", 1, 1}
@message "hello"
@system_id "system_id"
@password "password"
def start_link(port, waiting_pid, count, window) do
SMPPEX.ESME.start_link("127.0.0.1", port, {__MODULE__, [waiting_pid, count, window]})
end
def init(_, _, [waiting_pid, count, window]) do
Kernel.send(self(), :bind)
{:ok, %{waiting_pid: waiting_pid, count_to_send: count, count_waiting_resp: 0, window: window}}
end
def handle_resp(pdu, _original_pdu, st) do
case SMPPEX.Pdu.command_name(pdu) do
:submit_sm_resp ->
new_st = %{ st | count_waiting_resp: st.count_waiting_resp - 1 }
send_pdus(new_st)
:bind_transmitter_resp ->
send_pdus(st)
_ ->
{:ok, st}
end
end
def handle_resp_timeout(pdu, st) do
Logger.error("PDU timeout: #{inspect pdu}, terminating")
{:stop, :resp_timeout, st}
end
def terminate(reason, _, st) do
Logger.info("ESME stopped with reason #{inspect reason}")
Kernel.send(st.waiting_pid, {self(), :done})
:stop
end
def handle_info(:bind, st) do
{:noreply, [SMPPEX.Pdu.Factory.bind_transmitter(@system_id, @password)], st}
end
defp send_pdus(st) do
cond do
st.count_to_send > 0 ->
count_to_send = min(st.window - st.count_waiting_resp, st.count_to_send)
new_st = %{ st | count_waiting_resp: st.window, count_to_send: st.count_to_send - count_to_send }
{:ok, make_pdus(count_to_send), new_st}
st.count_waiting_resp > 0 ->
{:ok, st}
true ->
Logger.info("All PDUs sent, all resps received, terminating")
{:stop, :normal, st}
end
end
defp make_pdus(0), do: []
defp make_pdus(n) do
for _ <- 1..n, do: SMPPEX.Pdu.Factory.submit_sm(@from, @to, @message)
end
end
Not all callbacks are used yet in this example, for the full list see SMPPEX.Session
documentation.
SMPPEX.MC
is used for receiving and handling SMPP connections.
Here is an example of a very simple MC, which does the following:
- Starts and listens to connections on the specified port.
- Responds with OK status to all incoming binds.
- Responds with incremental message ids to all incoming
submit_sm
packets (regardless of the bind state).
defmodule MC do
use SMPPEX.Session
alias SMPPEX.Pdu
alias SMPPEX.Pdu.Factory, as: PduFactory
def child_spec(port) do
Supervisor.child_spec(
{
SMPPEX.MC,
session: {__MODULE__, []},
transport_opts: [port: port]
},
[]
)
end
def init(_socket, _transport, []) do
{:ok, 0}
end
def handle_pdu(pdu, last_id) do
case Pdu.command_name(pdu) do
:submit_sm ->
{:ok, [PduFactory.submit_sm_resp(0, to_string(last_id)) |> Pdu.as_reply_to(pdu)], last_id + 1}
:bind_transmitter ->
{:ok, [PduFactory.bind_transmitter_resp(0) |> Pdu.as_reply_to(pdu)], last_id}
_ ->
{:ok, last_id}
end
end
end
This server can be started by providing {MC, port}
as a child of some supervisor.