
Photo by Science in HD
Если вам когда-либо приходилось решать задачу отправки SMS из кода ваше��о приложения, скорее всего, вы использовали готовое REST API поставщика дополнительных услуг. Но что происходит после того, как поставщик получит ваш запрос? Какие протоколы используются и какой путь проходит текст сообщения, прежде чем оказаться на экране мобильного терминала пользователя?
В этой статье вы найдёте:
- Немного теории и терминологии SMPP-протокола: SMSC, ESME, PDU, MO/MT SM.
- Краткий обзор существующих библиотек для работы с SMPP в Erlang/Elixir.
- Пример реализации асинхронного клиента при помощи библиотеки SMPPEX. Возможно, он будет полезен тем, кто ещё не использовал Elixir-библиотеки в Erlang-проектах.
- Информацию по обработке deliver_sm, MO SM.
Чего тут точно нет, так это информации по отправке коротких сообщений через SIGTRAN.
Определимся с терминами и понятиями
Прежде чем погружаться в протоколы и код, предлагаю разобраться в терминологии. Если быть придирчивым к определениям, то отправить SMS невозможно. Вспоминается момент из «Джентльменов удачи»: «Кто ж его отправит — он же сервис!» SMS — акроним от short message service, что на русский переводится как «сервис/служба коротких сообщений». Если возвращаться к шутке, то отправляем мы SM, т. е. короткие сообщения, используя SMS — сервис коротких сообщений.
У каждого оператора мобильной связи есть компонент, отвечающий за работу службы коротких сообщений. Это так называемый SMS-центр, он же SMSC, он же SMS-SC. Его задачами являются хранение, передача, конвертация и доставка SM-сообщений. Наиболее распространенным внешним протоколом взаимодействия с SMSC является SMPP. SMPP — клиент-серверный протокол-комбайн, отвечающий за обмен короткими сообщениями в одноранговой сети. Источником SM могут быть устройства и приложения. В терминологии SMPP их называют ESME.
Давайте ответим на вопросы в начале статьи. Итак, ваше сообщение по REST API или SMPP попало к поставщику услуг, у которого заключён договор с одним или несколькими операторами связи или другими посредниками. Сервер поставщика подключается к SMSC и отправляет по SMPP ваше SM, затем получает отчёт о доставке или ответное SM. В процессе обработки SM могут проходить через маршрутизаторы — RE. SMSC, сходив в HLR, узнает местоположение абонента и доставит SM абоненту. Общая картина и понимание проблемы, надеюсь, у вас появились. Давайте погрузимся в протокольные тонкости.
SMPP
Выше я сказал, что SMPP — протокол-комбайн. Подобный эвфемизм я позволил себе из-за того, что SMPP применим не только для организации обмена SMS, с его помощью можно организовать различные сервисы: ESM, голосовой почты, уведомлений, сотового радиовещания, WAP, USSD и прочие. Весь обмен происходит с помощью пар запрос-ответ. Их называют PDU — блоками данных или пакетами.
Инициализация подключения
Перед началом обмена мы должны указать, в каком направлении будет работать наше подключение. За это отвечают соответствующие команды:
- bind_transmitter — клиент может только отправлять запросы на сервер;
- bind_receiver — клиент только получает ответы от сервера;
- bind_transceiver — клиент работает в обоих направлениях, этот режим появился в SMPPv3.4.
Безопасность
При выполнении команды привязки мы должны передать параметры безопасности для идентификации нашего ESME: system_id, system_type и password.
SMPP в экосистеме OTP
Недавно у хорошего друга возник вопрос по работе с SMPP в Erlang. Собственно, благодаря этому и родился этот текст.
Казалось бы, никаких проблем. Проверенный временем телеком-протокол с давно известными проблемами с одной стороны и телеком-язык с другой. Все должно быть просто и весело, как в песенке PPAP.

Но есть нюанс… Найти адекватную реализацию оказалось непросто.
Наверняка все, кто пытался серьёзно работать с Erlang, знают про его недостаток, связанный с ограниченным выбором библиотек. С SMPP такая же история — в OTP нет штатной поддержки этого протокола, а на первой странице выдачи Гугла творится что-то странное:
- esmpp — библиотека со странным API и отсутствующим сообществом;
- древний OSERL — проект стартовал 11 лет назад, последний коммит сделали более 5 лет назад;
- неподдерживаемый smpp34 — последний коммит был более 10 лет назад;
- куча вопросов вида «Какую библиотеку/клиента использовать для SMPP?» на тематических форумах.
Лично я бы загрустил от такого разнообразия существующих решений. Особенно, когда хочется асинхронного режима, адекватной поддержки SMPP 3.4 и возможности написать как клиент, так и сервер. Но на помощь приходит Elixir и библиотека SMPPEX.
SMPPEX
Про сам проект скажу только то, что он активный и готов для продакшен-применения. Это стабильная, всесторонняя библиотека для SMPP с продуманным API, хорошей поддержкой разработчиков и отсутствием проблем с лицензией.
От слов к делу
Сначала можно ознакомиться с примерами синхронного и асинхронного клиента в документации. Затем можно перейти к более сложным вещам в контексте использования SMPPEX в Erlang-проекте.
Для иллюстрации возможностей библиотеки возьмём простой сценарий:
- Поднять линк.
- Отправить сообщение.
- Дождаться отчёта о доставке либо обработать входящие сообщения.
Придумаем дополнительные требования. Допустим, мы хотим отправлять MT SM, получать отчёты о доставке и MO SM. При этом по каким-то причинам нам нужны кастомные PDU и полный контроль над линком, поэтому за формирование submit_sm PDU и обработку всех входящих PDU мы будем отвечать сами. При этом мы не должны забывать про требование асинхронности.
Работа с линком
Надеюсь, что сложностей с установкой зависимости из hex.pm у вас не возникло и мы можем приступить к написанию кода. Как говорилось выше, работать мы будем в асинхронном режиме, поэтому запускаем клиента с помощью модуля SMPPEX.ESME:
'Elixir.SMPPEX.ESME':start_link(SmscHost, SmscPort, {?MODULE, [Opts]})Для синхронного режима существует SMPPEX.ESME.Sync.
Наш клиент готов, и мы можем сделать привязку к SMSC. Предположим, что SMSC поддерживает SMPPv3.4 и мы можем использовать transceiver режим:
'Elixir.SMPPEX.Pdu.Factory':bind_transceiver(SystemId, Pass)Если всё прошло хорошо, нам должен прийти PDU с командой bind_transceiver_resp:
bind_transceiver_resp = 'Elixir.SMPPEX.Pdu':command_name(Pdu)Формирование PDU для MT SM
Линк поднят, и мы можем отправить наше сообщение. В терминах SMPP, сообщения адресуемые абоненту называются Mobile Terminated (MT SM). Соберём PDU для него:
submit_sm_pdu(SourceMsisdn, DestMsisdn, Message, Ttl) ->
{ok, CommandId} = 'Elixir.SMPPEX.Protocol.CommandNames':id_by_name(submit_sm),
{D, {H, M, S}} = calendar:seconds_to_daystime(Ttl),
VP = lists:flatten(io_lib:format("0000~2..0w~2..0w~2..0w~2..0w000R", [D, H, M, S])),
'Elixir.SMPPEX.Pdu':new(
CommandId,
#{
source_addr => SourceMsisdn,
source_addr_ton => 1,
source_addr_npi => 1,
destination_addr => DestMsisdn,
dest_addr_ton => 1,
dest_addr_npi => 1,
short_message => Message,
data_coding => 246,
protocol_id => 127,
%% For concatenated messages
esm_class => 64,
registered_delivery => 1,
validity_period => list_to_binary(VP)
}
).Обработка отчетов о доставке и MO SM
Mobile originated (MO SM) — сообщения от абонента.
После отправки сообщения в линк SMSC ответит нам submit_sm_resp, в котором указан уникальный ID нашего сообщения:
MsgId = 'Elixir.SMPPEX.Pdu':mandatory_field(Pdu, message_id)Теперь нам необходимо дождаться deliver_sm с этим message_id.
Чтобы отличить отчёты о доставке от MO SM, проанализируем esm_class:
EsmClass = 'Elixir.SMPPEX.Pdu':mandatory_field(Pdu, esm_class),
case <<EsmClass>> of
<<_Head : 2, 0 : 1, 0 : 1, 0 : 1, 1 : 1, _Tail : 2>> -> handle_delivery_receipt(Pdu);
<<_Head : 2, 0 : 1, 0 : 1, 0 : 1, 0 : 1, _Tail : 2>> -> handle_standart_message(Pdu);
Some -> ?LOG_ERROR("unknown deliver_sm: ~p", [Some])
endПри этом для обработки отчётов о доставке нам достаточно узнать ID доставленного сообщения:
SmsId = 'Elixir.SMPPEX.Pdu':field(Pdu, receipted_message_id)А для входящих сообщений узнать номер отправителя:
Msisdn = 'Elixir.SMPPEX.Pdu':field(Pdu, source_addr)и полезное содержимое сообщения:
Payload = 'Elixir.SMPPEX.Pdu':field(Pdu, short_message)Как известно, спецификация SMPP требует deliver_sm_resp в ответ на deliver_sm. Поэтому после обработки отчёта о доставке и входящего сообщения мы должны ответить deliver_sm_resp. Создадим PDU для него:
deliver_sm_resp_pdu(MessageId) ->
{ok, CommandId} = 'Elixir.SMPPEX.Protocol.CommandNames':id_by_name(deliver_sm_resp),
CommandStatus = 0,
SeqNumber = 0,
'Elixir.SMPPEX.Pdu':new({CommandId, CommandStatus, SeqNumber}, #{message_id => MessageId}, #{}).Я специально не указываю номер команды, добавим его автоматически:
ReplyPdu = 'Elixir.SMPPEX.Pdu':as_reply_to(deliver_sm_resp(SmsId), Pdu)Весь код демопроекта можно найти в репозитории.
OTP-тренды
В 2020 году на тренды развития OTP и BEAM всё большее влияние оказывает сообщество Elixir. Чаще и чаще хорошие инструменты и полезные библиотеки можно найти на Elixir, а не на Erlang. Это не повод для тревоги за Erlang, просто Elixir смог заинтересовать и привлечь больше людей в своё сообщество, и это прекрасно. А благодаря OTP для использования той или иной библиотеки нам не важно, на чём она написана. Надеюсь, пример из статьи смог показать гибкость SMPPEX как инструмента и удобство применения библиотек, написанных на Elixir в Erlang-проектах.
