- PVSM.RU - https://www.pvsm.ru -

ULE — самописное MC Java ядро. Часть #1.1 — HelloWorld и изменения…

Всем привет! Уже столько времени прошло с прошлой статьи [1], в которой я писал про реализацию своей небольшой версии, написанной на Go, как всегда исходный код доступен на GitHub [2]. Сразу думаю сказать, что за это время успел уже перейти на линукс(Mint Cinnamon), получить проблемы с интегрированной GPU, но в конце концов наконец я смог нормально работать с редактором от JetBrains и сделать переход с Go на Rust, это было сделано так-как я думал изначально писать на расте, но было очень проблематично компилировать... Но вот и был сделан всё-таки переход с улучшениями как производительности так и возможностей!)

Причина перехода с Go на Rust

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

  2. Код на расте работает в разы быстрее и безопаснее.

  3. Теперь скорость можно измерять в наносекундах...)

Немного важных уточнений

При переходе на 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 [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:

Функция чтения 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 и в данной конечно из важного было:

  1. Переход на Rust с языка Go

  2. Основной язык проекта - Английский

  3. Улучшение производительности в разы благодаря Rust

  4. Создание логгера и ввода

  5. Полная работа ядра пока в 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