Long Polling против WebSocket в Rust: битва за реальное время

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему 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 (упирается в количество файловых дескрипторов)

Блоги

Рекомендаций по книгам