Всем привет! Уже столько времени прошло с прошлой статьи, в которой я писал про реализацию своей небольшой версии, написанной на 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 мне больше нравиться благодаря своей приближённости к устройству и отличная работа с ОЗУ:
![Использование ресурсов при активном пинге Использование ресурсов при активном пинге](https://habrastorage.org/getpro/habr/upload_files/3c4/42e/61f/3c442e61fd80e60876d84e15adc51898.png)
![Результат при пинге сервера. Почти тоже самое, что и в прошлой части. Результат при пинге сервера. Почти тоже самое, что и в прошлой части.](https://habrastorage.org/getpro/habr/upload_files/61e/ea6/8b7/61eea68b7491d3d5138ea164ace0481f.png)
Сервер пингуется легко, потребляя 128КБ на Linux, а на моём 4300U запускался за 735236ns.
Я надеюсь вам интересно читать статьи о разработке ядра и снова скажу:
Исходный код ядра доступен на GitHub и если вы хотите поддержать меня валютой, то у меня есть patreon.
Напишите можалуйста ваше мнение о моём процессе. Буду стараться отвечать на все.
Взаранее скажу, что плагины скоро буду вводить и они будут на основе WASM, скорее всего благодаря движку Wasmer.