Страничка курса: https://maxcom.github.io/scala-course-2022/
Вторая часть - Akka HTTP.
Откуда берется асинхронность?
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 год).
Редко когда пишем на "сыром" NIO - используем готовые компоненты.
java.io - потоковые чтение/запись
Java.nio - блочные чтение/запись,
неблокирующий ввод-вывод
(+ работа с файловой системой)
Блочный I/O оптимальнее потокового, он избегает лишних копирований и преобразований данных.
(ByteBuffer - кошмар разработчика)
Специальный режим сокета.
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)
}
}
Вернулись к блокировкам?
Event Loop - один Selector на всех клиентов
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 и т.п.)
в JVM обычно не получается работать в одном потоке.
(пример реализации)
"Работу" делаем вне event loop.
Запускаем асинхронный процесс.
Ввод-вывод делаем в event loop
(i/o не потокобезопасный)
selectionKeys.foreach { sk ⇒
sk.interestOps(0) // не ждем новых событий
sk.attachment().asInstanceOf[Runnable].run()
}
но как "вернуть" сокет обратно в event loop?
// неблокирующаяся очередь, построенная на 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 executor: Executor = { r ⇒
queue.add(r)
selector.wakeup()
}
используем этот Executor для
возвращения в event loop
Итого мы получили:
Все современные сервера на JVM реализуют комбинированный подход.
Даже для сервлетов
(blocking over non-blocking)
Event-loop в HTTP-клиенте,
драйверах СУБД (кроме JDBC).
Число параллельных запросов не регулируется размером thread pool!
Протокол передачи данных, придуманный для Web.
Придуман для "классических" сайтов:
эволюционировали в JavaScript-приложения
спасибо gmail за изобретение такой схемы работы
HTTP используется для взаимодействия сервисов между собой.
Клиенты и серверы есть для всех языков программирования.
Три "живых" версии стандарта:
Будем говорить про HTTP/1.1
На практике не видел 2.0/3.0 между сервисами.
Схема "запрос-ответ".
Сервер ожидает соединений клиентов.
Сервер получает запрос и формирует ответ.
В одном соединении запросы обрабатываются последовательно.
Клиент может открыть несколько соединений к серверу (браузеры - до 6).
Запрос состоит из:
Ответ состоит из
Строка запроса
GET /scala-course-2020/slides/day7.html HTTP/1.1
"Метод" - действие над ресурсом
Путь - "адрес" ресурса на сервере
Основные методы:
У методов есть своя семантика по кешированию, безопасности и идемпотентности.
Заголовки запроса:
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
переносы строк запрещены (в отличии от почты)
Тело - данные запроса, поля формы, загружаемые данные и т.п.
Тело может быть почти у любого метода, даже у GET (см. RFC 7231, раздел 4.3.1).
Два режима передачи тела запроса:
известной длины и неизвестной (chunked).
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
заголовки можно делать свои
Тело ответа - содержимое ресурса
Вторая часть - Akka HTTP.