Всем привет! Уже столько времени прошло с прошлой статьи, в которой я писал про реализацию своей небольшой версии, написанной на Go, как всегда исходный код доступен на GitHub. Сразу думаю сказать, что за это время успел уже перейти на линукс(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, который передаётся в другой поток сетевого сервера 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 и если вы хотите поддержать меня валютой, то у меня есть patreon.

Напишите можалуйста ваше мнение о моём процессе. Буду стараться отвечать на все.

Взаранее скажу, что плагины скоро буду вводить и они будут на основе WASM, скорее всего благодаря движку Wasmer.

Комментарии (4)


  1. EragonRussia
    05.02.2022 19:13
    +5

    Вычитка, вычитка, вычитка…
    Код несколько пугает своим качеством. Попробуйте запустить cargo clippy, многое покажет. И поменьше unwrap'ов, бога ради.


    1. Distemi Автор
      05.02.2022 19:48

      Да, согласен, что код с проблемами, но я буду надеятсья, что в следующих частях смогу его делать лучше. Но всё-таки в этой часте конечно планировалось перенести с Go на Rust с некоторыми улучшениями.


  1. SnakeSolid
    06.02.2022 08:16
    +3

    Вам точно для остановки сервера нужны две булевых переменные SHUTDOWN_SERVER и NET_SERVER_WORKS внутри разных мьютексов, возможно имеет смысл заменить их на AtomicBool или один мьютекс со структурой { bool, bool } внутри?


    1. Distemi Автор
      06.02.2022 09:34

      Впринципе хорошая идея, попробую к следующей части реализовать такой подход.