Перейти к содержимому
docs.
WORKSPACE

Документации проектов

Открыть каталог проектов
Localzet ServerАсинхронный PHP-серверTriangleФреймворк и компонентыMIX IDИдентификация и интеграцииHack APIПубличные API хакатонаACNПроект и компонентыUserlay / UserNetПроект и компонентыAxiomПроект и компонентыМесседжинг через почтуПроект и компонентыLWT / JWT / JOSEПроект и компонентыФинансовая аналитика KimaiПроект и компонентыМультипарадигмальная БДПроект и компонентыZorin PlatformПроект и компонентыZorin TrustПроект и компонентыFloraScope / VegwatchПроект и компонентыaegis-qДокументация проектаaevumdbДокументация проектаauditДокументация проектаauto-invest-managerАвтотрейдерcaine-companionДокументация проектаcertbot-cloudflareДокументация проектаClusterLocalzet Server / ClustercmdveilДокументация проектаConsolePHP CLICronБиблиотеки Localzet Serverdata-connectorДокументация проектаdevice-controlDevice Controlenn-projectДокументация проектаEventsБиблиотеки Localzet ServerflowsintДокументация проектаforgeДокументация проектаHttpClientБиблиотеки Localzet ServerinterpreterСобственный языкinvariaДокументация проектаkeynotesДокументация проектаknotrouteKnotroutelaravel-personal-analyticsДокументация проектаlocalzet-osДокументация проектаmeridianДокументация проектаMQTTБиблиотеки Localzet Servernano-kernelСобственная ОСNoterДокументация проектаpersonal-analyticsДокументация проектаProxyБиблиотеки Localzet ServerPSRБиблиотеки Localzet ServerPSR-7Библиотеки Localzet Serverrschs-monitorДокументация проектаsemsealДокументация проектаshardsealДокументация проектаSocketIOБиблиотеки Localzet ServerSOCKS5Библиотеки Localzet Serverstandalone-analyticsДокументация проектаstatic-php-cliДокументация проектаStorageБиблиотеки Localzet ServersutureДокументация проектаtempoweaveДокументация проектаttsСинтез речиTunnelБиблиотеки Localzet Serverui-kitUI KitvesperДокументация проектаWebAnalyzerWebAnalyzerzorin-access-brokerДокументация проектаzorin-authority-brokerДокументация проектаzorin-authority-sdk-phpДокументация проектаzorin-authority-sdk-pythonДокументация проектаzorin-authority-sdk-tsДокументация проектаzorin-client-intelДокументация проектаzorin-device-core-aospДокументация проектаzorin-host-agentДокументация проектаzorin-native-labДокументация проектаzorin-nodeДокументация проектаzorin-opsДокументация проектаdonstu-python-sdkДокументация проектаfloortraceFloortraceserver-rsLocalzet Server / Cluster✓
Каталог

Rust для разработчика на PHP, Python и TypeScript

Этот текст — не справочник по каждому оператору. Его цель — перестроить модель мышления разработчика с опытом PHP, Python и TypeScript и показать Rust на примере сетевого сервера.

PHP, Python и обычный TypeScript-код живут в среде выполнения:

  • PHP управляет значениями, ссылками и памятью внутри процесса интерпретатора;
  • Python использует подсчёт ссылок и сборщик циклического мусора;
  • TypeScript после проверки типов превращается в JavaScript и подчиняется модели выполнения JS;
  • ошибки владения объектом или гонки часто обнаруживаются только во время работы.

Rust компилируется в нативный код и не использует сборщик мусора. Компилятор должен заранее доказать, что:

  • ссылка не переживёт значение, на которое указывает;
  • значение не будет использовано после освобождения;
  • изменяемый доступ не пересечётся с другим доступом опасным образом;
  • данные между потоками передаются безопасно;
  • все варианты enum обработаны.

Поэтому Rust часто ощущается не как «ещё один язык с фигурными скобками», а как статический анализатор архитектуры, который заодно генерирует машинный код.

Ключевой обмен:

PHP, Python, TypeScriptRust
Быстро написать первую версиюНужно раньше определить владельцев данных
Многие ошибки проявляются в runtimeМногие классы ошибок запрещены компилятором
Память освобождает runtimeПамять освобождается детерминированно по правилам владения
null, исключения и динамические проверкиOption, Result, enum и исчерпывающий match
Классы и наследованиеСтруктуры, traits и композиция
Объекты обычно передаются как ссылкиПередача может переместить, скопировать или одолжить значение

Основные единицы Rust:

  • package — пакет с Cargo.toml;
  • crate — единица компиляции: библиотека или бинарная программа;
  • module — пространство имён внутри crate;
  • item — функция, структура, trait, константа и другая декларация.

В этом проекте:

Cargo.toml
src/
lib.rs библиотечный crate server_rs
main.rs бинарный crate server-rs
server.rs модуль server
server_abstract.rs модуль server_abstract
connection/
mod.rs корень модуля connection
base.rs
tcp_connection.rs
tests/
tcp_server.rs интеграционные тесты

src/lib.rs объявляет внутренние модули и формирует публичный API:

pub mod connection;
mod server;
pub use server::{Server, ServerConfig};

mod server подключает модуль, а pub use реэкспортирует выбранные типы. Благодаря этому пользователь пишет server_rs::Server, а не знает внутренний путь server_rs::server::Server.

В отличие от PSR-4, имя файла не обязано повторять имя типа. Принятый стиль:

  • типы: UpperCamelCase;
  • функции, методы, переменные и файлы: snake_case;
  • константы: SCREAMING_SNAKE_CASE.

В Rust значения неизменяемы по умолчанию:

let address = "127.0.0.1:8080";
let mut connections = 0;
connections += 1;

mut — часть контракта. Она сообщает читателю и компилятору, что значение действительно меняется.

Это отличается от const:

const DEFAULT_BUFFER_SIZE: usize = 87_380;
let mut buffer_size = DEFAULT_BUFFER_SIZE;

const вычисляется в допустимом compile-time контексте и не имеет собственного изменяемого места хранения. let создаёт локальную привязку.

Полезна возможность затенения:

let address = "127.0.0.1:8080";
let address = address.parse::<std::net::SocketAddr>()?;

Это новое значение с новым типом, а не изменение старой переменной.

У каждого значения есть владелец. Когда владелец покидает область видимости, вызывается drop, и ресурсы освобождаются.

fn consume(value: String) {
println!("{value}");
}
let name = String::from("echo");
consume(name);
// println!("{name}"); // value was moved

String владеет памятью в heap. Передача в consume перемещает владение. Старую переменную использовать нельзя.

В PHP или Python две переменные обычно могут указывать на один объект, а runtime следит за его жизнью. В Rust совместное владение выражается явно.

Простые типы часто реализуют Copy:

let first = 10_u64;
let second = first;
println!("{first} {second}");

Для String, Vec<T>, файлов и сокетов побитовое неявное копирование было бы опасно, поэтому они перемещаются.

Если действительно нужна независимая копия:

let first = String::from("echo");
let second = first.clone();

clone() следует видеть как потенциально дорогую операцию. Не нужно добавлять его автоматически только ради победы над borrow checker.

Функция может временно одолжить значение:

fn print_name(name: &str) {
println!("{name}");
}
let name = String::from("echo");
print_name(&name);
println!("{name}");

&T — неизменяемая ссылка, &mut T — изменяемая.

Основное правило в одной области времени:

  • либо сколько угодно &T;
  • либо ровно одна &mut T;
  • эти режимы не пересекаются.
let mut data = vec![1, 2, 3];
let first = &data[0];
println!("{first}");
data.push(4);

Этот код допустим, потому что последнее использование first произошло до push. Если использовать first после push, компилятор запретит код: push мог перераспределить память, сделав старую ссылку недействительной.

4.3 Владение ресурсом вместо ручной очистки

Заголовок раздела «4.3 Владение ресурсом вместо ручной очистки»

TcpConnection владеет TcpStream. При уничтожении соединения срабатывает Drop:

impl Drop for TcpConnection {
fn drop(&mut self) {
let _ = self.close();
self.statistics.connection_closed();
}
}

Это RAII: ресурс связан со временем жизни значения. Тот же подход используется для файлов, mutex guard, транзакций и временных каталогов.

В PHP аналогом по намерению может быть finally, но Rust делает очистку структурной и автоматической, включая ранний выход через ?.

Lifetime — не таймер и не время в секундах. Это область, в которой ссылка гарантированно действительна.

Большинство lifetime компилятор выводит:

fn first(values: &[String]) -> Option<&String> {
values.first()
}

Здесь возвращаемая ссылка связана с входным slice. Она не может пережить values.

Явная запись нужна при неоднозначности:

fn choose<'a>(left: &'a str, right: &'a str, use_left: bool) -> &'a str {
if use_left { left } else { right }
}

'a не продлевает жизнь данных. Она описывает связь: результат действителен не дольше обоих возможных источников.

Типичная ошибка:

fn invalid() -> &str {
let value = String::from("temporary");
&value
}

После выхода из функции value уничтожен, поэтому возвращать ссылку нельзя. Нужно вернуть владеющий String.

Практическое правило для начала: структуры сервера пусть владеют долгоживущими данными (String, Vec<T>, Arc<T>), а функции принимают короткие заимствования (&str, &[u8], &T).

У PHP одна строка часто одновременно означает текст и байты. В Rust это разделено:

  • String — владеющий UTF-8 текст;
  • &str — заимствованный UTF-8 текст;
  • Vec<u8> — владеющий буфер байтов;
  • &[u8] — заимствованный slice байтов.

Сетевой сервер получает байты, а не гарантированный UTF-8:

fn on_message(&self, connection: &mut dyn Connection, data: &[u8]) -> io::Result<()> {
connection.send(data)
}

Нельзя без проверки считать пакет строкой:

match std::str::from_utf8(data) {
Ok(text) => println!("{text}"),
Err(error) => eprintln!("invalid UTF-8: {error}"),
}

Индексировать String как text[0] нельзя. Один Unicode-символ может занимать несколько байтов, а пользовательская графема — несколько Unicode scalar values.

Структура хранит данные:

pub struct ServerConfig {
name: String,
address: std::net::SocketAddr,
read_buffer_size: usize,
}

impl добавляет методы:

impl ServerConfig {
pub fn new(address: std::net::SocketAddr) -> Self {
Self {
name: "none".to_owned(),
address,
read_buffer_size: 87_380,
}
}
pub fn name(&self) -> &str {
&self.name
}
}

Важны формы receiver:

  • self — метод забирает объект;
  • &self — временно читает;
  • &mut self — временно изменяет;
  • Self — текущий конкретный тип.

Первоначальная попытка описать события как fn on_start(self, ...) забирала сервер при первом вызове. Для повторяемых callbacks обычно нужен &self или &mut self.

В Rust нет конструкторов как специального элемента языка. new — обычная ассоциированная функция по соглашению.

Trait описывает поведение:

pub trait Connection {
fn id(&self) -> u64;
fn send(&mut self, data: &[u8]) -> std::io::Result<()>;
fn close(&mut self) -> std::io::Result<()>;
}

Реализация задаётся отдельно:

impl Connection for TcpConnection {
fn id(&self) -> u64 {
self.id
}
fn send(&mut self, data: &[u8]) -> std::io::Result<()> {
use std::io::Write;
self.stream.write_all(data)
}
}

Это похоже на PHP interface, но мощнее:

  • trait может содержать реализацию методов по умолчанию;
  • trait можно реализовать для существующего типа, если соблюдено orphan rule;
  • generic-код может требовать набор traits;
  • traits участвуют в проверке потокобезопасности (Send, Sync).

Rust не поддерживает наследование данных. Вместо TcpConnection extends ConnectionInterface используется:

  • TcpConnection хранит собственные данные;
  • impl Connection for TcpConnection добавляет общий контракт;
  • повторное использование строится через композицию и generic-функции.

8.1 Статическая и динамическая диспетчеризация

Заголовок раздела «8.1 Статическая и динамическая диспетчеризация»

Generic-вариант:

fn close_connection<C: Connection>(connection: &mut C) -> std::io::Result<()> {
connection.close()
}

Компилятор создаёт специализированный код для конкретного C. Это статическая диспетчеризация.

Trait object:

fn close_connection(connection: &mut dyn Connection) -> std::io::Result<()> {
connection.close()
}

dyn Connection хранит ссылку на данные и таблицу методов. Это динамическая диспетчеризация, близкая к вызову метода через interface в PHP.

В сервере Server<H: ServerHandler> использует generic для handler, а callback получает &mut dyn Connection, чтобы одинаково работать с будущими TCP, UDP или Unix-соединениями.

Не каждый trait совместим с dyn. Например, методы, возвращающие неизвестный Self, или ассоциированные константы могут сделать trait непригодным как trait object.

Rust enum — алгебраический тип, а не просто число с именем:

enum Command {
Start { daemon: bool },
Stop,
Reload { graceful: bool },
}

Каждый вариант может хранить разные данные.

Отсутствующее значение выражается явно:

fn find_connection(id: u64) -> Option<TcpConnection> {
todo!()
}

Option<T> имеет два варианта:

enum Option<T> {
Some(T),
None,
}

Обработка через match:

match find_connection(10) {
Some(connection) => println!("{}", connection.id()),
None => println!("not found"),
}

Или компактнее:

if let Some(connection) = find_connection(10) {
println!("{}", connection.id());
}

В TypeScript strictNullChecks приближает проверку к этому, но Option<T> является обычным типом с методами map, and_then, unwrap_or и другими комбинаторами.

Не стоит повсеместно вызывать unwrap(). Это осознанная паника, уместная прежде всего в тестах или при доказанном инварианте.

Ожидаемая ошибка возвращается как значение:

fn bind(address: std::net::SocketAddr) -> std::io::Result<std::net::TcpListener> {
std::net::TcpListener::bind(address)
}

Result<T, E>:

enum Result<T, E> {
Ok(T),
Err(E),
}

Оператор ? означает: извлечь Ok, а Err немедленно вернуть, при необходимости преобразовав через From.

fn send_file(stream: &mut std::net::TcpStream) -> std::io::Result<()> {
let data = std::fs::read("response.bin")?;
use std::io::Write;
stream.write_all(&data)?;
Ok(())
}

Ошибки делятся на категории:

  • ожидаемые и восстанавливаемые — Result;
  • отсутствующие значения — Option;
  • нарушение внутреннего инварианта — panic!, assert!;
  • ошибки, которые нужно обработать локально — match или if let Err(...).

В библиотечном коде предпочтительны конкретные error types. В приложении иногда допустим общий boxed error:

fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}

В текущем проекте используется io::Result, потому что все ошибки первого среза относятся к I/O.

match одновременно заменяет часть switch, destructuring и проверку типа:

match listener.accept() {
Ok((stream, address)) => println!("connected: {address}"),
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {}
Err(error) => eprintln!("accept failed: {error}"),
}

Важное свойство — исчерпывающая проверка. После добавления нового варианта enum компилятор покажет места, где его забыли обработать.

Destructuring работает и для структур:

struct Limits {
connections: usize,
buffer_size: usize,
}
let Limits {
connections,
buffer_size,
} = limits;

Частые соответствия:

PHPPythonTypeScriptRust
array как списокlistArray<T>Vec<T>
ассоциативный arraydictMap<K, V>HashMap<K, V>
foreachforfor...offor / Iterator

Rust различает три режима итерации:

let mut names = vec![String::from("tcp"), String::from("udp")];
for name in &names {
println!("{name}");
}
for name in &mut names {
name.make_ascii_uppercase();
}
for name in names {
println!("{name}");
}

Последний цикл потребляет names.

Цепочки итераторов ленивы:

let ports: Vec<u16> = [80_u16, 443, 8080]
.into_iter()
.filter(|port| *port >= 1024)
.collect();

В отличие от многих цепочек array_map/array_filter, итераторы Rust обычно не создают промежуточные коллекции.

Замыкание может захватывать окружение:

let prefix = String::from("client");
let format_id = move |id: u64| format!("{prefix}-{id}");

move перемещает захваченные значения внутрь closure. Это часто требуется при передаче работы в поток.

Есть три основных callback-trait:

  • Fn — не изменяет и не потребляет захваченное состояние;
  • FnMut — может изменять захваченное состояние;
  • FnOnce — может быть вызван хотя бы один раз и вправе потребить захваченное.

Каждый Fn также подходит там, где требуется FnMut или FnOnce, но не наоборот.

В текущем сервере callbacks представлены trait ServerHandler, а не набором closure-полей. Это даёт:

  • единый тип обработчика;
  • методы по умолчанию;
  • удобное тестирование;
  • возможность хранить состояние в handler;
  • отсутствие сложных типов вида Arc<dyn Fn(...) + Send + Sync> в публичной конфигурации.

Состояние handler при необходимости можно хранить через атомики или mutex.

Rust предотвращает data race на этапе компиляции.

  • Send — значение можно передать в другой поток;
  • Sync — &T можно безопасно разделять между потоками;
  • Arc<T> — атомарный shared ownership;
  • Mutex<T> — эксклюзивный изменяемый доступ;
  • атомики — операции над простыми числовыми состояниями без mutex.

Rc<T> подходит только одному потоку. Для сервера используется Arc<T>:

let handler = std::sync::Arc::new(handler);
let worker_handler = std::sync::Arc::clone(&handler);
std::thread::spawn(move || {
worker_handler.on_server_start(&config);
});

Arc::clone копирует не весь handler, а увеличивает счётчик владельцев.

Изменяемое разделяемое состояние:

use std::sync::{Arc, Mutex};
let messages = Arc::new(Mutex::new(Vec::<String>::new()));
let worker_messages = Arc::clone(&messages);
std::thread::spawn(move || {
worker_messages.lock().unwrap().push(String::from("hello"));
});

Mutex guard освобождает блокировку через Drop.

Для счётчиков сервер использует AtomicU64. Ordering описывает гарантии видимости между потоками:

  • Relaxed подходит независимой статистике;
  • Acquire/Release используются для согласования флага остановки;
  • SeqCst даёт наиболее строгую модель, но не должен быть автоматическим выбором вместо понимания протокола синхронизации.

Сейчас сервер создаёт один OS thread на TCP-соединение. Плюсы:

  • понятный blocking I/O;
  • только стандартная библиотека;
  • удобное изучение ownership и Send/Sync;
  • простая трассировка выполнения.

Ограничения:

  • поток имеет заметную стоимость памяти и переключения контекста;
  • число одновременно открытых соединений необходимо ограничивать;
  • для десятков тысяч соединений нужен event loop или async runtime.

Это осознанный учебный этап, а не финальная high-load архитектура.

async fn возвращает Future. Она выполняется только когда runtime или другой executor опрашивает future.

async fn load() -> std::io::Result<Vec<u8>> {
todo!()
}

Стандартная библиотека определяет Future, но не предоставляет полноценный сетевой async runtime. Обычно используют Tokio или async-std, однако добавлять их до понимания blocking-версии не обязательно.

Async полезен, когда много задач преимущественно ждут I/O. Для CPU-bound работы всё равно нужны потоки или отдельный пул.

Переход сервера к async изменит внутренний механизм ожидания, но публичные понятия ServerConfig, Connection и ServerHandler желательно сохранить или адаптировать минимально.

16. Почему нельзя буквально транслировать PHP

Заголовок раздела «16. Почему нельзя буквально транслировать PHP»

Механический перенос приводит к архитектуре, которая борется с Rust.

PHP-сервер хранит множество глобальных mutable static-полей. В Rust глобальная изменяемость требует синхронизации и усложняет тесты. Лучше, чтобы состояние принадлежало Server:

pub struct Server<H> {
config: ServerConfig,
handler: std::sync::Arc<H>,
statistics: std::sync::Arc<ConnectionStatistics>,
}

PHP stdClass, mixed и динамические поля позволяют прикрепить любые данные. В Rust состояние лучше моделировать конкретным типом или generic context. Это требует больше проектирования, но исключает опечатки и неверные типы в runtime.

PHP ServerAbstract может дать пустые методы наследнику. В Rust тот же смысл естественно выражается trait-методами по умолчанию:

pub trait ServerHandler {
fn on_connect(&self, _connection: &mut dyn Connection) {}
}

В Rust ошибка обычно проходит через сигнатуру Result. Видно, какие функции могут завершиться ошибкой, а ? оставляет распространение компактным.

PHP array одновременно является списком и ordered map. Rust заставляет выбрать Vec, HashMap, BTreeMap, массив фиксированной длины или slice. Выбор структуры данных становится частью контракта.

Рекомендуемый порядок:

  1. src/main.rs — создание конфигурации и реализация echo-handler.
  2. src/server_abstract.rs — события и значения по умолчанию.
  3. src/connection/base.rs — контракт соединения и статистика.
  4. src/connection/tcp_connection.rs — владение сокетом, чтение, запись и Drop.
  5. src/server.rs — bind, accept loop, потоки и остановка.
  6. tests/tcp_server.rs — использование публичного API снаружи crate.

Поток данных:

TcpListener::accept
|
v
TcpConnection::new
|
v
ServerHandler::on_connect
|
v
TcpConnection::read -> ServerHandler::on_message -> Connection::send
|
v
ServerHandler::on_close -> Drop

Обрати внимание на границы владения:

  • Server владеет конфигурацией;
  • Arc совместно владеет handler и статистикой;
  • worker thread владеет конкретным TcpConnection;
  • callback только временно получает &mut dyn Connection;
  • входной пакет передаётся как &[u8] без копирования.
cargo check
cargo test
cargo fmt
cargo clippy --all-targets --all-features -- -D warnings
cargo doc --open
  • cargo check быстро проверяет типы без полноценной сборки бинарника;
  • cargo test запускает unit-, integration- и doc-тесты;
  • cargo fmt приводит код к единому стандартному стилю;
  • cargo clippy находит неидиоматичные и подозрительные конструкции;
  • cargo doc строит документацию по публичному API.

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

  1. понять, кто должен владеть значением;
  2. проверить, должен ли параметр быть T, &T или &mut T;
  3. уменьшить время жизни заимствования;
  4. только затем рассматривать clone, Arc, RefCell или Mutex.

Unit-тесты обычно находятся рядом с кодом:

#[cfg(test)]
mod tests {
#[test]
fn adds_values() {
assert_eq!(2 + 2, 4);
}
}

Интеграционные тесты в tests/ видят crate как внешний пользователь. Поэтому они проверяют не приватные детали, а реальный публичный API.

Текущий TCP-тест:

  1. получает свободный локальный порт;
  2. запускает сервер в отдельном потоке;
  3. подключает реальный TcpStream;
  4. отправляет байты и проверяет echo;
  5. останавливает сервер через ServerHandle;
  6. проверяет финальную статистику.

Это надёжнее теста, который просто вызывает внутренние методы и никогда не затрагивает настоящий сокет.

Если функция только читает текст, обычно принимай &str. Для байтов — &[u8].

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

Это аналог «сделать всё глобальным объектом». Сначала раздели владение по компонентам; синхронизацию добавляй только для действительно общего mutable state.

Разделяй данные на структуры, поведение на traits, а повторное использование строй композицией.

unwrap() не является аналогом ?. unwrap() завершает поток при ошибке, ? возвращает ошибку вызывающему коду.

Хранить ссылки в долгоживущих структурах слишком рано

Заголовок раздела «Хранить ссылки в долгоживущих структурах слишком рано»

Это приводит к сложным lifetime-параметрам. Для конфигурации и runtime-состояния часто проще владеть данными.

unsafe не отключает borrow checker целиком, но переносит часть доказательств безопасности на разработчика. Для этого проекта на текущих этапах он не нужен.

Писать собственный event loop до работающего протокола

Заголовок раздела «Писать собственный event loop до работающего протокола»

Сначала полезнее реализовать корректное фреймирование, лимиты пакетов, backpressure и тесты. Иначе одновременно отлаживаются слишком многие новые концепции.

Темы:

  • package, crate и modules;
  • structs и traits;
  • ownership сокета;
  • Result и io::Error;
  • потоки, Arc, атомики;
  • integration tests.

Самостоятельные упражнения:

  1. Добавить max_connections в ServerConfig.
  2. Добавить в статистику общее число принятых соединений, не уменьшаемое при закрытии.
  3. Реализовать Display для ServerStatus.
  4. Проверить callback on_connect отдельным тестом.

Темы:

  • trait Protocol;
  • associated types;
  • slices и работа с байтами;
  • неполные TCP-пакеты;
  • Vec<u8> и буферизация;
  • проверка максимального размера сообщения.

Важно понять: один read не равен одному сообщению. TCP — поток байтов. Одно сообщение может прийти частями, а несколько сообщений — одним чтением.

Для Frame-протокола можно использовать заголовок длины:

[4 bytes big-endian length][payload]

Темы:

  • enums для методов и версий;
  • borrowing частей входного буфера;
  • HashMap или специализированное хранение headers;
  • builder API для response;
  • разделение parsing и transport;
  • fuzz-friendly parser без panic на пользовательском вводе.

Не нужно начинать с multipart, cookies и sessions. Сначала достаточно request line, headers и Content-Length.

Темы:

  • nonblocking sockets;
  • readiness;
  • state machines;
  • очередь записи;
  • high/low water marks;
  • ограничение ресурсов.

Здесь станет видно, стоит ли писать учебный event loop на системных API или подключить Tokio для прикладной версии.

Темы:

  • HTTP Upgrade;
  • бинарный формат frame;
  • masking;
  • continuation frames;
  • ping/pong и close handshake;
  • защита от слишком больших сообщений.

Темы:

  • сигналы на Unix;
  • graceful shutdown;
  • platform-specific modules через #[cfg(unix)] и #[cfg(windows)];
  • PID/status files;
  • structured logging.

Процессную модель исходного PHP-сервера не следует переносить первой: она платформенно-зависима и отвлекает от сетевого ядра.

22. Архитектурные ориентиры для дальнейшего переноса

Заголовок раздела «22. Архитектурные ориентиры для дальнейшего переноса»

Сохранять стоит не буквальную форму PHP-классов, а проверенные доменные границы:

  • Server — lifecycle и приём соединений;
  • Connection — транспорт и буферы;
  • Protocol — границы сообщений и кодирование;
  • Handler — пользовательские события;
  • Event loop — ожидание готовности I/O;
  • Timer — планирование по времени.

Желательные зависимости:

application handler
|
v
protocol abstraction
|
v
connection abstraction
|
v
event loop / operating system

Protocol не должен управлять процессами, Connection не должен разбирать HTTP, а event loop не должен знать о WebSocket. Это позволит менять blocking transport на async без переписывания протоколов целиком.

ЗадачаPHP/Python/TSRust
Необязательное значениеnull, None, undefinedOption<T>
Успех или ошибкаисключениеResult<T, E>
Объект с полямиclass/objectstruct
Контракт поведенияinterface/protocoltrait
Закрытая иерархия вариантовenum/unionenum
Динамический интерфейсobject/interface reference&dyn Trait, Box<dyn Trait>
Общий владелец в одном потокеобычная ссылкаRc<T>
Общий владелец между потокамиобычная ссылка/runtimeArc<T>
Синхронизация измененийlock/runtime conventionsMutex<T>, RwLock<T>, atomics
Списокarray/list/ArrayVec<T>
Словарьarray/dict/MapHashMap<K, V>
Деструктор/ finally__destruct, context managerDrop/RAII
Callbackcallable/functionclosure или trait
Проверка типа в runtimeinstanceof, isinstancegenerics, traits, enum

Не пытайся запомнить все lifetime-аннотации заранее. Для практики важнее каждый раз отвечать на четыре вопроса:

  1. Кто владеет этим значением?
  2. Кто только временно его читает?
  3. Кто и когда имеет право его менять?
  4. Может ли значение пережить поток, callback или async-задачу, куда оно передаётся?

Если ответы ясны, типы Rust обычно получаются естественно. Если приходится беспорядочно добавлять clone, Arc<Mutex<_>>, 'static или unsafe, чаще всего не определена граница владения или компонент выполняет слишком много обязанностей.

Лучший учебный цикл для этого проекта:

  1. взять маленькую возможность исходного PHP-сервера;
  2. сформулировать её контракт без привязки к PHP;
  3. определить владельцев данных;
  4. написать минимальный Rust API;
  5. сначала написать проверяемый сценарий;
  6. реализовать его на стандартной библиотеке;
  7. прогнать fmt, тесты и Clippy;
  8. только затем обобщать архитектуру.

Так переписывание становится не переводом синтаксиса, а последовательным изучением системного проектирования на Rust.

Исходник