Photo by Science in HD


Если вам когда-либо приходилось решать задачу отправки SMS из кода ваше��о приложения, скорее всего, вы использовали готовое REST API поставщика дополнительных услуг. Но что происходит после того, как поставщик получит ваш запрос? Какие протоколы используются и какой путь проходит текст сообщения, прежде чем оказаться на экране мобильного терминала пользователя?


В этой статье вы найдёте:


  1. Немного теории и терминологии SMPP-протокола: SMSC, ESME, PDU, MO/MT SM.
  2. Краткий обзор существующих библиотек для работы с SMPP в Erlang/Elixir.
  3. Пример реализации асинхронного клиента при помощи библиотеки SMPPEX. Возможно, он будет полезен тем, кто ещё не использовал Elixir-библиотеки в Erlang-проектах.
  4. Информацию по обработке 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-проекте.


Для иллюстрации возможностей библиотеки возьмём простой сценарий:


  1. Поднять линк.
  2. Отправить сообщение.
  3. Дождаться отчёта о доставке либо обработать входящие сообщения.

Придумаем дополнительные требования. Допустим, мы хотим отправлять 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-проектах.