Забудьте о вечном опросе: встречайте Server-Sent Events в Kotlin
Представьте: вы открываете биржевой терминал, и котировки обновляются сами собой. Или заходите в админку сайта, а там — живая лента количества пользователей онлайн. Никаких кнопок «Обновить», никаких бесконечных запросов к серверу. Просто поток данных, льющийся в реальном времени.
В мире веб-разработки за это отвечают Server-Sent Events (SSE) — протокол, который позволяет серверу самостоятельно отправлять данные клиенту через одно HTTP-соединение. И сегодня я покажу, как реализовать SSE в Kotlin с помощью фреймворка Ktor. Это будет не сухая теория, а полноценный рабочий пример от сервера до клиента.
Что такое Server-Sent Events и почему это круто?
Когда-то давно, чтобы получить свежие данные, разработчики использовали polling (опрос). Клиент каждые N секунд стучался на сервер: «Ну что, есть новости?». Сервер отвечал: «Нет». И так по кругу. Это дико неэффективно: 99% запросов — пустышки, а сервер тратит ресурсы на обработку лишних соединений.
SSE решает проблему кардинально: клиент открывает одно соединение, и сервер сам отправляет данные, когда они появляются. Это как подписка на новости — вы просто ждёте, и вам приносят свежий номер.
Преимущества Server-Sent Events в Kotlin:
- Простота — работает поверх обычного HTTP, не требует дополнительных библиотек на клиенте (кроме встроенного EventSource в браузере).
- Автоматическое восстановление — если соединение оборвётся, браузер сам переподключится.
- Экономия ресурсов — одно соединение вместо десятков.
- Нативная поддержка в Kotlin — с Ktor и корутинами писать SSE-сервер — одно удовольствие.
Главный минус? SSE — односторонний канал (только от сервера к клиенту). Если нужно общаться в обе стороны — смотрите в сторону WebSocket. Но для уведомлений, лент новостей, котировок, мониторинга — SSE идеален.
Когда стоит использовать SSE в вашем Kotlin-проекте?
- Лента уведомлений — новые сообщения, комментарии, лайки.
- Мониторинг и логи — стриминг логов с сервера в реальном времени.
- Биржевые данные — обновление курсов валют, акций, криптовалют.
- Прогресс выполнения задач — отчёт о загрузке файла или обработке данных.
- Системы оповещения — аварийные сигналы, статусы устройств.
Если вы ищете способ передавать обновления с сервера на клиент без головной боли — SSE Kotlin ваш выбор. Давайте перейдём к коду.
Настройка проекта: Ktor + SSE
Для начала создадим новый проект Ktor. Я буду использовать Gradle Kotlin DSL. В build.gradle.kts добавляем зависимости:
val ktor_version = "2.3.7"
dependencies {
implementation("io.ktor:ktor-server-core:$ktor_version")
implementation("io.ktor:ktor-server-netty:$ktor_version")
implementation("io.ktor:ktor-server-content-negotiation:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version")
implementation("ch.qos.logback:logback-classic:1.4.14")
implementation("io.ktor:ktor-server-cors:$ktor_version")
}
Обратите внимание: для SSE нам не нужна отдельная библиотека — Ktor поддерживает потоковую передачу данных из коробки через корутины.
Создаём SSE-эндпоинт
Теперь самое интересное — напишем сервер, который будет отправлять клиенту события каждые 2 секунды. Это будет симуляция биржевых котировок.
import io.ktor.server.application.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.delay
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import java.util.*
@Serializable
data class StockPrice(
val symbol: String,
val price: Double,
val timestamp: Long
)
fun main() {
embeddedServer(Netty, port = 8080) {
configureRouting()
}.start(wait = true)
}
fun Application.configureRouting() {
routing {
get("/api/stocks") {
// Устанавливаем заголовки для SSE
call.response.header("Content-Type", "text/event-stream")
call.response.header("Cache-Control", "no-cache")
call.response.header("Connection", "keep-alive")
val json = Json { ignoreUnknownKeys = true }
val symbols = listOf("AAPL", "GOOGL", "TSLA", "MSFT")
// Запускаем бесконечный поток данных
call.response.writeChannel {
while (true) {
val price = StockPrice(
symbol = symbols.random(),
price = 100.0 + Random().nextDouble() * 200,
timestamp = System.currentTimeMillis()
)
val data = "data: ${json.encodeToString(StockPrice.serializer(), price)}\n\n"
writeStringUtf8(data)
flush()
delay(2000)
}
}
}
}
}
Разберём код по косточкам:
- Content-Type: text/event-stream — обязательный заголовок для SSE.
- Cache-Control: no-cache — чтобы браузер не кешировал поток.
- writeChannel — корутинный способ записи в ответ. Мы используем
writeStringUtf8для отправки строк. - Формат сообщения — каждая порция данных начинается с
data:и заканчивается двумя переводами строки\n\n.
Важный момент: мы не закрываем соединение — цикл while(true) держит его открытым, пока клиент не отключится. Ktor автоматически обработает разрыв соединения.
Улучшаем SSE: идентификаторы и типы событий
Базовая реализация работает, но в реальных проектах нужно больше контроля. Добавим ID события и тип события. Это поможет клиенту обрабатывать разные типы данных и восстанавливать пропущенные сообщения.
var eventId = 0L
call.response.writeChannel {
while (true) {
eventId++
val price = StockPrice(...)
val message = buildString {
append("id: $eventId\n")
append("event: stockUpdate\n")
append("data: ${json.encodeToString(StockPrice.serializer(), price)}\n\n")
}
writeStringUtf8(message)
flush()
delay(2000)
}
}
Теперь клиент может:
- Игнорировать события, если тип не подходит.
- При переподключении отправить
Last-Event-IDи получить