WebSockets в Rust: создаём real-time приложения с нуля

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

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

Начать курс

Почему Rust и WebSockets — идеальный дуэт для real-time?

Представьте: вам нужно создать чат, который выдерживает 100 000 одновременных подключений, или биржевой терминал, где каждая миллисекунда задержки стоит денег. Вы тянетесь к Python? Node.js? А что, если я скажу, что есть язык, который даёт производительность C++ с безопасностью памяти, о которой мечтает каждый разработчик? Да, это Rust.

WebSockets в Rust — это не просто модная связка. Это возможность создавать real-time приложения, которые работают на грани физических ограничений железа. Rust с его нулевой стоимостью абстракций и асинхронным рантаймом (tokio, async-std) превращает написание вебсокет-серверов в элегантный и безопасный процесс.

В этой статье мы не будем просто читать документацию. Мы соберём полноценный real-time чат, разберёмся с библиотеками tokio-tungstenite и actix-web, и я покажу, как Rust решает проблемы, с которыми вы сталкивались в других языках. Поехали!

Что такое WebSockets и почему Rust?

Краткий ликбез по протоколу

WebSocket — это протокол полнодуплексной связи поверх TCP. В отличие от HTTP, где клиент всегда инициирует запрос, WebSocket позволяет серверу отправлять данные клиенту в любой момент. Это идеально для:

  • Чатов и мессенджеров
  • Торговых терминалов и биржевых данных
  • Онлайн-игр
  • Мониторинга и логов в реальном времени
  • Совместного редактирования документов

Почему именно Rust?

Давайте честно: написать вебсокет-сервер на JavaScript легко, но он будет жрать память как не в себя. Python? GIL не даст вам масштабироваться. Rust же предлагает:

  • Отсутствие GC — предсказуемая производительность без «фризов»
  • Безопасная работа с памятью — компилятор отлавливает гонки данных на этапе сборки
  • Асинхронность без оверхеда — tokio позволяет обрабатывать миллионы соединений на одном потоке
  • Zero-cost abstractions — высокоуровневый код компилируется в эффективный машинный

Когда дело доходит до высоконагруженных систем, Rust — это выбор профессионалов, которые не любят компромиссы.

Инструментарий: выбираем библиотеки

В экосистеме Rust есть несколько путей для работы с WebSockets. Вот основные:

Библиотека Когда использовать
tokio-tungstenite Чистый WebSocket, низкоуровневый контроль
actix-web + actix-ws Полноценный HTTP + WS сервер
warp Функциональный, композитный подход
axum Современный, от создателей tokio

Для нашего проекта мы возьмём tokio-tungstenite — он лёгкий, быстрый и даёт полный контроль. Если вам нужен HTTP+WS в одном флаконе, смотрите в сторону axum или actix-web.

Пишем real-time чат на Rust

Шаг 1: Настройка проекта

Создаём новый проект и добавляем зависимости:

cargo new rust-websocket-chat
cd rust-websocket-chat

Редактируем Cargo.toml:

[dependencies]
tokio = { version = "1.35", features = ["full"] }
tokio-tungstenite = "0.21"
futures-util = "0.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "1.6", features = ["v4"] }
log = "0.4"
env_logger = "0.10"

Шаг 2: Архитектура приложения

Наш чат будет состоять из трёх компонентов:

  • Сервер — принимает WebSocket-соединения
  • Хаб (Hub) — управляет подключёнными клиентами
  • Обработчик сообщений — парсит JSON и рассылает сообщения

Ключевая идея: использовать tokio::sync::broadcast для рассылки сообщений всем клиентам. Это эффективнее, чем хранить список сокетов и итерироваться по нему.

Шаг 3: Пишем код сервера

use futures_util::{SinkExt, StreamExt};
use tokio::net::{TcpListener, TcpStream};
use tokio_tungstenite::{accept_async, tungstenite::Message};
use std::sync::Arc;
use tokio::sync::broadcast;

#[derive(Clone, serde::Serialize, serde::Deserialize)]
struct ChatMessage {
    username: String,
    text: String,
    timestamp: u64,
}

#[tokio::main]
async fn main() {
    env_logger::init();
    let addr = "127.0.0.1:8080";
    let listener = TcpListener::bind(addr).await.expect("Не удалось запустить сервер");
    log::info!("Сервер запущен на {}", addr);

    // Создаём канал для рассылки сообщений
    let (tx, _rx) = broadcast::channel::<String>(100);

    loop {
        let (stream, peer) = listener.accept().await.unwrap();
        let tx = tx.clone();
        tokio::spawn(handle_connection(stream, peer, tx));
    }
}

async fn handle_connection(
    stream: TcpStream,
    peer: std::net::SocketAddr,
    tx: broadcast::Sender<String>,
) {
    log::info!("Новое подключение: {}", peer);

    let ws_stream = match accept_async(stream).await {
        Ok(ws) => ws,
        Err(e) => {
            log::error!("Ошибка handshake: {}", e);
            return;
        }
    };

    let (mut write, mut read) = ws_stream.split();
    let mut rx = tx.subscribe();

    // Задача на отправку сообщений клиенту
    let send_task = tokio::spawn(async move {
        while let Ok(msg) = rx.recv().await {
            if write.send(Message::Text(msg)).await.is_err() {
                break;
            }
        }
    });

    // Задача на получение сообщений от клиента
    let recv_task = tokio::spawn(async move {
        while let Some(Ok(msg)) = read.next().await {
            match msg {
                Message::Text(text) => {
                    // Парсим и пересылаем всем
                    if let Ok(chat_msg) = serde_json::from_str::<ChatMessage>(&text) {
                        let broadcast_msg = serde_json::to_string(&chat_msg).unwrap();
                        let _ = tx.send(broadcast_msg);
                    }
                }
                Message::Close(_) => break,
                _ => {}
            }
        }
    });

    // Ждём завершения любой из задач
    tokio::select! {
        _ = send_task => {},
        _ = recv_task => {},
    }

    log::                

Блоги

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