Страничка курса: https://maxcom.github.io/scala-course-2020/
Основные источники асинхронности в JVM:
Протоколы, в которых клиент открывает tcp соединение с сервером и они обмениваются запросами/сообщениями.
Это HTTP, SMTP, XMPP и многие другие.
private val executor: Executor =
Executors.newCachedThreadPool()
val serverSocket: ServerSocket = new ServerSocket(2000)
val greetings: Array[Byte] =
"Hello, world!".getBytes(US_ASCII)
while (true) { // упрощенный вариант сервера
val socket: Socket = serverSocket.accept()
executor.execute { () ⇒
// блокирующийся ввод-вывод
socket.getOutputStream.write(greetings)
socket.close()
}
}
упрощенная схема реализации
"Пассивные" клиенты занимают потоки:
Не предполагает распараллеливания обработки одного запроса
Трудности с обработкой таймаутов и fallback.
Можно ли обработать всех клиентов в одном потоке?
Да, с использованием мультиплексированного ввода вывода.
Такая архитектура показала свою эффективность.
Например:
Java NIO - низкоуровневые API для ввода-вывода.
"New IO" - "новый" API (2002 год).
Дополняется в каждой новой версии JDK.
Java IO - потоковые чтение/запись
Java NIO - блочные чтение/запись
Блочный I/O оптимальнее потокового, он избегает лишних копирований
и преобразований данных.*
* ByteBuffer - кошмар разработчика.
Нам NIO интересен тем, что только в нем есть неблокирующий ввод-вывод.
socketChannel.configureBlocking(false)
Блокирущий read: читает сколько может;
если данных нет - ждет
Неблокирующий read: мгновенно читает сколько может; нет данных - пустой результат
Блокирующий write: пишет всё что передали; если клиент не успевает - ждет и продолжает
Неблокирующий write: мгновенно пишет сколько может; если клиент не готов - ничего не пишет
executor.execute { () ⇒
val buffer: ByteBuffer = ByteBuffer.wrap(greetings)
while (buffer.hasRemaining) { // busy wait - плохая идея
socket.write(buffer)
}
socket.close()
}
val selector: Selector = Selector.open()
socket.configureBlocking(false)
val key = socket.register(selector, SelectionKey.OP_WRITE)
while (buffer.hasRemaining) {
selector.select() // блокируется
if (key.isWritable) {
socket.write(buffer)
}
}
Вернулись к блокировкам?
val selector: Selector = Selector.open()
// можно зарегистрировать много сокетов
serverSocket.register(selector, SelectionKey.OP_ACCEPT,
() -> println("Got new connection"))
while (true) {
selector.select()
val selectionKeys = selector.selectedKeys.asScala
selectionKeys.foreach { sk ⇒
sk.attachment().asInstanceOf[Runnable].run()
}
}
Итого:
Сложности?
Запутанная логика чтения - нарезка потока от клиента на произвольные блоки.
Запись только по готовности клиента
Запутанная машина состояний TLS
Обычно не проблема - программист работает или с готовым сервером, или с framework (netty, akka-io и т.п.)
(пример реализации)
Раньше наши обработчики работали
прямо в нашем потоке.
Сделаем их функциями
Unit ⇒ Future[Action]
Action - кодирует следующую операцию над сокетом
selectionKeys.foreach { sk ⇒
sk.interestOps(0) // не ждем новых событий
sk.attachment().asInstanceOf[() ⇒ Future[Action]].apply()
}
но как "вернуть" сокет обратно в работу?
// неблокирующаяся очередь, построенная на CAS операциях
val queue = new ConcurrentLinkedQueue[Runnable]
val executor: Executor = task ⇒ queue.add(task)
очередь разбираем цикле обработки событий:
var task: Runnable = _
while ({ task = queue.poll(); task } != null) {
task.run()
}
val reactorExecutor = ExecutionContext.fromExecutor { r ⇒
queue.add(r)
selector.wakeup()
}
selectionKeys.foreach { sk ⇒
sk.interestOps(0)
sk.attachment()
.asInstanceOf[() ⇒ Future[Action]]
.apply()
.onComplete(processAction)(reactorExecutor)
}
Итого мы получили:
Все современные сервера на JVM реализуют комбинированный подход.
Похожий event-loop используется в клиентах к веб-сервисам и к некоторым СУБД.
Функция выполнения запроса возвращает Future, которая обрабатывается в event loop клиента.
Протокол передачи данных, придуманный для Web.
Придуман для "классических" сайтов:
Позже эволюционировали в JavaScript-приложения
HTTP используется для взаимодействия сервисов между собой.
Две "живых" версии стандарта:
Будем говорить про HTTP/1.1
Схема "запрос-ответ".
Сервер ожидает соединений клиентов.
Сервер получает запрос и формирует ответ.
В одном соединении запросы обрабатываются последовательно.
Клиент может открыть несколько соединений к серверу (браузеры - до 6).
Запрос состоит из:
Ответ состоит из
Строка запроса
GET /scala-course-2020/slides/day7.html HTTP/1.1
Метод, путь ресурса и версия протокола
Путь - "адрес" ресурса на сервере;
"метод" - действие над ресурсом
Основные методы:
Всего около 20 методов, если включить все расширения
Заголовки запроса:
Host: localhost:9000
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7
Cache-Control: max-age=0
If-Modified-Since: Sun, 25 Mar 2018 07:25:21 GMT
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36
Переносы строк запрещены в современном HTTP
Тело запроса: данные, передаваемые на сервер
Тело - данные запроса, поля формы, загружаемые данные и т.п.
Тело может быть почти у любого метода, даже у GET (см. RFC 7231, раздел 4.3.1).
Два режима передачи тела запроса:
известной длины и неизвестной (chunked).
Строка ответа
HTTP/1.1 200 OK
версия протокола и статус ответа
Статус - трехзначный код плюс его описание
1xx - информационные коды
2xx - успешные коды
3xx - перенаправления
4xx - ошибки клиента
5xx - ошибки сервера
Хороший справочник по кодам - в английской википедии.
Заголовки ответа
Server: QRATOR
Date: Sun, 25 Mar 2018 08:57:43 GMT
Content-Type: text/html;charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
Keep-Alive: timeout=15
Set-Cookie: CSRF_TOKEN="GjxRyqELgF6WoDWtvm3dMQ=="; Version=1; Max-Age=64281600; Expires=Tue, 07-Apr-2020 08:57:43 GMT; Path=/
Cache-Control: private
Strict-Transport-Security: max-age=7776000
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Expires: Sat, 24 Mar 2018 12:57:43 GMT
Last-Modified: Sun, 25 Mar 2018 08:57:41 GMT
Set-Cookie: JSESSIONID=1485581EB69CB469B9E766D8DCEF4652; Path=/; Secure; HttpOnly
Тело ответа - содержимое ресурса
План задания
Как строить API поверх HTTP?
Можно инкапсулировать протокол поверх HTTP.
Но это сложно и не позволяет использовать полезные свойства протокола.
REST - методика создания API поверх протокола HTTP
"REST" как функционально программирование - его все любят, но никто точно не знает что это такое.
Подробнее рассмотрим на примере Play Framework
Современный "легкий" framework для создания сайтов и веб-приложений, альтернатива традиционным Java-фреймворкам.
Никаких сервлетов, "контейнеров" и т.п.
Быстрый асинхронный веб-сервер на базе Netty или Akka-HTTP.
плюс опциональные
компоненты:
Play подходит для "классических" сайтов, для разработки JavaScript-приложений, и для разработки сервисов.
sbt-web - сборка JavaScript приложения
средствами sbt
webjars - управление зависимостями JavaScript
Добавляем Play в готовый проект
(создать проект с "нуля" проще - см. документацию)
Выбираем версию sbt: project/build.properties
sbt.version=1.3.8
Создаем project/plugins.sbt
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.0")
Включаем Play и используем стандартные пути файлов:
libraryDependencies += guice
enablePlugins(PlayScala)
disablePlugins(PlayLayoutPlugin)
PlayKeys.playMonitoredFiles ++=
(sourceDirectories in
(Compile, TwirlKeys.compileTemplates)).value
build.sbt
Создаем пустой конфиг
src/main/resources/application.conf
Отключаем фильтр по имени хоста в application.conf:
play.filters.disabled+=play.filters.hosts.AllowedHostsFilter
В демо-проекте он нам не нужен,
в реальной жизни его
нужно настроить
На этом этапе можно уже запустить веб-сервер:
sbt run
в терминале или через меню в IDEA
Сервер запускается в режиме разработчика
http://localhost:9000/
при изменениях сервер не нужно перезапускать:
изменения
автоматически подхватываются
Hello, world!
src/main/scala/controllers/HelloWorld.scala
package controllers
import javax.inject.Inject
import play.api.mvc.{AbstractController, ControllerComponents}
class HelloWorld @Inject()(cc: ControllerComponents)
extends AbstractController(cc) {
def hello() = Action {
Ok("Hello, world!") // просто text/plain
}
}
Таблица роутинга запросов:
src/main/resources/routes
GET / controllers.HelloWorld.hello
Action может быть асинхронным:
class HelloWorld @Inject()(cc: ControllerComponents)
extends AbstractController(cc) {
implicit val ec: ExecutionContext = defaultExecutionContext
def hello() = Action.async {
Future {
Ok("Hello, world!")
}
}
}
Java-сервлеты научились быть асинхронными через 10 лет после появления первой версии.
И еще 3 года появился асинхронный ввод-вывод.
Вернемся к REST:
URI должен указывать на объект на сервере
Например профиль пользователя admin
/users/admin
Действия над пользователями:
В Play роутинг может быть такой:
GET /users controllers.Users.users()
POST /users controllers.Users.createUser()
GET /users/:login controllers.Users.getAdmin(login)
PUT /users/:login controllers.Users.setAdmin(login)
PATCH /users/:login controllers.Users.updateAdmin(login)
DELETE /users/:login controllers.Users.dropAdmin(login)
Коды HTTP классифицируют ошибки
(подробности - в теле ответа)
Пример
Ошибки клиента
Ошибки сервера
Будем делать "классический" сайт, а не приложение.
(не будем писать код на front-end)
Задание:
Сделаем HTML форму для ввода текста.
Обработчик вернет класс текста (позитив/негатив) и исходный текст с диагностикой.
Используем HTML шаблоны Twirl
src/main/twirl/views/form.scala.html
@(text: Option[String], category: Option[String],
debug: Option[String])
Классификатор
@if(category.nonEmpty) {
Результат: @category
@debug
}
В контроллере возвращаем HTML:
def showForm() = Action {
Ok(views.html.form(None, None, None))
}
Данные формы при методе GET передаются параметрами в URL
В функцию обработки запроса нужно добавить параметр, Play передаст туда значение из URL.
Страшный "программистский" дизайн?
Можно легко исправить
(опционально, если есть желание)
Используем готовый CSS шаблон
Популярный вариант: Bootstrap
Его можно подключить как зависимость в sbt
Пример - на сайте webjars
Использование - в документации на Bootstrap
Напоминаю: