Всем привет! Уже столько времени прошло с прошлой статьи, в которой я писал про реализацию своей небольшой версии, написанной на Go, как всегда исходный код доступен на GitHub. Сразу думаю сказать, что за это время успел уже перейти на линукс(Mint Cinnamon), получить проблемы с интегрированной GPU, но в конце концов наконец я смог нормально работать с редактором от JetBrains и сделать переход с Go на Rust, это было сделано так-как я думал изначально писать на расте, но было очень проблематично компилировать... Но вот и был сделан всё-таки переход с улучшениями как производительности так и возможностей!)
Причина перехода с Go на Rust
Изначально я задумывался о создании на нём, но не мог нормально скомпилировать код.
Код на расте работает в разы быстрее и безопаснее.
Теперь скорость можно измерять в наносекундах...)
Немного важных уточнений
При переходе на Rust я решил, что стоит делать по-умолчанию английский язык, так-как его знают большинство, а это значит если человек, который не знает русского - сможет вполне использовать, из-за этого все комментарии в статье и коде на GitHub будут написаны на английском языке, при этом может быть и много ошибок... Получилось так-же много файлов, но есть которые почти пустые и только обьединяют несколько модулей, поэтому такие файлы я не буду комментировать, а только оставлю код.
Продолжение #1.1
Код на расте вышел всё-таки в разы больше так-как я пытался использовать как можно больше своего, но всё-таки для лучшего результата использовал много, но важных библиотек и прописал их в Cargo.toml:
Содержимое Cargo.toml
[package] name = "ule" version = "0.1.0" edition = "2021" publish = true authors = [ "Distemi <distemi.bot@mail.ru>" ] homepage = "https://github.com/Distemi/ULE" repository = "https://github.com/Distemi/ULE" [dependencies] # Быстрый HashMap и другое. ahash = "0.7.6" # Глобальные переменные lazy_static = "1.4.0" # Struct <-> JSON serde = { version = "1.0.136", features = ["derive"] } serde_json = "1.0.78" # Утилиты логирования log = "0.4.14" fern = { version = "0.6", features = ["colored"] } # Время chrono = "0.4.19" # Асинхронность(скоро точно понадобится) async-std = "1.10.0" # Однопоточный TCP и UDP сервер [dependencies.mio] version = "0.8.0" default-features = false features = [ "os-ext", "net" ] [profile.release] opt-level = "z"
Как раз наш Cargo.toml является одним из основных файлов для проекта, а следющий по важности src/main.rs:
Наш main.rs
#![allow(unused_must_use)] use crate::config::{ADDRESS, ADDRESS_PORT}; use crate::logger::start_input_handler; use crate::network::network_server_start; use fern::colors::Color; use std::error::Error; use std::process; use std::sync::mpsc::channel; use std::sync::Arc; use std::time::SystemTime; use std::{fmt, thread}; use utils::logger; // Use a macros from serde(Serialize and Deserialize), log(Logging) and lazy_static(Global variables) #[macro_use] extern crate serde; #[macro_use] extern crate log; #[macro_use] extern crate lazy_static; pub mod config; pub mod network; pub mod utils; // Main function of application fn main() { let start = SystemTime::now(); // Initialize logger println!("Starting ULE v1.0.0..."); if let Err(err) = logger::setup_logger() { eprintln!("Failed to initialize logger: {}", err); process::exit(1); } // Creating channel for multithreading communication with main's thread and network's thread let (tx, rx) = channel::<bool>(); // Generate server's address and make it accessible with thread safe let address = Arc::new(String::from(format!( "{}:{}", ADDRESS, ADDRESS_PORT.to_string() ))); // Start network in another thread thread::spawn({ let address = address.to_string(); move || { // Start network // If failed to start when return error if let Err(err) = network_server_start(address, &tx) { error!("{}", err); tx.send(false); } } }); // Wait for status from server's network if rx.recv().unwrap_or(false) { // If Server successful started info!("Server started at {}", address); // Showing about the full launch and showing the time to start { let elapsed = start.elapsed().unwrap(); info!( "The server was successfully started in {}", if elapsed.as_secs() >= 1 { format!("{}s", elapsed.as_secs()) } else if elapsed.as_millis() >= 1 { format!("{}ms", elapsed.as_millis()) } else { format!("{}ns", elapsed.as_nanos()) } ); drop(elapsed); }; } else { // If Failed to start Server error!("Failed to start server on {}.", address); process::exit(1); } // Remove channel std::mem::drop(rx); // Start console input handler(input commands) start_input_handler(); } // Custom error(yes, not std::io:Error) #[derive(Debug)] pub struct SimpleError(String, Option<std::io::Error>); impl Error for SimpleError {} impl fmt::Display for SimpleError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { // Check is error provided if self.1.is_some() { write!(f, "{}: {:?}", self.0, self.1) } else { write!(f, "{}", self.0) } } } // Custom Result with custom Error pub type SResult<T> = Result<T, SimpleError>;
В нашем main.rs в самом начале инициализируем отсчёт времени, который вскоре будем использовать для показа время запуска. Потом мы пытаемся инициализировать логгер(fern + log), при неудаче - выводим ошибку и "убиваем" процесс. Следующим шагом у нас идёт создания некого канала, а после строки адресса сервера, но канал, который на самом деле выглядит по методам как UDP, но с блокировкой потоков, он нам нужен для ожидания основного потока, который ждёт результат запуска от потока сетевого сервера(TCP сервер), получилось - выводим информацию об успешном запуске сетевого сервера и за сколько времени запустилось, при ошибке - выводим информацию о проблеме запуска. Если получилось запустить наш TCP сервер, то удаляем наш канал связи и начинаем слушать ввод с консоли. Как видно есть типы SResul(из сокращения SimpleResult) и SimpleError, первый говорит сам за себя, как и второй, но для которого идёт приминение разных trait для показа ошибок.
Наш метод инициализации логгера лежит в файле src/utils/logger/input.rs, но я покажу так-же src/utils/mod.rs и src/utils/logger/mod.rs, так-как они зависимы:
src/utils/mod.rs
pub mod chat; pub mod logger;
Просто импортируем модули чата и логгера публично
src/utils/logger/mod.rs
mod input; mod log_lib; pub use {input::start_input_handler, log_lib::setup_logger};
Тут идёт импорт input.rs и log_lib.rs, а так-же экспорт методов start_input_handler и setup_logger
Содержимое файла с методом инициализации логгера
use crate::Color; use fern::colors::ColoredLevelConfig; use std::fs; // Logger's initialize(fern, color and log) pub fn setup_logger() -> Result<(), fern::InitError> { // Removing latest log if exists fs::remove_file("latest.log"); // Setting colors let colors = ColoredLevelConfig::new() .info(Color::BrightBlack) .warn(Color::Yellow) .error(Color::Red) .trace(Color::BrightRed); // Setting fern fern::Dispatch::new() // Setting custom format to logging .format(move |out, message, record| { out.finish(format_args!( "{} [{}] {}", chrono::Local::now().format("[%m-%d %H:%M:%S]"), colors.color(record.level()), message )) }) // Setting log-level .level(log::LevelFilter::Info) // Setting target's logger .chain(std::io::stdout()) // Setting log's file .chain(fern::log_file("latest.log")?) // Applying settings .apply()?; // If successful setting - returning ok Ok(()) }
При инициализации логера мы в первую очередь удаляем файл последнего лога latest.log, потом устанавливаем на каждый уровень лога свой цвет(INFO = серый, WARN - жёлтый, ERROR - красный, TRACE - ярко-красный). Позже идёт инициализация самого логгера fern и для него мы устанавливаем формат: [ДАТА] [УРОВЕНЬ] СООБЩЕНИЕ, цвет имеет только уровень, а дата и сообщение стандартным цвветом консоли. Для логгера устанавливаем вывод в stdout(консоль вывода), минимальный уровень вывода - INFO, а так-же вывод в лог-файл и принимаем эти изменения. Если не было ошибок при этих действиях - возращяем успешный пустой результат.
Далее у нас через main.rs создаётся канал mpsc, который передаётся в другой поток сетевого сервера TCP и это делается через network_server_start из пакета network, который имеет много "пустых" файлов, но оттуда нам нужен лишь протокол, сервер, буферы и обработчики. Сам сетевой сервер располагается по пути src/network/server.rs:
Содержимое сетевого сервера
use crate::network::handler::{handshaking, status_handler}; use crate::network::network_client::ConnectionType::HANDSHAKING; use crate::network::network_client::NetworkClient; use ahash::AHashMap; use mio::net::TcpListener; use mio::{Events, Interest, Poll, Token}; use std::io; use std::sync::mpsc::Sender; use std::sync::Mutex; use std::time::Duration; // Declare global variables lazy_static! { // Server need to shut down? (true - yes, needs to shutdown network server). pub static ref SHUTDOWN_SERVER: Mutex<bool> = Mutex::new(false); // Server's works status. pub static ref NET_SERVER_WORKS: Mutex<bool> = Mutex::new(true); } // Server's Token(ID) const SERVER: Token = Token(0); // Next Token fn next(current: &mut Token) -> Token { let next = current.0; current.0 += 1; Token(next) } // Start a network server pub fn network_server_start(address: String, tx: &Sender<bool>) -> std::io::Result<()> { // Creating Network Pool let mut poll = Poll::new()?; // Creating Network Events Pool let mut events = Events::with_capacity(256); // Converting String's address to SocketAddr let addr = address.parse().unwrap(); // Starting a Network Listener let mut server = TcpListener::bind(addr)?; // Register server's Token poll.registry() .register(&mut server, SERVER, Interest::READABLE)?; // Creating a list of connections let mut connections: AHashMap<Token, NetworkClient> = AHashMap::new(); // Creating a variable with latest token. let mut unique_token = Token(SERVER.0 + 1); // Send over the channel that the server has been successfully started tx.send(true); // Network Events getting timeout let timeout = Some(Duration::from_millis(10)); // Infinity loop(while true) to handing events loop { // Checks whether it is necessary to shutdown the network server if *SHUTDOWN_SERVER.lock().unwrap() { *NET_SERVER_WORKS.lock().unwrap() = false; info!("Network Server Stopped!"); return Ok(()); } // Getting a events from pool to event's pool with timeout poll.poll(&mut events, timeout)?; // Handing a events for event in events.iter() { // Handing event by token match event.token() { // If it server's event // Reading a all incoming connection SERVER => loop { // Accepting connection let (mut connection, _) = match server.accept() { // If successful Ok(v) => v, // If not exists incoming connection Err(e) if e.kind() == io::ErrorKind::WouldBlock => { break; } // If failed to get incoming connection Err(e) => { return Err(e); } }; // Generating new token for this connection let token = next(&mut unique_token); // Registering connection with token poll.registry().register( &mut connection, token, Interest::READABLE.add(Interest::WRITABLE), )?; // Pushing connection into connection's list connections.insert( token, NetworkClient { stream: connection, conn_type: HANDSHAKING, }, ); }, // Handing event from client token => { // Handing event by connection's stage let done = if let Some(connection) = connections.get_mut(&token) { let m = match &connection.conn_type { HANDSHAKING => handshaking, _ => status_handler, }; // Trying to handing m(connection, &event).unwrap_or(false) } else { false }; // If needs to close connection - removing from list, unregister and close connection's stream if done { if let Some(mut connection) = connections.remove(&token) { poll.registry().deregister(&mut connection.stream)?; connections.remove(&token); } } } } } } }
Да, уже целых 125 строчек, но это ещё мало
В нём мы инициализируем глобальные переменные используя lazy_static, обратите внимание, что тип bool завёрнут в оболочку Mutex, который гарантирует мультипоточный доступ к переменной, к чтении и записи, но для получения этих переменных блокируется поток ожидая информации. Создаётся так-же простая примитивная структура SERVER, который имеет значение айди сервера в сетевом сервере. Далее идёт next, который работает как ++ для переменных(в расте не ++, а +=1), а следует за этой функцией уже другая - network_server_start. Функция сетевого сервера на старте инициализирует два пула, один отвечает за хранилища событий и дальше идёт парсинг строки в адресс, а следом попытка запустить TCP сервер на этом адрессе, который регестрируем в пуле как только-чтение, после мы создаём список подключений и уникальный токен на новое подключение, если не было ошибок, то отправляем в канал связи - true, который означать о успешном запуске. Переменная timeout используется как лимит ожидания событий, чтобы начать цикл обработки запросов заного, что есть в цикле: проверка на нужду в отключении сервера и если надо, то просто устанавливаем статус, что сервер выключен и останавливаем цикл, остальную работу по одключении слушателя и тд делаем сам компилятор. Если же останавливать нам не надо, то мы ждём до 10мс сетевые события и позже обрабатываем существующие события. Серверные события бывают только - принятие нового подключения, поэтому мы создаём ещё цикл в котором принимаем все подключения, регистрируем их. В случае если это события связанные с клиентами(присланный пакет например) - в зависимости от типа подключения(HandShaking, Status и тд) передаём соответствующему обработчику и в случае если обработчик возращает true, то мы разрываем соединение и удаляем его из хранилища подключений.
Протокол, обработка подключений, чтение и создание пакетов
Так-как ядро я пытаюсь сделать менее зависимым от библиотек, то для чата и протокола будет всё написано с 0 и для обеспечения большего контроля над чтением и записью.
Было решено хранить буферы пакетов в виде векторов к которым добавлены методы чтения и записи(только для Vec<u8>):
Чтение буферов
use crate::{SResult, SimpleError}; /// Reader [Vec] of bytes pub trait PacketReader { // 1-Byte fn get_u8(&mut self) -> u8; fn get_i8(&mut self) -> i8; // 2-Byte fn get_u16(&mut self) -> u16; fn get_i16(&mut self) -> i16; // 4-Byte fn get_varint(&mut self) -> SResult<i32>; // 8-Byte fn get_i64(&mut self) -> i64; // Another fn get_string(&mut self) -> SResult<String>; fn read_base(&mut self) -> SResult<(i32, i32)>; } // Apply reader to Vec impl PacketReader for Vec<u8> { // Read a single byte as u8 ( 8-Bit Unsigned Integer ) fn get_u8(&mut self) -> u8 { self.remove(0) } // Read a single byte as i8 ( 8-Bit Integer ) fn get_i8(&mut self) -> i8 { self.remove(1) as i8 } // Read a two bytes as u16 ( 16-Bit Unsigned Integer ) fn get_u16(&mut self) -> u16 { u16::from_be_bytes([self.get_u8(), self.get_u8()]) } // Read a two bytes as i16 ( 16-Bit Integer ) fn get_i16(&mut self) -> i16 { i16::from_be_bytes([self.get_u8(), self.get_u8()]) } // Read a VarInt ( Dynamic-length 32-Bit Integer ) fn get_varint(&mut self) -> SResult<i32> { // Result variable let mut ans = 0; // Read up to 4 bytes for i in 0..4 { // Read one byte let buf = self.get_u8(); // Calculate res with bit moving and another ans |= ((buf & 0b0111_1111) as i32) << 7 * i; // If it's limit when stop reading if buf & 0b1000_0000 == 0 { break; } } // Return result as successful Ok(ans) } // Read a Long ( 64-Bit Integer ) fn get_i64(&mut self) -> i64 { // Yes, read 8 bytes i64::from_be_bytes([ self.get_u8(), self.get_u8(), self.get_u8(), self.get_u8(), self.get_u8(), self.get_u8(), self.get_u8(), self.get_u8(), ]) } // Read a String ( VarInt as len; bytes[::len] ) fn get_string(&mut self) -> SResult<String> { // Getting string-length let len = self.get_varint()?; // Create String's bytes buffer let mut buf = Vec::new(); // Reading Bytes for _ in 0..len { buf.push(self.get_u8()) } // Convert Bytes to UTF8 String match String::from_utf8(buf) { Ok(v) => Ok(v), Err(_) => Err(SimpleError(String::from("Failed to parse chars"), None)), } } // Read first two VarInt(Packet's length and id) fn read_base(&mut self) -> SResult<(i32, i32)> { let len = self.get_varint()?; let pid = self.get_varint()?; Ok((len, pid)) } }
Тут мы можем наглядно увидеть чтение VarInt, String, Long и другое, что пока надо было при написании ядра.
Запись в буферы пакетов
/// Writer [Vec] of bytes pub trait PacketWriter { // 1-Byte fn write_u8(&mut self, value: u8); fn write_i8(&mut self, value: i8); // 2-Byte fn write_u16(&mut self, value: u16); fn write_i16(&mut self, value: i16); // 4-Byte fn write_varint(&mut self, value: i32); // 8-Byte fn write_i64(&mut self, value: i64); // Another fn write_vec_bytes(&mut self, bytes: Vec<u8>); fn write_string(&mut self, value: String); fn create_packet(&mut self, pid: i32) -> Vec<u8>; } impl PacketWriter for Vec<u8> { // Writing byte fn write_u8(&mut self, value: u8) { self.push(value); } // Writing byte fn write_i8(&mut self, value: i8) { self.push(value as u8) } // Writing 2-byte unsigned integer fn write_u16(&mut self, value: u16) { self.extend_from_slice(&value.to_be_bytes()); } // Writing 2-byte unsigned integer fn write_i16(&mut self, value: i16) { self.extend_from_slice(&value.to_be_bytes()); } // Writing bytes as VarInt fn write_varint(&mut self, mut value: i32) { // Bytes buffer let mut buf = vec![0u8; 1]; // Byte's length let mut n = 0; // Converts value to bytes loop { // Break if it's limit if value <= 127 || n >= 8 { break; } // Pushing a byte to buffer buf.insert(n, (0x80 | (value & 0x7F)) as u8); // Moving value's bits on 7 value >>= 7; value -= 1; n += 1; } // Pushing byte, because it lower that 256(<256) buf.insert(n, value as u8); n += 1; // Pushing converted bytes into byte's buffer self.extend_from_slice(&buf.as_slice()[..n]) } // Writing Long ( 64-Bit Integer ) fn write_i64(&mut self, value: i64) { self.extend_from_slice(value.to_be_bytes().as_slice()) } // Alias of extend_from_slice, but works with Vec, not Slice fn write_vec_bytes(&mut self, mut bytes: Vec<u8>) { self.append(&mut bytes); } // Write String (VarInt as len and string's bytes) fn write_string(&mut self, value: String) { // Getting String as Bytes let bytes = value.as_bytes(); // Writing to buffer a length as VarInt self.write_varint(bytes.len() as i32); // Writing to buffer a string's bytes self.extend_from_slice(bytes); } // Packet's base builder fn create_packet(&mut self, pid: i32) -> Vec<u8> { // Creating empty packet's buffer let mut packet = Vec::new(); // Creating length's bytes buffer and fill it as VarInt let mut len_bytes: Vec<u8> = Vec::new(); len_bytes.write_varint(pid); // Writing full packet's length(content + length's bytes) packet.write_varint((self.len() + len_bytes.len()) as i32); // Writing length bytes packet.extend_from_slice(len_bytes.as_slice()); // Drop(Free) length bytes buffer drop(len_bytes); // Writing some packet's content packet.extend_from_slice(self.as_slice()); // Returning result packet } }
Запись к буферам выглядит в разы интересней из-за больших требований к стандарту протокола MineCraft.
Обработчики пакетов на статусы 0(HandShaking) и 1(Status) расположены в одном файле src/network/handler.rs и в нём на каждый тип своя функция.
Вот например HandShaking:
pub fn handshaking(conn: &mut NetworkClient, event: &Event) -> SResult<bool> { // Checking if we can read the package if !event.is_readable() { return Ok(false); } // Reading packet let handshake = read_handshake_packet(conn); // Checking if is error if handshake.is_err() { return Ok(true); } // Getting results let (_, _, _, next_state) = handshake.unwrap(); // Change types conn.conn_type = match next_state { 1 => STATUS, _ => STATUS, }; Ok(false) }
Хоть функция и имеет 20 строчек, но в ней мы требуем лишь чтения первого пакета для определения следующего статуса ну и основное чтение пакета происходит в read_handshake_packet:
Функция чтения HandShake
pub fn read_handshake_packet(client: &mut NetworkClient) -> SResult<(u32, String, u16, u32)> { // Read bytes from client let (ok, p, err) = match client.read() { Ok((ok, p)) => (ok, Some(p), None), Err(err) => (false, None, Some(err)), }; // If failed to read when... if !ok || err.is_some() { return Err(SimpleError( String::from("Failed to read packet"), if err.is_some() { err.unwrap().1 } else { None }, )); } // Reading packet let mut p: Vec<u8> = p.unwrap(); // Try to read Length and PacketID from packet(on handshaking stage only 0x00) p.read_base()?; // Reading version, address and etc. let ver = p.get_varint()? as u32; let address = p.get_string()?; let port = p.get_u16(); let next_state = p.get_varint()? as u32; // States can be only 1 - status, 2 - play if next_state >= 3 { return Err(SimpleError(String::from("Invalid client"), None)); } // Returning results Ok((ver, address, port, next_state)) }
Мы читаем пакет и при ошибке возращаем её, а если получилось прочитать полностью, то и возращаем результаты чтения.
Для обработки статуса у нас есть иная функция:
pub fn status_handler(conn: &mut NetworkClient, event: &Event) -> SResult<bool> { // Checking if we can read and write if !event.is_readable() || !event.is_writable() { return Ok(false); } // Getting a input's bytes let (ok, p, err) = match conn.read() { Ok((ok, p)) => (ok, Some(p), None), Err(err) => (false, None, Some(err)), }; // Checking if a read or not if !ok { return Ok(err.is_some()); } // Packet's bytes let mut p: Vec<u8> = p.unwrap(); // Cloning bytes(for ping-pong) let bytes = p.clone(); // Reading a packet's length(and remove...) and PacketID let (_, pid) = p.read_base()?; match pid { // Is Ping List 0x00 => { drop(bytes); conn.stream.write_all(&*create_server_list_ping_response()); } // Is Ping-Pong 0x01 => { conn.stream.write_all(bytes.as_slice()); match conn.stream.peer_addr() { Ok(v) => info!("Server pinged from {}", v), Err(_) => { info!("Server pinged.") } } } _ => {} } Ok(false) }
В ней снова при вызове в первую очередь проверяем на возможность не только чтения, но и записи так-как на этом этапе мы всегда отдаём некий результат. Снова читаем буффер и пытаемся прочитать его начал сохранив при этом айди пакета(0x00 - Список, 0x01 - Пинг-Понг) я так-же реализовал небольшой генератор буффера для списка:
Сам генератор ответа на список
// Structs for status MOTD response #[derive(Debug, Serialize)] pub struct ListPingResponse { pub version: ListPingResponseVersion, pub players: ListPingResponsePlayers, pub description: ChatMessage, } #[derive(Debug, Serialize)] pub struct ListPingResponseVersion { pub name: String, pub protocol: u32, } #[derive(Debug, Serialize)] pub struct ListPingResponsePlayers { pub max: u32, pub online: u32, pub sample: Vec<ListPingResponsePlayerSample>, } #[derive(Debug, Serialize)] pub struct ListPingResponsePlayerSample { pub name: String, pub id: String, } /// Build packet's bytes as result pub fn create_server_list_ping_response() -> Vec<u8> { // Initialize empty byte's vector let mut bytes = Vec::new(); // Generating String and convert to bytes. // String generated as JSON by serde and serde_json libraries bytes.write_string( serde_json::to_string(&ListPingResponse { version: ListPingResponseVersion { name: String::from("ULE"), protocol: PROTOCOL_VERSION, }, players: ListPingResponsePlayers { max: 10, online: 0, sample: vec![], }, // Some clients can read colors and so on without convert into JSON description: ChatMessage::str("&a&lHello!"), }) .unwrap(), ); // Build completed packet. Server List Ping - PacketID is 0x00 bytes.create_packet(0x00) }
Для него мы сначала используем информацию о том как должен выглядеть JSON ответа и для ответа мы делаем пустой буффер, записываем байты переведённой сструктуры в JSON и генерируем пакет с айди 0x00. Для серелизации используем serde и serde_json.
Если пингануть сервер, то можно увидеть будет результат. При получении пинг-понг мы просто отправляем копию буфера так-как это более экономный вариант так-как иначе бы пришлось читать Long и другое, что нагружало бы процессор и ОЗУ больше чем просто копия буффера.
Последнее... Input в консоли или же STDIN.
Последняя функция из main.rs - ввод комманд, пока он будет очень примитивный и иметь всего stop и вывод введённого. Так-как имеется не так много возможностей казалось бы, то и ничего важного не будет, но нет! Он будет нам блокировать основной поток приложения возволяя ему работать, ведь если основной поток будет остановлен, то и вся программа остановится. Поэтому как выглядит функция так:
use crate::network::{NET_SERVER_WORKS, SHUTDOWN_SERVER}; use std::time::Duration; use std::{io, process, thread}; // Loop for handling input pub fn start_input_handler() -> std::io::Result<()> { // Input buffer let mut inp = String::new(); // STDIN - os input let stdin = io::stdin(); // loop for infinity handling loop { // Before write buffer we need to clear buffer inp.clear(); // Reading a line stdin.read_line(&mut inp)?; // Clearing input's buffer inp = inp.replace("\n", ""); // Simple realisation of stop command, but in updates be removed from here in another place if inp.starts_with("stop") { // Sending status to shutdown network server *SHUTDOWN_SERVER.lock().unwrap() = true; info!("Stopping server..."); // Running process killing in 6 secs if failed to common shutdown thread::spawn(|| { thread::sleep(Duration::from_secs(6)); process::exit(0); }); // Waiting for shutdown network's server loop { if *NET_SERVER_WORKS.lock().unwrap() == true { thread::sleep(Duration::from_millis(25)); } else { break; } } // Disabling the input return Ok(()); } // If it's not stop command - when display buffer, but in updates be removed info!("Entered: {}", inp); } }
Мы сначала создаём буффер и stdin, а так-же запускаем цикл в котором сначала очищаем буффер от прошлого ввода и потом блокируем поток в ожидании ввода. При получении ввода проверяем на содержмиое и если это stop, то устанавливаем сетевому серверу информацию о том, что надо выключать слушатель и дожидаемся выключения, но если за 6 секунд не произошло отключения - завершаем процесс с кодом 0, если же у нас была введена иная комманда, то просто выводим её в консоль с уровнем INFO, да, просто выводим.
Итог части #1.1
Данная часть была как-бы заменой #1 и в данной конечно из важного было:
Переход на Rust с языка Go
Основной язык проекта - Английский
Улучшение производительности в разы благодаря Rust
Создание логгера и ввода
Полная работа ядра пока в 2 потоках, а не как выходило в Go
Вот такие изменения я думаю стоили такого перехода, тем более учитывая, что Rust мне больше нравиться благодаря своей приближённости к устройству и отличная работа с ОЗУ:


Сервер пингуется легко, потребляя 128КБ на Linux, а на моём 4300U запускался за 735236ns.
Я надеюсь вам интересно читать статьи о разработке ядра и снова скажу:
Исходный код ядра доступен на GitHub и если вы хотите поддержать меня валютой, то у меня есть patreon.
Напишите можалуйста ваше мнение о моём процессе. Буду стараться отвечать на все.
Взаранее скажу, что плагины скоро буду вводить и они будут на основе WASM, скорее всего благодаря движку Wasmer.
