Введение в WebSocket на Rust
Real-time приложения стали неотъемлемой частью современного веба. Чат-системы, онлайн-игры, биржевые котировки и уведомления в реальном времени — всё это требует постоянного двустороннего обмена данными между клиентом и сервером. Протокол WebSocket предоставляет именно такую возможность, а Rust благодаря своей производительности и безопасности является идеальным языком для создания таких систем.
В этой статье мы подробно разберём, как создавать real-time приложения на Rust с использованием WebSocket. Вы узнаете о популярных крейтах, архитектурных подходах и получите готовые примеры кода для быстрого старта.
Основы WebSocket и выбор библиотек
WebSocket — это протокол, обеспечивающий полнодуплексную связь через одно TCP-соединение. В отличие от HTTP, где клиент инициирует запрос, а сервер отвечает, WebSocket позволяет обеим сторонам отправлять данные в любой момент.
Популярные крейты для WebSocket в Rust
- tokio-tungstenite — асинхронный WebSocket-клиент и сервер на базе Tokio. Лёгкий и быстрый.
- actix-web — мощный веб-фреймворк со встроенной поддержкой WebSocket.
- tungstenite — синхронная версия, удобная для простых сценариев.
- warp — ещё один асинхронный фреймворк с поддержкой WebSocket.
Для большинства проектов мы рекомендуем tokio-tungstenite или actix-web. Первый — для лёгких серверов и клиентов, второй — для полноценных веб-приложений.
Создание WebSocket-сервера на Rust
Рассмотрим создание простого echo-сервера, который принимает сообщения и отправляет их обратно клиенту. Используем tokio-tungstenite.
Настройка проекта
Создайте новый проект Cargo и добавьте зависимости в Cargo.toml:
[dependencies]
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = "0.21"
futures-util = "0.3"
Базовый echo-сервер
use futures_util::{SinkExt, StreamExt};
use tokio::net::TcpListener;
use tokio_tungstenite::accept_async;
#[tokio::main]
async fn main() -> Result<(), Box> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
println!("Сервер запущен на ws://127.0.0.1:8080");
while let Ok((stream, addr)) = listener.accept().await {
println!("Новое подключение: {}", addr);
tokio::spawn(handle_connection(stream));
}
Ok(())
}
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(msg) = read.next().await {
let msg = msg.unwrap();
// Отправляем сообщение обратно
write.send(msg).await.unwrap();
}
}
Этот код создаёт TCP-сервер на порту 8080, принимает WebSocket-соединения и для каждого клиента запускает асинхронную задачу, которая эхо-повторяет все входящие сообщения.
Создание WebSocket-клиента на Rust
Теперь напишем клиент, который подключается к нашему серверу, отправляет сообщение и получает ответ.
use futures_util::StreamExt;
use tokio_tungstenite::connect_async;
#[tokio::main]
async fn main() -> Result<(), Box> {
let url = "ws://127.0.0.1:8080";
let (ws_stream, _) = connect_async(url).await?;
println!("Подключено к серверу");
let (mut write, mut read) = ws_stream.split();
// Отправляем сообщение
write.send("Привет, WebSocket!".into()).await?;
// Получаем ответ
if let Some(msg) = read.next().await {
let msg = msg?;
println!("Получено: {}", msg.to_text().unwrap());
}
Ok(())
}
Real-time чат с использованием actix-web
Теперь создадим полноценный многопользовательский чат с помощью actix-web. Это более сложный пример, демонстрирующий broadcast-рассылку сообщений всем подключённым клиентам.
Добавление зависимостей
[dependencies]
actix-web = "4"
actix-ws = "0.2"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Реализация чата
use actix_web::{web, App, HttpServer, HttpRequest, HttpResponse};
use actix_ws::Message;
use serde::{Deserialize, Serialize};
use tokio::sync::broadcast;
#[derive(Serialize, Deserialize, Clone)]
struct ChatMessage {
user: String,
text: String,
}
async fn chat_route(
req: HttpRequest,
stream: web::Payload,
broadcaster: web::Data>,
) -> Result {
let (response, mut session, mut msg_stream) = actix_ws::handle(&req, stream)?;
let mut rx = broadcaster.subscribe();
actix_web::rt::spawn(async move {
loop {
tokio::select! {
Some(msg) = msg_stream.next() => {
match msg {
Ok(Message::Text(text)) => {
let chat_msg = ChatMessage {
user: "User".to_string(),
text: text.to_string(),
};
let _ = broadcaster.send(chat_msg);
}
Ok(Message::Ping(bytes)) => {
let _ = session.pong(&bytes).await;
}
_ => break,
}
}
Ok(msg) = rx.recv() => {
let json = serde_json::to_string(&msg).unwrap();
let _ = session.text(json).await;
}
}
}
});
Ok(response)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let (tx, _) = broadcast::channel::(100);
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(tx.clone()))
.route("/chat", web::get().to(chat_route))
})
.bind("127.0.0.1:8081")?
.run()
.await
}
В этом примере мы используем broadcast::channel для рассылки сообщений всем подключённым клиентам. Каждое новое сообщение получает каждый подписчик канала.