Akka HTTP на примере звонков и посылки SMS через Plivo

21 сентября 2015

Akka HTTP — это такая штука для написания своих веб-сервисов и хождения по HTTP в чужие, являющаяся преемником популярного веб-фреймворка Spray. Сегодня с помощью Akka HTTP мы напишем пару простых примеров клиентского и серверного кода. Поскольку писать телефонные книги уже как-то поднадоело (раз, два), примеры эти будут работать с сервисом plivo.com. Данный сервис позволяет звонить на телефоны, слать SMS и делать прочие подобного рода вещи.

Рассмотрим простую посылку HTTP-запроса. Подключаем к проекту следующие зависимости:

libraryDependencies ++= Seq(
    "com.typesafe.akka" %% "akka-actor" % "2.3.12",
    "com.typesafe.akka" %% "akka-http-experimental" % "1.0",
    "org.json4s" %% "json4s-jackson" % "3.2.11"
  )

Код:

package me.eax.examples.plivo.sms

import akka.actor._
import akka.stream._
import akka.http.scaladsl._
import akka.http.scaladsl.model._
import org.json4s._
import org.json4s.jackson.JsonMethods._

import scala.concurrent._
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global

object Main extends App {
  val authId = "Ololo"
  val authToken = "Trololo"
  val src = "NOTIFY" // "+13364960988"
  val dst = "+79161234567"
  val msg = "Your sms code: 7654"
  implicit val system = ActorSystem()
  implicit val materializer = ActorMaterializer()

  val fResponse = Http().singleRequest(
    HttpRequest(
      method = HttpMethods.POST,
      uri = s"https://api.plivo.com/v1/Account/$authId/Message/",
      headers = List(
        headers.Authorization(
          headers.BasicHttpCredentials(authId, authToken)
        )
      ),
      entity = HttpEntity(
        ContentTypes.`application/json`,
        compact(JObject(List(
          "src" -> JString(src),
          "dst" -> JString(dst),
          "text" -> JString(msg)
        )))
      )
    )
  )

  val fResult = {
    for {
      resp <- fResponse
    } yield {
      resp.status.isSuccess()
    }
  }

  val result = Await.result(fResult, Duration.Inf)
  println(s"Success: $result")
  System.exit(0)
}

Как видите, здесь шлется обычный POST-запрос, проставляются заголовки, тело запроса — ничего особенного. Для кодирования в JSON используется уже знакомый нам json4s. Разве что, не очень понятно, зачем нужен какой-то ActorMaterializer. Но нам это сейчас знать не обязательно. Раз нужен для чего-то, придется его просто дать.

Вы уже могли понять, что здесь происходит посылка SMS через API Plivo. Ничего примечательного в этом API нет. Просто регистрируете в системе номер, а затем указываете его в src при отправке SMS. Но для России на момент написания этих строк был нюанс. При отправке SMS на российский номер в src приходилось указывать «NOTIFY». Однако к моменту, когда вы будете читать эти строки, ситуация уже может и измениться.

Со звонками интереснее. API устроен таким образом, что нам понадобится не только посылать, но и самим принимать запросы. Например, когда пользователь берет трубку, Plivo дернет заданный нами URL. Притом, как только на это дергание придет ответ, звонящий робот положит трубку. То есть, нам нужно не только принять запрос, но и послать ответ на него только тогда, когда мы решим завершить звонок.

Новый веб-сервис создается так:

val serverSource = Http().bind(interface = "0.0.0.0",
                               port = listenPort)

Также нам понадобится хэндлер разпросов, который выглядит как-то так:

val reqHandler: HttpRequest => Future[HttpResponse] = {
  case req@HttpRequest(HttpMethods.POST, Uri.Path("/"), _, ent, _)
    if ent.contentType().mediaType ==
         MediaTypes.`application/x-www-form-urlencoded` =>

    // код в этом месте пропущен, так как он особо
    // ничем не примечателен

  case _: HttpRequest =>
    Future.successful {
      HttpResponse(
        StatusCodes.NotFound,
        entity = HttpEntity(
          MediaTypes.`text/html`,
          "<html><body>Not found!</body></html>"
        )
      )
    }
}

Идея такая же, как и в Finagle — сервис представляет собой лишь функцию, у которой на входе запрос, а на выходе футура с ответом.

И, наконец, связываем сервис с хэндлером:

val bindingFuture: Future[Http.ServerBinding] =
  serverSource.to(Sink.foreach { connection =>
    connection.handleWithAsyncHandler(reqHandler)
  }).run()

Внимательные читатели могут заметить тут элементы Akka Streams, однако эта тема уже выходит за рамки поста.

Собственно, это все, что касается написания сервиса. В полной версии исходного кода накручено еще много дополнительной логики, отвечающей непосредственно за совершение самого звонка. Но ничего нового об Akka HTTP мы из него не узнаем. Если желаете, можете ознакомиться с этой логикой самостоятельно в качестве упражнения.

Ссылки по теме:

А что вы думаете об Akka HTTP?

Дополнение: В качестве альтернативного сервиса для посылки SMS можно посоветовать Infobip. У Plivo бывают проблемы с российскими номерами. Он может какое-то время работать нормально, потом начать доставлять SMS с большой задержкой (несколько часов), потом и вовсе перестать доставлять SMS в Россию. Infobip в этом плане намного стабильнее. Пример посылки SMS через Infobip можно найти в заметке Как я выбирал скриптовый язык и остановился на Python.

Дополнение: Добавляем интроспекцию в Akka при помощи 100 строк кода

Метки: , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.