Страничка курса: https://maxcom.github.io/scala-course-2020/
Сегодня последняя лекция курса
Совмененный асинхронный HTTP сервер и клиент.
(основное использование - серверная сторона)
Построен на технологиях Akka.
Асинхронность - Future и Akka Streams.
Построен на:
Начнем с Hello, world!
build.sbt
libraryDependencies +=
"com.typesafe.akka" %% "akka-http" % "10.1.11"
// версию Akka выбираем сами (>= 2.5)
libraryDependencies +=
"com.typesafe.akka" %% "akka-stream" % "2.6.4"
Импортируем scaladsl
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
(не путаем с Java API)
// нужна "классическая" система
implicit val system: ActorSystem = ActorSystem("my-system")
// обработчик запросов
val route: Route = ???
val bindingFuture: Future[Http.ServerBinding] =
Http().bindAndHandle(route, "localhost", 8080)
val route: Route =
path("hello") { // путь "/hello"
get { // метод "GET"
complete(
HttpEntity(
ContentTypes.`text/html(UTF-8)`,
"Say hello to akka-http
"))
}
}
Обработчик запросов
type Route = RequestContext => Future[RouteResult]
// неявно преобразоввывается в Flow из akka-streams
// Flow[HttpRequest, HttpResponse, Any]
Альтернативный роутинг:
Стандартный роутинг (плюсы):
Стандартный роутинг (минусы):
type Route = RequestContext => Future[RouteResult]
RouteResult - ADT
Directive - элемент роутинга
abstract class Directive[L](implicit val ev: Tuple[L]) {
// tuple-apply
def tapply(f: L => Route): Route
... // функции модификации и композиции
}
Directive может:
Пример:
path("hello") {
get { // Directive0 требует метод GET
complete("Say hello to akka-http")
}
} ~ // Directive1 требует путь "order/$id"
path("order" / IntNumber) { id => // извлеченный id
complete(s"Order $id")
}
Композиция:
path("order" / IntNumber) { id =>
(get | put) {
extractMethod { m =>
complete(s"Received ${m.name} request for order $id")
}
}
}
// собрем custom директиву
val orderGetOrPutWithMethod =
path("order" / IntNumber) & (get | put) & extractMethod
val route =
orderGetOrPutWithMethod { (id, m) =>
complete(s"Received ${m.name} request for order $id")
}
Работа с Future
val future: Future[String] = Future.successful("value")
val route =
path("success") {
onSuccess(future) { v => // Directive[Tuple1[String]]
complete(s"Future was completed with $v.")
}
}
Примеры директив будут во второй половине
def complete(m: => ToResponseMarshallable): StandardRoute
// implicit преобразование
typeclass напрямую не используется
type ToResponseMarshaller[T] = Marshaller[T, HttpResponse]
sealed abstract class Marshaller[-A, +B] {
def apply(value: A)(implicit ec: ExecutionContext):
Future[List[Marshalling[B]]]
...
}
возможно несколько вариантов маршаллинга
(content negitiation)
JSON: akka-http-json
build.sbt
libraryDependencies +=
"de.heikoseeberger" %% "akka-http-play-json" % "1.31.0"
import de.heikoseeberger.akkahttpplayjson.PlayJsonSupport._
...
complete(Json.obj("value" -> "Hello, world!"))
...
case class Data(value: String)
object Data {
implicit val format: OFormat[Data] = Json.format[Data]
}
get {
complete(Data("Hello, world"))
} ~ post {
// entity - Directive1
// as[Data] ищет implicit FromRequestUnmarshaller[T]
entity(as[Data]) { data =>
complete(s"Got ${data.value}")
}
}
Варианты:
(*) готовая реализация
Чтение: обычно в клиенте или из файла/БД. Но бывает и для тела запроса:
implicit val jsonStreamingSupport: JsonEntityStreamingSupport =
EntityStreamingSupport.json()
entity(as[Source[Data, _]]) { data =>
Используем вне routing dsl:
Flow[ByteString]
.via(jsonStreamingSupport.framingDecoder)
.mapAsync(1)(bytes => Unmarshal(bytes).to[Data])
Разбиваем по переводу строки
// вместо jsonStreamingSupport.framingDecoder
Framing.delimiter(
ByteString("\r\n"),
maximumFrameLength = 16384,
allowTruncation = false)
Запись JSON
get {
val source: Source[Data, NotUsed] =
Source(Seq(Data("value")))
complete(source)
}
Проблема - обработка ошибок
Ошибки вместо пустых ответов:
def dataOrFail[T]
(source: Source[T, NotUsed]): Future[Source[T, NotUsed]] = {
source
.prefixAndTail(1)
.runWith(Sink.head)
.map { case (first, rest) =>
Source(first).concat(rest)
}
}
(future падает при ошибке получения 1-го элемента)
Кеширование - полезная "фича" HTTP.
Кешируются GET запросы без тела.
Поддерживается браузерами и многими http-клиентами.
Три варианта заголовков:
Не указано - на усмотрение клиента. Обычно:
если ожидаем странные клиенты то лучше всегда явно указывать
Выключение кеширования:
Cache-control: no-store, no-cache, must-revalidate
Pragma: no-cache
Expires: 0
respondWithHeader(
`Cache-Control`(`no-store`, `no-cache`, `must-revalidate`)) {
complete("Hello, world")
}
Кеширование с таймаутом
Cache-control: max-age=${seconds}
"Вечное" кеширование
Cache-control: max-age=365000000, immutable
Таймаут для меняющихся ответов должен быть очень коротким.
Кодируем "версию" в URL - иногда весьма эффективно.
Для REST можно комбинировать с redirect.
Кеширование с проверкой актуальности кеша.
Когда это эффективно?
Пример быстрых проверок:
Помечаем ответы заголовком, на выбор:
// нестандартный DateTime из Akka Http
val lastModified: DateTime = ???
conditional(lastModified) { // 304 Not Modified если не изменился
respondWithHeader(`Cache-Control`(`must-revalidate`)) {
complete("Hello, world")
}
}
24 апреля - демо. Стартуем в 17:00.
Показываем свой проект через Zoom.
Примерно 10 минут - покажите UI и код, расскажите о достижениях и проблемах. 5 минут на вопросы.
Напоминаю: