学习Rust:我如何与Azul进行UDP聊天



我一直在学习Rust。 我还是不太了解,所以我犯了很多错误。 上一次我尝试制作Snake游戏。 我尝试过循环,收集,与3D Three.rs一起工作。 了解有关ggezAmethyst的信息 。 这次,我尝试使客户端和服务器聊天。 对于GUI使用Azul 。 还观看了康罗德紫杉Orbtk 。 我尝试了多线程,渠道和网络。 我考虑了上一篇文章的错误,并尝试对此进行更详细的介绍。 有关详细信息,欢迎猫。

来源,适用于Windows 10 x64

对于网络,我使用UDP是因为我想使用该协议创建下一个项目,并且希望在此处进行培训。 对于GUI,我迅速在Rust上松了项目,查看了它们的基本示例,Azul迷上了我,因为它使用了文档对象模型和类似CSS的样式引擎,并且从事了很长时间的Web开发。 总的来说,我是主观选择框架的。 到目前为止,它是处于深层次的alpha中:滚动不起作用,输入焦点不起作用,没有光标。 为了在文本字段中输入数据,您需要将鼠标悬停在其上并在键入时将其保持在其正上方。 更多细节...

实际上,大多数文章都是代码注释。

阿祖尔


使用功能样式,DOM,CSS的GUI框架。 您的界面包含一个根元素,该元素具有许多后代,这些后代可以具有自己的后代,例如HTML和XML。 整个接口是基于来自单个DataModel的数据创建的。 其中,所有数据通常都传输到演示文稿中。 如果有人熟悉ASP.NET,则Azul及其数据模型就像Razor及其ViewModel。 与在HTML中一样,您可以将函数绑定到DOM元素的事件。 您可以使用CSS框架设置元素的样式。 这与HTML的CSS不同,但是非常相似。 在WPF,UWP中,还有Angular或MVVM中的双向绑定。 有关该网站的更多信息。

其余框架的简要概述


  • Orbtk-几乎与Azul相同,也为深Alpha
  • Conrod- 视频您可以创建跨平台的桌面应用程序。
  • Yew是WebAssembly,类似于React。 用于Web开发。

顾客


分组用于读写套接字的辅助功能的结构


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, } } } 

  1. 从套接字读取数据
  2. 用于从套接字读取数据的缓冲区。
  3. 阻止通话。 在这里,执行线程停止,直到读取数据或发生超时。
  4. 我们从字节数组中以UTF8编码获取字符串。
  5. 如果连接被超时中断或发生了其他错误,我们将到达此处。
  6. 将字符串发送到套接字。
  7. 将字符串转换为UTF8编码的字节,然后将数据发送到套接字。 将数据写入套接字不会阻塞,即 执行线程将继续其工作。 如果无法发送数据,则我们会以“无法发送”消息中断程序。

一种对功能进行分组的结构,用于处理来自用户的事件并修改我们的DataModel


 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 } } } 

  1. 超时(以毫秒为单位),此后从套接字读取的阻塞操作将被中断。
  2. 当用户想要向服务器发送新消息时,该功能实现。
  3. 我们在数据模型中拥有互斥量。 这将阻塞接口重绘线程,直到释放互斥锁为止。
  4. 我们复制用户输入的文本以进一步传输它,并清除文本输入字段。
  5. 我们正在发送一条消息。
  6. 我们通知框架,在处理此事件后,我们需要重绘接口。
  7. 当用户想要连接到服务器时,该功能起作用。
  8. 我们连接结构以表示标准库中的时间长度。
  9. 如果我们已经连接到服务器,则我们将中断该函数的执行,并告知Framework无需重绘该接口。
  10. 从Azul Framework的线程池中添加将在线程中异步执行的任务。 使用数据模型访问互斥锁会阻止更新UI,直到释放互斥锁为止。
  11. 添加在主线程中运行的重复任务。 接口更新将阻止此守护程序中的任何冗长的计算。
  12. 我们进入了互斥锁。
  13. 我们读取用户输入的端口,并基于该端口创建本地地址,我们将进行监听。
  14. 创建一个UDP套接字,以读取到达本地地址的数据包。
  15. 我们读取用户输入的服务器地址。
  16. 我们告诉UDP套接字仅从该服务器读取数据包。
  17. 设置从套接字读取操作的超时。 写入套接字的过程无需等待,也就是说,我们只写数据,什么都不期待,并且从套接字进行的读取操作会阻塞流并等待,直到可以读取的数据到达为止。 如果未设置超时,则从套接字进行的读取操作将无限期等待。
  18. 设置一个标志,指示用户已经连接到服务器。
  19. 我们将创建的套接字传递给数据模型。
  20. 我们通知框架,在处理此事件后,我们需要重绘接口。
  21. 在Azul Framework的线程池中运行的异步操作。
  22. 从我们的数据模型中获取套接字的副本。
  23. 尝试从套接字读取数据。 如果您不复制套接字,而是直接在此处等待直到消息从我们数据模型中互斥锁中的套接字收到,那么整个接口将停止更新,直到我们释放互斥锁为止。
  24. 如果我们收到某种消息,则更改数据模型,Modify的作用与lock()。Unwrap()相同,将结果传递给lambda并在lambda代码结束后释放互斥量。
  25. 设置一个标志以指示我们有一条新消息。
  26. 将消息添加到所有聊天消息的数组。
  27. 在主线程中运行的重复同步操作。
  28. 如果有新消息,则通知框架我们需要从头开始重新绘制接口并继续使用此守护程序;否则,我们不会从头开始绘制接口,而是在下一个周期中仍然调用此Function。
  29. 创建套接字的副本,以免互斥锁被数据模型锁定。
  30. 我们获得互斥锁并获得到套接字的链接。
  31. 创建套接字的副本。 退出功能时,互斥锁将自动释放。

Azul中的异步数据处理和守护程序


 // 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; }); } 

修改函数将调用lock(),并且具有数据模型的互斥锁因此会阻止在接口执行期间更新接口。

我们的风格


 const CUSTOM_CSS: &str = " .row { height: 50px; } .orange { background: linear-gradient(to bottom, #f69135, #f37335); font-color: white; border-bottom: 1px solid #8d8d8d; }"; 

实际上,用于创建要显示给用户的DOM的函数


 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 } } 

  1. 创建最终DOM的函数,每次需要重绘接口时都会调用该函数。
  2. 如果我们已经连接到服务器,则显示用于发送和读取消息的表格,否则,我们显示用于连接至服务器的表格。
  3. 创建用于输入连接服务器所需数据的表单。
  4. 创建一个带有文本题名“登录”的按钮。
  5. 将其转换为DOM对象。
  6. 向其添加行类。
  7. 将css类添加为橙色。
  8. 添加事件处理程序以单击按钮。
  9. 创建带有文本的文本标签以显示给用户和CSS类行。
  10. 我们创建一个文本框,用于输入来自模型属性和CSS类行的文本。
  11. 将文本字段绑定到我们的DataModel的属性。 这是两种方式的绑定。 现在,编辑TextInput会自动更改模型属性中的文本,反之亦然。 如果我们更改模型中的文本,则TextInput中的文本将更改。
  12. 我们创建一个根DOM元素,在其中放置U​​I元素。
  13. 创建用于发送和阅读消息的表单。
  14. 单击创建带有文本“ Send”的按钮和带有类“ row”,“ orange”的css和事件处理程序。
  15. 我们使用模型属性self.messaging_model.text_input_state和具有类“ row”的css创建具有双向绑定的文本输入字段。
  16. 添加文本标签,以显示聊天记录中写的消息。

我们存储接口状态的模型


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, } 

  1. 这将使我们能够以{:?}形式的模板中的字符串形式显示结构。
  2. 我们的数据模型。 以便可以在Azul中使用。 她必须实现Layout特性。
  3. 用于检查用户是否已连接到服务器的标志。
  4. 用于显示用于向服务器发送消息并保存从服务器接收的消息的表单的模型。
  5. 用于显示用于连接到服务器的表单的模型。
  6. 用户输入的端口。 我们将使用套接字监听它。
  7. 用户输入的服务器地址。 我们将连接到它。
  8. 用户消息。 我们将其发送到服务器。
  9. 来自服务器的消息数组。
  10. 我们通过它与服务器通信的套接字。
  11. 用于检查是否有新消息从服务器到达的标志。

最后,是应用程序的主要入口点。 从GUI绘图和用户输入处理开始一个周期


 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(); } 

  1. 我们用开始数据创建一个应用程序。
  2. 默认情况下,应用程序使用的样式。
  3. 给它们添加我们自己的样式。
  4. 我们创建一个窗口,在其中显示我们的应用程序。
  5. 在此窗口中启动应用程序。

伺服器


应用程序的主要入口点


在这里,我们通常有一个控制台应用程序。

 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(); } } 

  1. 创建一个套接字。
  2. 我们创建一个带有一个sx消息发送者和许多rx接收者的单向通道。
  3. 我们开始在单独的流中向所有收件人发送邮件。
  4. 我们从套接字读取数据并将其发送到流,该流将消息发送到连接到服务器的客户端。

创建用于向客户端发送消息的流的功能


 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()); }); } }); } 

  1. 启动一个新线程。 move表示变量分别接管lambda和流量。 更具体地说,我们的新线程将“吸收” rx和socket变量。
  2. 客户与我们联系的地址集合。 我们将向他们发送所有消息。 通常,在实际项目中,有必要进行处理,以使客户端与我们断开连接并从该阵列中删除其地址。
  3. 我们开始一个无限循环。
  4. 我们从通道读取数据。 在这里,流将被阻塞,直到新数据到达为止。
  5. 如果我们的数组中没有这样的地址,则在其中添加它。
  6. 从字节数组中解码UTF8字符串。
  7. 我们创建一个字节数组,该字节数组将发送给所有客户。
  8. 我们会收集地址并将数据发送给每个人。
  9. 对UDP套接字的写操作是非阻塞的,因此此处函数将不等待消息到达接收者并几乎立即执行。
  10. 期望在发生错误的情况下使用给定的消息紧急退出程序。

该函数根据用户输入创建套接字


 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 } 

  1. 我们读取服务器将侦听的端口,并基于该端口创建本地服务器地址。
  2. 创建一个监听此地址的UDP套接字。
  3. 设置读取操作的超时。 读取操作正在阻塞,它将阻塞流,直到新数据到达或发生超时。
  4. 我们从函数返回创建的套接字。
  5. 该函数从套接字读取数据,并将其与发送者地址一起返回。

从套接字读取数据的功能


 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; } }; } } 

  1. 缓冲区是我们将读取数据的地方。
  2. 启动一个循环,直到读取有效数据为止。
  3. 我们得到读取的字节数和发送者地址。
  4. 我们将数组从其开头切割为读取的字节数,然后将其转换为字节向量。
  5. 如果发生超时或其他错误,请继续执行循环的下一个迭代。

关于应用程序中的图层


题外话:6月2日上班的小型教育计划。 我决定把它放在这里,也许有人会派上用场。 6月的尖兵是C#中的例子,我们正在谈论ASP.NET
因此,无事可做,就在晚上,我决定为Artem和Victor写一个关于建筑的小型教育程序。 好吧,走吧。

实际上,我在这里添加内容是因为模式恢复,我每周只能写一次文章,而且材料已经存在,下周我想将其他内容上传到Habr。

通常,应用程序是分层的。 在每一层中,都有实现其所在层的行为特征的对象。 等等。 这些是图层。

  1. 表示层。
  2. 分层业务逻辑。
  3. 数据访问层。
  4. 实体(用户,动物等)


每个层可以包含其自己的DTO和具有任意方法的完全任意类。 最主要的是它们执行与它们所在的图层关联的功能。 在简单的应用程序中,某些层可能会丢失。 例如,可以通过MVC,MVP,MVVM模式实现层视图。 这是完全可选的。 最主要的是,该层中的类实现了分配给该层的功能。 请记住,模式和体系结构只是建议,而不是方向。 模式和体系结构不是法律,这是建议。

因此,我们将考虑使用标准实体框架的标准ASP.NET应用程序示例中的每一层。

表示层


我们这里有MVC。 这是提供用户交互的层。 命令在这里,用户从这里获取数据。 不一定是人,如果我们有一个API,那么我们的用户就是一个不同的程序。 汽车与汽车沟通。

业务逻辑层


在这里,通常将类称为Service,例如UserService,尽管它可以是任何东西。 只是一组带有方法的类。 最主要的是我们的应用程序的计算和计算都在这里进行。 这是最厚最笨重的层。 这里有大多数代码和各种类。 实际上,这就是我们的应用程序。

资料存取层


通常,EF在这里实现工作单元和存储库模式。 是的,DbContext是工作单元,并且DB将其设置为存储库。 实际上,这是我们放置数据和从中获取数据的地方。 不管数据源是数据库,另一个应用程序的API,内存中的缓存还是仅仅是某种随机数生成器。 任何数据源。

实体


是的,只有各种各样的用户,动物等等。 重要的一点-他们可能仅具有某种行为特征。 例如:

 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(); } } } 


聚苯乙烯


好吧,我要感谢Nastya纠正本文中的语法错误。所以,是的,纳斯佳(Nastya)拥有红色文凭并没有变得很酷。我爱你<3。

Source: https://habr.com/ru/post/zh-CN433624/


All Articles