- PVSM.RU - https://www.pvsm.ru -
Я продолжаю изучать Rust. Я еще много не знаю, поэтому делаю много ошибок. В прошлый раз я попробовал сделать игру Змейка [1]. Попробовал циклы, коллекции, работу с 3D Three.rs [2]. Узнал про ggez [3] и Amethyst [4]. В этот раз я попробовал сделать клиент и сервер для чата. Для GUI использовал Azul [5]. Так же смотрел Conrod [6], Yew [7] и Orbtk [8]. Попробовал многопоточность, каналы и работу с сетью. Учел ошибки прошлой статьи и постарался сделать эту более развернутой. За подробностями добро пожаловать под кат.
-->Исходники, работает на Windows 10 x64 [9]
Для сетевого взаимодействия я использовал UDP потому, что хочу сделать свой следующий проект с использованием этого протокола и хотел потренировать с ним тут. Для GUI по-быстрому погуглил проекты на Rust, посмотрел базовые примеры для них и меня зацепил Azul потому, что там используется Document Object Model и движок стилей похожий на CSS, а я длительное время занимался веб-разработкой. В общем, выбирал Фреймворк субъективно. Он, пока что, в глубокой альфе: не работает скроллинг, не работает фокус ввода, нет курсора. Для того чтобы ввести данные в текстовое поле нужно навести мышку на него и прям над ним держать его пока печатаете. Подробнее ... [10]Собственно, большая часть статьи — это комментарии к коду.
GUI фреймворк, использующий функциональный стиль, DOM, CSS. Ваш интерфейс состоит из корневого элемента, у которого множество потомков, у которых могут быть свои потомки как, например, в HTML и XML. Весь интерфейс создается на основе данных из одной единственной DataModel. В ней передаются в представление вообще все данные. Если кто знаком с ASP.NET, то Azul и его DataModel это как Razor и его ViewModel. Как и в HTML можно привязать функции к событиям DOM элементов. Можно стилизовать элементы с помощью CSS фреймворка. Это не тот же самый CSS что в HTML, но очень на него похож. Так же есть двухсторонняя привязка как в Angular или MVVM в WPF, UWP. Подробнее на сайте https://azul.rs/ [11].
struct ChatService {}
impl ChatService {
//1
fn read_data(socket: &Option<UdpSocket>) -> Option<String> {
//2
let mut buf = [0u8; 4096];
match socket {
Some(s) => {
//3
match s.recv(&mut buf) {
//4
Ok(count) => Some(String::from_utf8(buf[..count].into())
.expect("can't parse to String")),
Err(e) => {
//5
println!("Error {}", e);
None
}
}
}
_ => None,
}
}
//6
fn send_to_socket(message: String, socket: &Option<UdpSocket>) {
match socket {
//7
Some(s) => { s.send(message.as_bytes()).expect("can't send"); }
_ => return,
}
}
}
struct Controller {}
//1
const TIMEOUT_IN_MILLIS: u64 = 2000;
impl Controller {
//2
fn send_pressed(app_state: &mut azul::prelude::AppState<ChatDataModel>, _event: azul::prelude::WindowEvent<ChatDataModel>) -> azul::prelude::UpdateScreen {
//3
let data = app_state.data.lock().unwrap();
//4
let message = data.messaging_model.text_input_state.text.clone();
data.messaging_model.text_input_state.text = "".into();
//5
ChatService::send_to_socket(message, &data.messaging_model.socket);
//6
azul::prelude::UpdateScreen::Redraw
}
//7
fn login_pressed(app_state: &mut azul::prelude::AppState<ChatDataModel>, _event: azul::prelude::WindowEvent<ChatDataModel>) -> azul::prelude::UpdateScreen {
//8
use std::time::Duration;
//9
if let Some(ref _s) = app_state.data.clone().lock().unwrap().messaging_model.socket {
return azul::prelude::UpdateScreen::DontRedraw;
}
//10
app_state.add_task(Controller::read_from_socket_async, &[]);
//11 app_state.add_daemon(azul::prelude::Daemon::unique(azul::prelude::DaemonCallback(Controller::redraw_daemon)));
//12
let mut data = app_state.data.lock().unwrap();
//13
let local_address = format!("127.0.0.1:{}", data.login_model.port_input.text.clone().trim());
//14
let socket = UdpSocket::bind(&local_address)
.expect(format!("can't bind socket to {}", local_address).as_str());
//15
let remote_address = data.login_model.address_input.text.clone().trim().to_string();
//16
socket.connect(&remote_address)
.expect(format!("can't connect to {}", &remote_address).as_str());
//17
socket.set_read_timeout(Some(Duration::from_millis(TIMEOUT_IN_MILLIS)))
.expect("can't set time out to read");
// 18
data.logged_in = true;
// 19
data.messaging_model.socket = Option::Some(socket);
//20
azul::prelude::UpdateScreen::Redraw
}
//21
fn read_from_socket_async(app_data: Arc<Mutex<ChatDataModel>>, _: Arc<()>) {
//22
let socket = Controller::get_socket(app_data.clone());
loop {
//23
if let Some(message) = ChatService::read_data(&socket) {
//24
app_data.modify(|state| {
//25
state.messaging_model.has_new_message = true;
//26
state.messaging_model.messages.push(message);
});
}
}
}
//27
fn redraw_daemon(state: &mut ChatDataModel, _repres: &mut azul::prelude::Apprepres) -> (azul::prelude::UpdateScreen, azul::prelude::TerminateDaemon) {
//28
if state.messaging_model.has_new_message {
state.messaging_model.has_new_message = false;
(azul::prelude::UpdateScreen::Redraw, azul::prelude::TerminateDaemon::Continue)
} else {
(azul::prelude::UpdateScreen::DontRedraw, azul::prelude::TerminateDaemon::Continue)
}
}
//29
fn get_socket(app_data: Arc<Mutex<ChatDataModel>>) -> Option<UdpSocket> {
//30
let ref_model = &(app_data.lock().unwrap().messaging_model.socket);
//31
match ref_model {
Some(s) => Some(s.try_clone().unwrap()),
_ => None
}
}
}
// Problem - blocks UI :(
fn start_connection(app_state: &mut AppState<MyDataModel>, _event: WindowEvent<MyDataModel>) -> UpdateScreen {
//Добавляем асинхронную задачу
app_state.add_task(start_async_task, &[]);
//Добавляем демон
app_state.add_daemon(Daemon::unique(DaemonCallback(start_daemon)));
UpdateScreen::Redraw
}
fn start_daemon(state: &mut MyDataModel, _repres: &mut Apprepres) -> (UpdateScreen, TerminateDaemon) {
//Блокирует UI на десять секунд
thread::sleep(Duration::from_secs(10));
state.counter += 10000;
(UpdateScreen::Redraw, TerminateDaemon::Continue)
}
fn start_async_task(app_data: Arc<Mutex<MyDataModel>>, _: Arc<()>) {
// simulate slow load
app_data.modify(|state| {
//Блокирует UI на десять секунд
thread::sleep(Duration::from_secs(10));
state.counter += 10000;
});
}
Демон всегда выполняется в основном потоке, поэтому там блокировка неизбежна. С асинхронной задачей, если сделать, например, вот так-то никакой блокировки на 10 секунд не будет.
fn start_async_task(app_data: Arc<Mutex<MyDataModel>>, _: Arc<()>) {
//Не блокируем UI. Ожидаем асинхронно.
thread::sleep(Duration::from_secs(10));
app_data.modify(|state| {
state.counter += 10000;
});
}
Функция modify вызывает lock() а мютекс с моделью данных поэтому блокирует обновление интерфейса на время своего выполнения.
const CUSTOM_CSS: &str = "
.row { height: 50px; }
.orange {
background: linear-gradient(to bottom, #f69135, #f37335);
font-color: white;
border-bottom: 1px solid #8d8d8d;
}";
impl azul::prelude::Layout for ChatDataModel {
//1
fn layout(&self, info: azul::prelude::WindowInfo<Self>) -> azul::prelude::Dom<Self> {
//2
if self.logged_in {
self.chat_form(info)
} else {
self.login_form(info)
}
}
}
impl ChatDataModel {
//3
fn login_form(&self, info: azul::prelude::WindowInfo<Self>) -> azul::prelude::Dom<Self> {
//4
let button = azul::widgets::button::Button::with_label("Login")
//5
.dom()
//6
.with_class("row")
//7
.with_class("orange")
//8
.with_callback(
azul::prelude::On::MouseUp,
azul::prelude::Callback(Controller::login_pressed));
//9
let port_label = azul::widgets::label::Label::new("Enter port to listen:")
.dom()
.with_class("row");
//10
let port = azul::widgets::text_input::TextInput::new()
//11
.bind(info.window, &self.login_model.port_input, &self)
.dom(&self.login_model.port_input)
.with_class("row");
// 9
let address_label = azul::widgets::label::Label::new("Enter server address:")
.dom()
.with_class("row");
//10
let address = azul::widgets::text_input::TextInput::new()
//11
.bind(info.window, &self.login_model.address_input, &self)
.dom(&self.login_model.address_input)
.with_class("row");
//12
azul::prelude::Dom::new(azul::prelude::NodeType::Div)
.with_child(port_label)
.with_child(port)
.with_child(address_label)
.with_child(address)
.with_child(button)
}
//13
fn chat_form(&self, info: azul::prelude::WindowInfo<Self>) -> azul::prelude::Dom<Self> {
//14
let button = azul::widgets::button::Button::with_label("Send")
.dom()
.with_class("row")
.with_class("orange")
.with_callback(azul::prelude::On::MouseUp, azul::prelude::Callback(Controller::send_pressed));
//15
let text = azul::widgets::text_input::TextInput::new()
.bind(info.window, &self.messaging_model.text_input_state, &self)
.dom(&self.messaging_model.text_input_state)
.with_class("row");
//12
let mut dom = azul::prelude::Dom::new(azul::prelude::NodeType::Div)
.with_child(text)
.with_child(button);
//16
for i in &self.messaging_model.messages {
dom.add_child(azul::widgets::label::Label::new(i.clone()).dom().with_class("row"));
}
dom
}
}
В документации Azul написано, что в ней должны храниться все данные приложения, в том числе и подключение к базе данных, поэтому я поместил в нее UDP сокет.
//1
#[derive(Debug)]
//2
struct ChatDataModel {
//3
logged_in: bool,
//4
messaging_model: MessagingDataModel,
//5
login_model: LoginDataModel,
}
#[derive(Debug, Default)]
struct LoginDataModel {
//6
port_input: azul::widgets::text_input::TextInputState,
//7
address_input: azul::widgets::text_input::TextInputState,
}
#[derive(Debug)]
struct MessagingDataModel {
//8
text_input_state: azul::widgets::text_input::TextInputState,
//9
messages: Vec<String>,
//10
socket: Option<UdpSocket>,
//11
has_new_message: bool,
}
pub fn run() {
//1
let app = azul::prelude::App::new(ChatDataModel {
logged_in: false,
messaging_model: MessagingDataModel {
text_input_state: azul::widgets::text_input::TextInputState::new(""),
messages: Vec::new(),
socket: None,
has_new_message: false,
},
login_model: LoginDataModel::default(),
}, azul::prelude::AppConfig::default());
// 2
let mut style = azul::prelude::css::native();
//3
style.merge(azul::prelude::css::from_str(CUSTOM_CSS).unwrap());
//4
let window = azul::prelude::Window::new(azul::prelude::WindowCreateOptions::default(), style).unwrap();
//5
app.run(window).unwrap();
}
Здесь у нас обычно консольное приложение.
pub fn run() {
//1
let socket = create_socket();
//2
let (sx, rx) = mpsc::channel();
//3
start_sender_thread(rx, socket.try_clone().unwrap());
loop {
//4
sx.send(read_data(&socket)).unwrap();
}
}
fn start_sender_thread(rx: mpsc::Receiver<(Vec<u8>, SocketAddr)>, socket: UdpSocket) {
//1
thread::spawn(move || {
//2
let mut addresses = Vec::<SocketAddr>::new();
//3
loop {
//4
let (bytes, pre) = rx.recv().unwrap();
// 5
if !addresses.contains(&pre) {
println!(" {} connected to server", pre);
addresses.push(pre.clone());
}
//6
let result = String::from_utf8(bytes)
.expect("can't parse to String")
.trim()
.to_string();
println!("received {} from {}", result, pre);
//7
let message = format!("FROM: {} MESSAGE: {}", pre, result);
let data_to_send = message.as_bytes();
//8
addresses
.iter()
.for_each(|s| {
//9
socket.send_to(data_to_send, s)
//10
.expect(format!("can't send to {}", pre).as_str());
});
}
});
}
const TIMEOUT_IN_MILLIS: u64 = 2000;
fn create_socket() -> UdpSocket {
println!("Enter port to listen");
//1
let local_port: String = read!("{}n");
let local_address = format!("127.0.0.1:{}", local_port.trim());
println!("server address {}", &local_address);
//2
let socket = UdpSocket::bind(&local_address.trim())
.expect(format!("can't bind socket to {}", &local_address).as_str());
//3
socket.set_read_timeout(Some(Duration::from_millis(TIMEOUT_IN_MILLIS)))
.expect("can't set time out to read");
//4
socket
}
fn read_data(socket: &UdpSocket) -> (Vec<u8>, SocketAddr) {
//1
let mut buf = [0u8; 4096];
//2
loop {
match socket.recv_from(&mut buf) {
//3
Ok((count, address)) => {
//4
return (buf[..count].into(), address);
}
//5
Err(e) => {
println!("Error {}", e);
continue;
}
};
}
}
Каждый слой может содержать свои DTO и совершенно произвольные классы с произвольными методами. Главное, чтобы они выполняли функционал, связанный со слоем, в котором они находятся. В простых приложениях некоторые из слоев могут отсутствовать. Например, слой представление может реализоваться через MVC, MVP, MVVM паттерн. Что совершенно не обязательно. Главное, чтобы классы, которые находятся в этом слое реализовали функционал, возложенный на слой. Помните, паттерны и архитектура — это всего лишь рекомендации, а не указания. Паттерн и архитектура — это не закон, это совет.И так, рассмотрим каждый слой на примере стандартного ASP.NET приложения, использующего стандартный Entity Framework.
У нас тут MVC. Это тот слой, который обеспечивает взаимодействие с пользователем. Сюда приходят команды и от сюда получают данные пользователи. Не обязательно люди, если у нас API, то наш пользователь — это другая программа. Машины общаются с машинами.
Тут, обычно, классы именуют Service, например, UserService, хотя может быть вообще, что угодно. Просто набор классов с методами. Главное, чтобы тут происходили вычисления и расчеты нашего приложения. Это самый толстый и громоздкий слой. Тут больше всего кода и различных классов. Это, собственно, и есть наше приложение.
Обычно у нас тут EF реализует паттерны Unit Of Work и Repository. Таки да, DbContext это, можно сказать, Unit Of Work, а ДБ сеты его это Repository. Это, собственно, то место куда мы кладем данные и откуда их берем. Не зависимо от того, источник данных это БД, АПИ другого приложения, Кеш в Памяти или просто какой-то генератор случайных чисел. Любой источник данных.
Да, просто всякие User, Animal и прочее. Одно важное замечание – у них может быть какое-то поведение характерное только для них. Например:
class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName
{
get
{
return FirstName + " " + LastName;
}
}
public bool Equal(User user)
{
return this.FullName == user.FullName;
}
}
using System;
using System.Collections.Generic;
using System.Text;
//Entities
class User
{
public int Id { get; set; }
public string Name { get; set; }
}
//Data Access Layer
class UserRepository
{
private readonly Dictionary<int, User> _db;
public UserRepository()
{
_db = new Dictionary<int, User>();
}
public User Get(int id)
{
return _db[id];
}
public void Save(User user)
{
_db[user.Id] = user;
}
}
//Business Logic Layer
class UserService
{
private readonly UserRepository _repo;
private int _currentId = 0;
public UserService()
{
_repo = new UserRepository();
}
public void AddNew()
{
_currentId++;
var user = new User
{
Id = _currentId,
Name = _currentId.ToString()
};
_repo.Save(user);
}
public string GetAll()
{
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= _currentId; i++)
{
sb.AppendLine($"Id: {i} Name: {_repo.Get(i).Name}");
}
return sb.ToString();
}
}
//presentation Layer aka Application Layer
class UserController
{
private readonly UserService _service;
public UserController()
{
_service = new UserService();
}
public string RunExample()
{
_service.AddNew();
_service.AddNew();
return _service.GetAll();
}
}
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
var controller = new UserController();
Console.WriteLine(controller.RunExample());
Console.ReadLine();
}
}
}
Ну шо, таки хочу сказать спасибо моей Насте за то что исправила грамматические ошибки в статье. Таки да, Настя ты не зря с красным дипломом и вообще классная. Люблю тебя <3.
Автор: VanquisherWinbringer
Источник [13]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/302789
Ссылки в тексте:
[1] Змейка: https://habr.com/post/428830/
[2] Three.rs: https://github.com/three-rs/three
[3] ggez: https://github.com/ggez/ggez
[4] Amethyst: https://github.com/amethyst/amethyst
[5] Azul: https://github.com/maps4print/azul
[6] Conrod: https://github.com/PistonDevelopers/conrod/
[7] Yew: https://github.com/DenisKolodin/yew
[8] Orbtk: https://github.com/redox-os/orbtk
[9] Исходники, работает на Windows 10 x64: https://github.com/VictoremWinbringer/UdpClientServerChat
[10] Подробнее ...: https://github.com/maps4print/azul/issues/75
[11] https://azul.rs/: https://azul.rs/
[12] Видео: https://www.youtube.com/watch?v=n2UrjogA0j0
[13] Источник: https://habr.com/post/433624/?utm_campaign=433624
Нажмите здесь для печати.