std::net
HTTP-клиенты: reqwest
HTTP-серверы: hyper
HTTP-серверы: actix-web
Обработка ошибок в сети
Пример: Простой чат-сервер
Упражнение: HTTP-клиент для API
Добро пожаловать в главу 23 курса по Rust, посвящённую сетевому программированию! В этой главу мы глубоко погрузимся в основы работы с сетью в Rust, начиная с низкоуровневых инструментов стандартной библиотеки std::net
, переходя к высокоуровневым HTTP-клиентам и серверам с использованием библиотек reqwest
и hyper
, а также разберём обработку ошибок, создадим простой чат-сервер и выполним практическое упражнение по написанию HTTP-клиента для работы с API. Лекция будет самодостаточной, с избыточным покрытием всех тем, включая нюансы, подводные камни и лучшие практики, детализацию и практическую применимость.
std::net
Сетевое программирование в Rust начинается с модуля std::net
, который предоставляет низкоуровневые инструменты для работы с протоколами TCP и UDP. Эти инструменты блокируют операции ввода-вывода (synchronous I/O), что делает их простыми для изучения, но менее эффективными для высоконагруженных приложений по сравнению с асинхронными альтернативами (например, tokio
). Мы начнём с основ, чтобы заложить фундамент.
TcpListener
и TcpStream
TCP (Transmission Control Protocol) — это надёжный, ориентированный на соединение протокол. В Rust для создания серверов используется TcpListener
, а для взаимодействия с клиентами — TcpStream
.
TcpListener
TcpListener
позволяет привязаться к определённому адресу и порту, чтобы принимать входящие соединения.
use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};
fn main() -> std::io::Result<()> {
// Привязываем сервер к адресу 127.0.0.1:8080
let listener = TcpListener::bind("127.0.0.1:8080")?;
println!("Сервер запущен на 127.0.0.1:8080");
// Бесконечный цикл для обработки входящих соединений
for stream in listener.incoming() {
match stream {
Ok(mut stream) => {
println!("Новое соединение: {}", stream.peer_addr()?);
// Читаем данные от клиента
let mut buffer = [0; 1024];
stream.read(&mut buffer)?;
println!("Получено: {}", String::from_utf8_lossy(&buffer[..]));
// Отправляем ответ
stream.write_all(b"Hello from server!")?;
}
Err(e) => {
eprintln!("Ошибка соединения: {}", e);
}
}
}
Ok(())
}
Объяснение:
TcpListener::bind
создаёт сервер и возвращает Result
. Если порт занят или адрес недоступен, вернётся ошибка.incoming()
возвращает итератор по входящим соединениям. Каждое соединение — это Result<TcpStream>
.TcpStream
реализует трейты Read
и Write
, что позволяет читать и записывать данные.peer_addr()
возвращает адрес клиента.Нюанс: Этот код блокирует выполнение на каждом соединении. Для параллельной обработки нужно использовать потоки (std::thread
) или асинхронность (о чём позже).
std::thread
)Чтобы обрабатывать несколько клиентов параллельно, можно передать каждое соединение в отдельный поток:
use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};
use std::thread;
fn handle_client(mut stream: TcpStream) -> std::io::Result<()> {
println!("Новое соединение: {}", stream.peer_addr()?);
let mut buffer = [0; 1024];
stream.read(&mut buffer)?;
println!("Получено: {}", String::from_utf8_lossy(&buffer[..]));
stream.write_all(b"Hello from server!")?;
Ok(())
}
fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:8080")?;
println!("Сервер запущен на 127.0.0.1:8080");
for stream in listener.incoming() {
match stream {
Ok(stream) => {
thread::spawn(|| {
if let Err(e) = handle_client(stream) {
eprintln!("Ошибка в потоке: {}", e);
}
});
}
Err(e) => {
eprintln!("Ошибка соединения: {}", e);
}
}
}
Ok(())
}
Объяснение:
thread::spawn
создаёт новый поток для каждого клиента.Подводный камень: Потоки потребляют больше ресурсов, чем асинхронные задачи. Для сотен или тысяч соединений это решение неэффективно.
tokio
)Для масштабируемости используем асинхронную библиотеку tokio
. Добавьте в Cargo.toml
:
[dependencies]
tokio = { version = "1.0", features = ["full"] }
use tokio::net::{TcpListener, TcpStream};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
async fn handle_client(mut stream: TcpStream) -> std::io::Result<()> {
println!("Новое соединение: {}", stream.peer_addr()?);
let mut buffer = [0; 1024];
stream.read(&mut buffer).await?;
println!("Получено: {}", String::from_utf8_lossy(&buffer[..]));
stream.write_all(b"Hello from server!").await?;
Ok(())
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
println!("Сервер запущен на 127.0.0.1:8080");
loop {
let (stream, _) = listener.accept().await?;
tokio::spawn(async move {
if let Err(e) = handle_client(stream).await {
eprintln!("Ошибка: {}", e);
}
});
}
}
Объяснение:
tokio::net::TcpListener
и TcpStream
— асинхронные аналоги из std::net
..await
используется для асинхронных операций.tokio::spawn
создаёт легковесные задачи вместо потоков.Преимущество: Асинхронность позволяет обрабатывать тысячи соединений с минимальными затратами ресурсов.
TcpStream
Теперь напишем клиент, который подключается к серверу:
use std::net::TcpStream;
use std::io::{Read, Write};
fn main() -> std::io::Result<()> {
let mut stream = TcpStream::connect("127.0.0.1:8080")?;
println!("Подключено к серверу!");
// Отправляем сообщение
stream.write_all(b"Hello from client!")?;
// Читаем ответ
let mut buffer = [0; 1024];
stream.read(&mut buffer)?;
println!("Ответ сервера: {}", String::from_utf8_lossy(&buffer[..]));
Ok(())
}
Подводные камни:
connect
вернёт ошибку ConnectionRefused
.UdpSocket
UDP (User Datagram Protocol) — это ненадёжный, неориентированный на соединение протокол. Он быстрее TCP, но не гарантирует доставку данных.
use std::net::UdpSocket;
fn main() -> std::io::Result<()> {
// Создаём UDP-сокет и привязываем к адресу
let socket = UdpSocket::bind("127.0.0.1:8080")?;
println!("UDP-сервер запущен на 127.0.0.1:8080");
let mut buffer = [0; 1024];
let (bytes, sender) = socket.recv_from(&mut buffer)?;
println!("Получено {} байт от {}: {}", bytes, sender, String::from_utf8_lossy(&buffer[..bytes]));
// Отправляем ответ
socket.send_to(b"Hello from UDP server!", sender)?;
Ok(())
}
Особенности:
recv_from
возвращает кортеж: количество принятых байтов и адрес отправителя.Практический совет: Используйте UDP для задач, где важна скорость, а потеря данных допустима (например, потоковое видео или игры).
reqwest
Для высокоуровневой работы с HTTP в Rust популярна библиотека reqwest
. Она проста в использовании, поддерживает как синхронный, так и асинхронный код, и идеально подходит для запросов к API.
Добавьте в Cargo.toml
:
[dependencies]
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.0", features = ["full"] }
use reqwest;
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let response = reqwest::get("https://api.github.com/users/rust-lang")
.await?
.text()
.await?;
println!("Ответ: {}", response);
Ok(())
}
Объяснение:
reqwest::get
возвращает Future
, поэтому нужен асинхронный runtime (здесь tokio
).text()
извлекает тело ответа как строку.Result
.Предположим, мы хотим получить данные в структурированном виде:
use serde::{Deserialize, Serialize};
use reqwest;
#[derive(Debug, Serialize, Deserialize)]
struct User {
login: String,
id: u32,
public_repos: u32,
}
#[tokio::main]
async fn main() -> Result<(), reqwest::Error> {
let user: User = reqwest::get("https://api.github.com/users/rust-lang")
.await?
.json()
.await?;
println!("Пользователь: {:?}", user);
Ok(())
}
Нюанс: Для десериализации JSON нужен serde
с трейтом Deserialize
. Убедитесь, что структура соответствует API.
Подводный камень: Если API возвращает неожиданный формат, json()
завершится ошибкой. Всегда проверяйте документацию API.
hyper
hyper
— это низкоуровневая библиотека для создания HTTP-серверов и клиентов. Она мощная, но требует больше кода, чем фреймворки вроде actix-web
.
[dependencies]
hyper = { version = "0.14", features = ["server", "http1"] }
tokio = { version = "1.0", features = ["full"] }
use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};
use std::convert::Infallible;
async fn handle(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
Ok(Response::new(Body::from("Hello, Hyper!")))
}
#[tokio::main]
async fn main() {
let addr = ([127, 0, 0, 1], 8080).into();
let make_svc = make_service_fn(|_conn| async {
Ok::<_, Infallible>(service_fn(handle))
});
let server = Server::bind(&addr).serve(make_svc);
println!("Сервер запущен на http://{}", addr);
if let Err(e) = server.await {
eprintln!("Ошибка сервера: {}", e);
}
}
Объяснение:
handle
— обработчик запросов, возвращает Response
.make_service_fn
создаёт сервис для каждого подключения.service_fn
оборачивает функцию handle
в сервис.Практический совет: Для реальных приложений используйте маршрутизацию (например, с tower
) или фреймворки вроде axum
, построенные на hyper
.
actix-web
actix-web
— это мощный и высокопроизводительный веб-фреймворк, построенный поверх actix
(акторная система) и использующий асинхронность для обработки запросов. Он проще в использовании, чем hyper
, благодаря встроенной маршрутизации, поддержке middleware и удобным абстракциям, что делает его популярным выбором для создания REST API и веб-приложений.
[dependencies]
actix-web = "4"
serde = { version = "1.0", features = ["derive"] }
actix-web
Создадим сервер с несколькими маршрутами:
use actix_web::{web, App, HttpServer, HttpResponse, Responder};
async fn greet() -> impl Responder {
HttpResponse::Ok().body("Hello, Actix!")
}
async fn echo(req_body: String) -> impl Responder {
HttpResponse::Ok().body(req_body)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/", web::get().to(greet))
.route("/echo", web::post().to(echo))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Объяснение:
HttpServer::new
создаёт сервер. Замыкание внутри определяет конфигурацию приложения.App::new()
создаёт экземпляр приложения, где задаются маршруты.route
принимает путь, HTTP-метод (например, web::get()
, web::post()
) и обработчик.#[actix_web::main]
заменяет необходимость явно использовать tokio::main
, автоматически обеспечивая асинхронный runtime.Тест:
http://127.0.0.1:8080/
вернёт "Hello, Actix!".http://127.0.0.1:8080/echo
с телом вернёт это же тело как эхо.Добавим маршрут для обработки JSON:
use actix_web::{web, App, HttpServer, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct User {
name: String,
age: u32,
}
async fn create_user(user: web::Json<User>) -> impl Responder {
HttpResponse::Ok().json(User {
name: format!("Hello, {}!", user.name),
age: user.age,
})
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/user", web::post().to(create_user))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Объяснение:
web::Json<T>
автоматически десериализует тело запроса в структуру User
..json()
.Тест: Отправьте POST-запрос на /user
с телом {"name": "Alice", "age": 25}
— сервер вернёт {"name": "Hello, Alice!", "age": 25}
.
actix-web
использует многопоточность по умолчанию. Настройте количество воркеров с помощью .workers(n)
для оптимизации под нагрузку.actix_web::error::Error
или кастомные типы ошибок с thiserror
для более информативных ответов.App::wrap
. Пример:use actix_web::middleware::Logger;
App::new()
.wrap(Logger::default())
.route("/", web::get().to(greet))
Result
для кастомных ответов.Практический совет: Для простых API actix-web
— отличный выбор благодаря читаемому синтаксису и встроенным инструментам. Для сложных проектов изучите интеграцию с базами данных (например, sqlx
) и WebSocket.
Сетевые операции подвержены сбоям: соединение может оборваться, сервер не ответить, данные прийти в неверном формате. В Rust ошибки обрабатываются через Result
и Option
.
use std::net::TcpStream;
use std::io::{Read, Write};
fn connect_to_server() -> Result<(), Box<dyn std::error::Error>> {
let mut stream = TcpStream::connect("127.0.0.1:8080")
.map_err(|e| format!("Не удалось подключиться: {}", e))?;
stream.write_all(b"Test")?;
let mut buffer = [0; 1024];
stream.read(&mut buffer)?;
println!("Получено: {}", String::from_utf8_lossy(&buffer));
Ok(())
}
fn main() {
match connect_to_server() {
Ok(()) => println!("Успешно завершено"),
Err(e) => eprintln!("Ошибка: {}", e),
}
}
Лучшая практика: Используйте thiserror
или anyhow
для удобной работы с ошибками в крупных проектах.
Создадим чат-сервер с использованием TcpListener
и потоков:
use std::net::{TcpListener, TcpStream};
use std::io::{Read, Write};
use std::thread;
use std::sync::mpsc::{channel, Sender};
fn handle_client(mut stream: TcpStream, tx: Sender<String>) {
let mut buffer = [0; 1024];
while match stream.read(&mut buffer) {
Ok(n) if n > 0 => {
let msg = String::from_utf8_lossy(&buffer[..n]).to_string();
tx.send(msg).unwrap();
true
}
_ => false,
} {}
}
fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:8080")?;
let (tx, rx) = channel::<String>();
// Поток для广播 сообщений
let tx_clone = tx.clone();
thread::spawn(move || {
for msg in rx {
println!("Сообщение: {}", msg);
}
});
for stream in listener.incoming() {
let stream = stream?;
let tx = tx_clone.clone();
thread::spawn(move || handle_client(stream, tx));
}
Ok(())
}
Нюанс: Это простой пример. Для масштабируемости используйте tokio
или async-std
.
tokio
для масштабируемостиПерепишем чат-сервер с использованием tokio
для асинхронной обработки и вещания сообщений через tokio::sync::broadcast
:
use tokio::net::{TcpListener, TcpStream};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::broadcast;
#[tokio::main]
async fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
println!("Чат-сервер запущен на 127.0.0.1:8080");
let (tx, _rx) = broadcast::channel(100); // Канал на 100 сообщений
loop {
let (mut stream, addr) = listener.accept().await?;
let tx = tx.clone();
let mut rx = tx.subscribe();
tokio::spawn(async move {
let mut buffer = [0; 1024];
loop {
tokio::select! {
result = stream.read(&mut buffer) => {
match result {
Ok(n) if n > 0 => {
let msg = String::from_utf8_lossy(&buffer[..n]).to_string();
println!("{}: {}", addr, msg);
tx.send(msg).unwrap(); // Отправляем всем
}
_ => break, // Клиент отключился
}
}
result = rx.recv() => {
if let Ok(msg) = result {
stream.write_all(msg.as_bytes()).await.unwrap(); // Отправляем клиенту
}
}
}
}
});
}
}
Объяснение:
broadcast::channel
создаёт канал для вещания сообщений всем клиентам.tokio::select!
позволяет одновременно читать от клиента и отправлять ему сообщения от других.tokio::spawn
).Преимущество: Этот сервер масштабируем, так как использует асинхронность вместо потоков. Для тестирования подключитесь несколькими клиентами через telnet 127.0.0.1 8080
.
Напишите клиент, который запрашивает погоду с OpenWeatherMap API.
use reqwest;
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct Weather {
main: Main,
weather: Vec<WeatherDesc>,
}
#[derive(Debug, Deserialize)]
struct Main {
temp: f32,
}
#[derive(Debug, Deserialize)]
struct WeatherDesc {
description: String,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key = "ВАШ_API_КЛЮЧ"; // Замените на свой ключ
let city = "Moscow";
let url = format!(
"http://api.openweathermap.org/data/2.5/weather?q={}&appid={}&units=metric",
city, api_key
);
let weather: Weather = reqwest::get(&url).await?.json().await?;
println!(
"Погода в {}: {}°C, {}",
city, weather.main.temp, weather.weather[0].description
);
Ok(())
}
Задание: Добавьте обработку ошибок и запрос для нескольких городов.
Эта лекция охватывает всё, что нужно для уверенного старта в сетевом программировании на Rust. Экспериментируйте, изучайте документацию и переходите к асинхронным библиотекам для продвинутых задач!