WebSocket на Rust: real-time приложения от А до Я

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

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

Начать курс

Введение в WebSocket на Rust: почему это выбор профессионалов

Современные веб-приложения требуют мгновенной передачи данных. Будь то онлайн-чат, биржевой терминал, многопользовательская игра или система мониторинга — протокол WebSocket стал стандартом для real-time коммуникации. Rust, с его акцентом на производительность и безопасность памяти, является идеальной платформой для создания таких систем.

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

1. Основы WebSocket в Rust: выбираем библиотеку

Экосистема Rust предлагает несколько зрелых решений для работы с WebSocket. Наиболее популярные:

  • tokio-tungstenite — асинхронный WebSocket на базе tokio, низкоуровневый и гибкий
  • warp — веб-фреймворк со встроенной поддержкой WebSocket
  • actix-web — мощный фреймворк с WebSocket-поддержкой

Для нашего руководства мы выберем tokio-tungstenite как наиболее универсальное решение.

Установка зависимостей

Создайте новый проект и добавьте в Cargo.toml:

[dependencies]
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = "0.21"
futures-util = "0.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Базовый 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() {
    let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
    println!("Сервер запущен на ws://127.0.0.1:8080");

    while let Ok((stream, addr)) = listener.accept().await {
        println!("Новое подключение: {}", addr);
        tokio::spawn(handle_connection(stream));
    }
}

async fn handle_connection(stream: tokio::net::TcpStream) {
    let ws_stream = accept_async(stream).await.unwrap();
    let (mut write, mut read) = ws_stream.split();

    while let Some(Ok(msg)) = read.next().await {
        if let Message::Text(text) = msg {
            println!("Получено: {}", text);
            let response = format!("Эхо: {}", text);
            write.send(Message::Text(response.into())).await.unwrap();
        }
    }
}

Этот код демонстрирует базовую структуру: асинхронный TCP-сервер, приём WebSocket-соединений и обработка сообщений в отдельных задачах.

2. Архитектура real-time чата на Rust

Теперь построим полноценный чат, где сообщения от одного клиента рассылаются всем подключённым пользователям. Для этого нам потребуется разделяемое состояние — список активных соединений.

Реализация с shared state

use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{Mutex, broadcast};

type Tx = broadcast::Sender<String>;
type Clients = Arc<Mutex<HashMap<String, Tx>>>;

#[tokio::main]
async fn main() {
    let clients: Clients = Arc::new(Mutex::new(HashMap::new()));
    let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();

    while let Ok((stream, addr)) = listener.accept().await {
        let clients = clients.clone();
        tokio::spawn(handle_client(stream, addr, clients));
    }
}

async fn handle_client(
    stream: tokio::net::TcpStream,
    addr: std::net::SocketAddr,
    clients: Clients
) {
    let ws_stream = accept_async(stream).await.unwrap();
    let (mut write, mut read) = ws_stream.split();

    // Создаём канал для рассылки
    let (tx, mut rx) = broadcast::channel::<String>(100);
    let client_id = addr.to_string();

    // Регистрируем клиента
    clients.lock().await.insert(client_id.clone(), tx);

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

    // Чтение сообщений от клиента
    while let Some(Ok(msg)) = read.next().await {
        if let Message::Text(text) = msg {
            let broadcast_msg = format!("{}: {}", client_id, text);
            let clients = clients.lock().await;
            for (_, tx) in clients.iter() {
                let _ = tx.send(broadcast_msg.clone());
            }
        }
    }

    // Удаляем клиента при отключении
    clients.lock().await.remove(&client_id);
    write_task.abort();
}

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

3. Продвинутые техники: авторизация и JSON-протокол

В реальных приложениях необходима аутентификация и структурированные данные. Рассмотрим, как добавить эти возможности.

WebSocket с авторизацией через URL

use url::Url;

async fn handle_connection(stream: tokio::net::TcpStream, request: http::Request<()>) {
    // Парсим URL запроса
    let uri = request.uri();
    let parsed_url = Url::parse(&format!("ws://localhost{}", uri)).unwrap();
    
    // Извлекаем токен из query-параметров
    let token = parsed_url.query_pairs()
        .find(|(key, _)| key == "token")
        .map(|(_, value)| value.to_string());

    match token {
        Some(t) if t == "secret123" => {
            // Авторизация успешна
            let ws_stream = accept_async(stream).await.unwrap();
            // ... обработка сообщений
        }
        _ => {
            // Отклоняем соединение
            println!("Неавторизованный доступ");
        }
    }
}

Работа с JSON-сообщениями

Используем serde для структурированных данных:

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
struct ChatMessage {
    msg_type: String,
    username: String,                

Блоги

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