- PVSM.RU - https://www.pvsm.ru -
Всем привет! Уже столько времени прошло с прошлой статьи [1], в которой я писал про реализацию своей небольшой версии, написанной на Go, как всегда исходный код доступен на GitHub [2]. Сразу думаю сказать, что за это время успел уже перейти на линукс(Mint Cinnamon), получить проблемы с интегрированной GPU, но в конце концов наконец я смог нормально работать с редактором от JetBrains и сделать переход с Go на Rust, это было сделано так-как я думал изначально писать на расте, но было очень проблематично компилировать... Но вот и был сделан всё-таки переход с улучшениями как производительности так и возможностей!)
Изначально я задумывался о создании на нём, но не мог нормально скомпилировать код.
Код на расте работает в разы быстрее и безопаснее.
Теперь скорость можно измерять в наносекундах...)
При переходе на Rust я решил, что стоит делать по-умолчанию английский язык, так-как его знают большинство, а это значит если человек, который не знает русского - сможет вполне использовать, из-за этого все комментарии в статье и коде на GitHub будут написаны на английском языке, при этом может быть и много ошибок... Получилось так-же много файлов, но есть которые почти пустые и только обьединяют несколько модулей, поэтому такие файлы я не буду комментировать, а только оставлю код.
Код на расте вышел всё-таки в разы больше так-как я пытался использовать как можно больше своего, но всё-таки для лучшего результата использовал много, но важных библиотек и прописал их в 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:
#![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, так-как они зависимы:
pub mod chat;
pub mod logger;
Просто импортируем модули чата и логгера публично
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 [3], который передаётся в другой поток сетевого сервера 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:
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 и другое, что нагружало бы процессор и ОЗУ больше чем просто копия буффера.
Последняя функция из 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 и в данной конечно из важного было:
Переход на Rust с языка Go
Основной язык проекта - Английский
Улучшение производительности в разы благодаря Rust
Создание логгера и ввода
Полная работа ядра пока в 2 потоках, а не как выходило в Go
Вот такие изменения я думаю стоили такого перехода, тем более учитывая, что Rust мне больше нравиться благодаря своей приближённости к устройству и отличная работа с ОЗУ:


Сервер пингуется легко, потребляя 128КБ на Linux, а на моём 4300U запускался за 735236ns.
Я надеюсь вам интересно читать статьи о разработке ядра и снова скажу:
Исходный код ядра доступен на GitHub [2] и если вы хотите поддержать меня валютой, то у меня есть patreon [4].
Напишите можалуйста ваше мнение о моём процессе. Буду стараться отвечать на все.
Взаранее скажу, что плагины скоро буду вводить и они будут на основе WASM, скорее всего благодаря движку Wasmer.
Автор: Эдуард
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/minecraft/371854
Ссылки в тексте:
[1] прошлой статьи: https://habr.com/ru/post/646483/
[2] исходный код доступен на GitHub: https://github.com/Distemi/ULE
[3] канал mpsc: https://doc.rust-lang.org/std/sync/mpsc/
[4] есть patreon: https://www.patreon.com/user?u=64366641
[5] Источник: https://habr.com/ru/post/649779/?utm_source=habrahabr&utm_medium=rss&utm_campaign=649779
Нажмите здесь для печати.