Глава 23: Сетевое программирование в Rust

Содержание: Основы TCP/UDP: std::net HTTP-клиенты: reqwest HTTP-серверы: hyper HTTP-серверы: actix-web Обработка ошибок в сети Пример: Простой чат-сервер Упражнение: HTTP-клиент для API

Добро пожаловать в главу 23 курса по Rust, посвящённую сетевому программированию! В этой главу мы глубоко погрузимся в основы работы с сетью в Rust, начиная с низкоуровневых инструментов стандартной библиотеки std::net, переходя к высокоуровневым HTTP-клиентам и серверам с использованием библиотек reqwest и hyper, а также разберём обработку ошибок, создадим простой чат-сервер и выполним практическое упражнение по написанию HTTP-клиента для работы с API. Лекция будет самодостаточной, с избыточным покрытием всех тем, включая нюансы, подводные камни и лучшие практики, детализацию и практическую применимость.


1. Основы TCP/UDP: std::net

Сетевое программирование в Rust начинается с модуля std::net, который предоставляет низкоуровневые инструменты для работы с протоколами TCP и UDP. Эти инструменты блокируют операции ввода-вывода (synchronous I/O), что делает их простыми для изучения, но менее эффективными для высоконагруженных приложений по сравнению с асинхронными альтернативами (например, tokio). Мы начнём с основ, чтобы заложить фундамент.

1.1. TCP: 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(())
}

Объяснение:

Нюанс: Этот код блокирует выполнение на каждом соединении. Для параллельной обработки нужно использовать потоки (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(())
}

Объяснение:

Подводный камень: Потоки потребляют больше ресурсов, чем асинхронные задачи. Для сотен или тысяч соединений это решение неэффективно.

Пример с асинхронностью (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);
            }
        });
    }
}

Объяснение:

Преимущество: Асинхронность позволяет обрабатывать тысячи соединений с минимальными затратами ресурсов.

Клиент с 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(())
}

Подводные камни:

1.2. UDP: 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(())
}

Особенности:

Практический совет: Используйте UDP для задач, где важна скорость, а потеря данных допустима (например, потоковое видео или игры).


2. HTTP-клиенты: 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"] }

Простой GET-запрос

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(())
}

Объяснение:

Работа с JSON

Предположим, мы хотим получить данные в структурированном виде:

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.


3. HTTP-серверы: 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);
    }
}

Объяснение:

Практический совет: Для реальных приложений используйте маршрутизацию (например, с tower) или фреймворки вроде axum, построенные на hyper.


3.1. HTTP-серверы: 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
}

Объяснение:

Тест:

Работа с JSON

Добавим маршрут для обработки 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
}

Объяснение:

Тест: Отправьте POST-запрос на /user с телом {"name": "Alice", "age": 25} — сервер вернёт {"name": "Hello, Alice!", "age": 25}.

Нюансы и лучшие практики

use actix_web::middleware::Logger;

App::new()
    .wrap(Logger::default())
    .route("/", web::get().to(greet))

Практический совет: Для простых API actix-web — отличный выбор благодаря читаемому синтаксису и встроенным инструментам. Для сложных проектов изучите интеграцию с базами данных (например, sqlx) и WebSocket.


4. Обработка ошибок в сети

Сетевые операции подвержены сбоям: соединение может оборваться, сервер не ответить, данные прийти в неверном формате. В 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 для удобной работы с ошибками в крупных проектах.


5. Пример: Простой чат-сервер

Создадим чат-сервер с использованием 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(); // Отправляем клиенту
                        }
                    }
                }
            }
        });
    }
}

Объяснение:

Преимущество: Этот сервер масштабируем, так как использует асинхронность вместо потоков. Для тестирования подключитесь несколькими клиентами через telnet 127.0.0.1 8080.


6. Упражнение: HTTP-клиент для API

Напишите клиент, который запрашивает погоду с 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. Экспериментируйте, изучайте документацию и переходите к асинхронным библиотекам для продвинутых задач!