Введение: Почему Rust — идеальный кандидат для real-time?
Представьте себе ситуацию: вы пишете чат-сервер, биржевой терминал или систему мониторинга на Rust. Пользователи хотят получать обновления мгновенно. Никто не любит нажимать кнопку «Обновить» вручную — это прошлый век. Но как организовать доставку данных с сервера клиенту?
Долгое время единственным способом был long polling — элегантный, но немного «костыльный» трюк. Затем пришли WebSocket и перевернули мир веб-разработки. А теперь у нас есть Rust — язык, который позволяет выжать максимум из обоих подходов, не жертвуя безопасностью памяти.
В этой статье мы не просто сравним long polling и WebSocket в Rust — мы напишем рабочие примеры, запустим их и посмотрим, что происходит под капотом. Если вы пишете real-time приложение на Rust и сомневаетесь в выборе технологии — вы по адресу.
Что такое Long Polling на пальцах
Long polling — это техника, при которой клиент открывает HTTP-запрос к серверу и… ждёт. Сервер не отвечает сразу, а держит соединение открытым до тех пор, пока не появится новое событие или не истечёт таймаут.
Представьте, что вы звоните другу и говорите: «Позвони мне, когда придёт посылка» — и висите на линии час. Это long polling. Обычный polling — это когда вы перезваниваете каждые 5 минут: «Ну что, пришла?»
Реализация Long Polling на Rust с Actix-web
Давайте напишем простейший сервер на actix-web, который использует long polling. У нас будет очередь сообщений — когда появляется новое сообщение, все «ждуны» его получают.
use actix_web::{web, App, HttpServer, HttpRequest, HttpResponse};
use std::sync::Mutex;
use tokio::sync::broadcast;
struct AppState {
tx: broadcast::Sender<String>,
}
async fn long_poll_handler(data: web::Data<Mutex<AppState>>) -> HttpResponse {
let mut rx = {
let state = data.lock().unwrap();
state.tx.subscribe()
};
// Ждём новое сообщение в течение 30 секунд
match tokio::time::timeout(
std::time::Duration::from_secs(30),
rx.recv()
).await {
Ok(Ok(msg)) => HttpResponse::Ok().body(msg),
Ok(Err(_)) => HttpResponse::InternalServerError().finish(),
Err(_) => HttpResponse::NoContent().finish(), // таймаут
}
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let (tx, _) = broadcast::channel::<String>(100);
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(Mutex::new(AppState { tx: tx.clone() })))
.route("/poll", web::get().to(long_poll_handler))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
Клиент будет просто висеть на GET /poll. Когда придёт сообщение — сервер ответит. Если за 30 секунд ничего нет — вернёт 204 No Content, и клиент перезапросит.
Плюсы:
- Работает через обычный HTTP — не нужны прокси-серверы с поддержкой WebSocket
- Простота интеграции с существующими REST API
- Отлично подходит для корпоративных сетей, где прокси блокируют WebSocket
Минусы:
- Каждый «поллинг» держит соединение — при 10 000 клиентов это 10 000 открытых TCP-соединений
- Задержка: клиент всё равно переподключается после таймаута
- Сервер тратит ресурсы на поддержание «висящих» запросов
WebSocket в Rust: когда хочется скорости
WebSocket — это уже не «звонок и ожидание», а полноценный двусторонний канал. Сервер и клиент могут обмениваться сообщениями в любое время, без оверхеда на HTTP-заголовки.
В Rust экосистема WebSocket богата: tokio-tungstenite для низкоуровневой работы, actix-web со встроенной поддержкой, warp и другие.
Пишем WebSocket сервер на Rust с tokio-tungstenite
Вот пример простого эхо-сервера, который принимает WebSocket-соединения и отправляет обратно то, что получил:
use futures_util::{SinkExt, StreamExt};
use tokio::net::TcpListener;
use tokio_tungstenite::accept_async;
use tokio_tungstenite::tungstenite::Message;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let listener = TcpListener::bind("127.0.0.1:9001").await?;
while let Ok((stream, addr)) = listener.accept().await {
tokio::spawn(async move {
println!("Новое подключение: {}", addr);
let ws_stream = accept_async(stream).await.expect("Ошибка handshake");
let (mut write, mut read) = ws_stream.split();
while let Some(Ok(msg)) = read.next().await {
if msg.is_text() || msg.is_binary() {
// Эхо-ответ
write.send(msg).await.unwrap();
} else if msg.is_close() {
break;
}
}
});
}
Ok(())
}
Всё просто: accept_async делает handshake, после чего мы получаем объект, реализующий Stream и Sink. А дальше — чистая работа с async/await.
Плюсы WebSocket в Rust
- Настоящий full-duplex: сервер может инициировать отправку данных
- Низкий оверхед: после handshake нет HTTP-заголовков
- Эффективность: одно TCP-соединение живёт долго, не пересоздаётся
- Экосистема Rust: библиотеки вроде
tokio-tungsteniteиactix-webдают полный контроль
Минусы WebSocket
- Требуется поддержка на уровне прокси и балансировщиков (не все умеют проксировать WS)
- Сложнее в отладке — стандартные инструменты для HTTP не работают
- В некоторых корпоративных средах WebSocket блокируется на уровне файрвола
Long Polling vs WebSocket: когда что использовать?
Вот практическая таблица принятия решений для Rust-разработчика:
| Критерий | Long Polling | WebSocket |
|---|---|---|
| Количество одновременных клиентов | До ~10k (упирается в количество файловых дескрипторов) |
БлогиРекомендаций по книгамЧитайте также |