Rustifying Chat:用 Rust 创建一个终端 UI 聊天应用
编程世界涌现出许多新的语言和框架, Rust就是其中一种越来越受欢迎的语言。你可能在 Stack Overflow 和专注于编程的 Twitter 账号上都听说过它。
我不会在这里推销 Rust 本身,毕竟, Stack Overflow 上的这个投票已经说明了一切。不过,我确实想通过创建一个 Rust 聊天应用来展示如何将发布/订阅消息机制集成到你的 Rust 项目中。
您可以跟随教程进行操作,也可以在 GitHub 上查看完整的源代码。
什么是发布/订阅模式?
发布/订阅(Publish-Subscribe,简称 Pub/Sub)是一种让服务和客户端实时通信的方式。在 Rust 中,Pub/Sub 的一个常见用例是连接多个设备或服务器,并同时在它们之间同步任何更改或更新。这些有效负载可用于更新系统、发送聊天应用消息、实时地理位置跟踪等等。另一个用途是WebAssembly中的网络通信。WebAssembly 是一种预编译的二进制格式,它将取代浏览器中的 JavaScript。这将使开发人员能够使用 JavaScript 以外的其他语言来创建网站前端。它无需在运行时解释 JavaScript,而是在页面加载之前将其他编程语言编译成二进制文件。
市面上有一些发布/订阅 API,我打算使用PubNub。之所以选择它搭配 Rust 开发,是因为它提供了简洁易用的 REST API。此外,它还提供了预先编写好的开源 API 集成,为我的应用场景提供了坚实的基础架构。
获取免费的 PubNub API 密钥,迈出创建此项目的第一步。
搭建 Rust 环境
刚开始用 Rust 开发时,我发现编译器非常有用。它不仅能运行你的代码,还能告诉你哪里出错了,以及出错的原因,如果你问它,它还会告诉你!让我们 在终端输入命令来安装它。curl https://sh.rustup.rs -sSf | sh
完成后,导航到您希望存放新 Rust 项目的位置并输入cargo new rustychat
现在进入您的项目并使用cargo run以下命令运行您的项目!
现在你已经创建了一个项目,让我们来看看项目中的一些文件。
“Cargo.toml”文件类似于Node.js中的package.json文件。你可以在这里存放所有库(crate),构建项目时它们会被安装。如果你使用Cocoapods安装了iOS框架,那么列出这些依赖项的过程也类似。
在这里,在 dependencies 部分,您应该包含我们的应用程序使用的六个 crate。这些库将使我们能够:
- 发送 GET 请求
- 更轻松地定义自定义错误类型
- 构建和销毁 JSON 对象
- 从 JSON 派生自定义结构体
- 创建自定义终端用户界面
- 对我们的请求进行 URL 编码
[dependencies]
reqwest = "0.9.18"
custom_error = "1.6.0"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
cursive = { version = "0.12.0", default-features = false, features = ["pancurses-backend"] }
percent-encoding = "1.0.1"
现在,你的 Cargo.toml 文件应该包含了我们计划使用的所有 crate。构建项目时,Rust 会自动安装这些依赖项。
下一个文件是 src -> main.rs,这是你的 Rust 主代码文件。这里包含了你的 main 函数,以及 crate 的导入和结构体的定义。
在主函数上方插入以下代码,以便访问您刚刚列出的依赖项 crate。其中包含一些 Rust 自带的 crate。
extern crate reqwest;
use percent_encoding::{percent_encode, PATH_SEGMENT_ENCODE_SET};
use serde::{Deserialize, Serialize};
use std::sync::mpsc::{channel, Sender};
use std::thread;
use cursive::align::HAlign;
use cursive::traits::*;
use cursive::Cursive;
use cursive::view::ScrollStrategy;
use cursive::views::{BoxView, Dialog, DummyView, EditView, LinearLayout, ScrollView, TextView};
use custom_error::custom_error;
如果你再次运行你的代码,它应该会构建所有 crate,然后输出“Hello World”。
创建自定义错误
在代码中执行风险操作时,应该编写错误处理程序来优雅地终止出错的代码执行。这就是我使用`custom_error` 的原因,这是一个可以减少 Rust 中创建错误所需样板代码的 crate。我们定义错误类型,并针对每种预期的错误类型返回相应的描述。
在我们的代码中,我们将把一个对象转换成 JSON 字符串(反之亦然),向一个 URL 发送 GET 请求,并访问返回的响应内容。这三项操作都存在风险,可能会失败,但我们可以进行恢复。一旦检测到错误,就创建一个描述信息输出到终端。
下面列举了一些我所涵盖的错误类型以及我对它们的定义示例。
custom_error! {ChatError
JSONError{source: serde_json::error::Error} = @{
source.to_string()
},
ReqwestError{source: reqwest::Error } = @{
source.to_string().split(": ").collect::<Vec<&str>>()[1]
},
Unknown = "unknown error"
}
定义 Rust JSON 响应的结构体
在本节中,我们将定义几个结构体,它们代表从 PubNub 获取的 JSON 响应。我们将使用四个结构体,分别代表我们收到的响应对象中的一个层。
响应对象包含一个 Time 对象和一个 MessageResp 向量。Time 对象包含一个时间标记字符串,MessageResp 对象包含一个 Message 对象,该对象包含一个 UUID 字符串字段和一个文本字符串字段。这些字段都需要可反序列化。这样我们就可以将 JSON 响应放入一个 Response 结构体中,它会自动处理数据组织。Message 对象也需要可序列化才能转换为 JSON 数据。
#[derive(Deserialize)]
struct Response {
t: Time,
m: Vec<MessageResp>,
}
#[derive(Deserialize)]
struct MessageResp {
d: Message,
}
#[derive(Deserialize)]
struct Time {
t: String,
}
//Message is a sub object of MessageResp
#[derive(Serialize, Deserialize)]
struct Message {
uuid: String,
text: String,
}
Rust 异步编程:如何进行多线程编程
本项目的主要功能是创建两个线程:一个线程用于搜索新消息,另一个线程用于用户界面。首先,我们来实现程序的多线程运行。通过多线程,我们可以阻塞一个线程等待消息,同时用户仍然可以在聊天输入框中输入新消息。
默认情况下,该应用程序在一个线程中逐行运行。我们可以创建一个额外的线程,同时运行订阅循环。在创建这个额外的线程之前,我们如何在主线程上访问它的数据?
这就是通道的作用所在。它创建了一个发送方和一个接收方,也称为生产者和消费者。发送方可以被克隆,但接收方不能,这使得接收方能够按照消息发送的顺序访问它们。
创建两个独立的通道,一个用于从 UI 向订阅线程发送通道,另一个用于从订阅线程向 UI 发送消息。
fn main() {
//We create two channels, one to pass the channel name to the subscribe function
//Another to send new messages from the subscribe function to the UI
let (channel_sender, channel_receiver) = channel();
let (mut msg_sender, msg_receiver) = channel();
//...
//REST OF THE MAIN FUNCTION
}
现在使用`spawn`创建一个线程,这是 Rust 中创建新线程最简单的方法。我们在回调函数前加上关键字 `move`,以便获取通道接收器和消息发送器的所有权。在新线程中,创建一个空字符串,我们将用它来作为初始时间令牌。要订阅,我们需要从 UI 获取一个通道。UI 会在用户提交通道后发送一个,但在此之前,我们应该暂停线程。
使用通道的好处在于我们可以选择等待信息发送。让我们recv()“在接收器上使用“函数”,该函数会等待单个值通过通道传递。
//...
//INSIDE MAIN
//Create a seperate thread, this allows us to have a subscribe loop that wont stop the UI from updating
let _handle1 = thread::spawn(move || {
let mut time_token = "".to_string();
//We wait for the UI to send us the channel name
let test_channel = channel_receiver.recv();
//...
//REST OF THREAD
}
//...
//REST OF MAIN
当我们从另一个线程接收到一个变量时,我们无法判断它是错误还是正常。如果值为“Ok”,则将其解包为字符串。接下来,我们来创建订阅循环!使用“loop”关键字创建一个无限循环。
//...
//INSIDE THREAD
if test_channel.is_ok() {
let channel_name: String = test_channel.unwrap();
loop {
//...
//REST OF LOOP
}
}
下一步是调用我们的订阅函数。它会返回一个包含字符串和 ChatError 类型的枚举值 Result。Result 中包含的值被包裹在“Ok”或“Err”中。如果操作成功,则返回“Ok”;如果出现错误,则返回“Err”。您可以检查结果是成功还是错误,还可以通过解包来查看其具体值。订阅函数会借用时间令牌、消息发送者的可变版本以及频道名称。
//...
//Loop
let result: Result<String, ChatError> = subscribe(&time_token, &mut msg_sender, &channel_name);
我们还没创建这个函数,但我很快会一步步讲解。现在我们只需要知道这个函数可能会返回错误。我们来处理这两种情况。
如果我们的订阅结果顺利完成,“result.is_ok()“应该返回 true”。如果是这样,请解包并将您的时间令牌赋值给它。
if result.is_ok() {
//We update the time_token var to get all messages that happened after that specific time.
time_token = result.ok().unwrap();
}
如果操作失败,暂时不必担心通知用户;这可能是“超时”错误。如果没有收到新消息,这种情况偶尔会发生。
由于 PubNub 的 REST API 使用了HTTP 长轮询,超时意味着没有新的数据发生。如果没有超时,我们应该打印错误信息并跳出循环。这样就终止了程序。循环到此结束。如果我们从订阅函数收到时间令牌或超时错误,则重新开始循环。
else if result.is_err() {
let err = result.unwrap_err();
//If the request times out, thats okay, we just restart it with that same time token, looking for new messages.
if err.to_string() != "timed out" {
println!(
"Error: {:?} \nPlease restart application to try again.",
err.to_string()
);
break;
}
}
//...
//END LOOP
//END IF
//END THREAD
现在我们已经完成了订阅循环,是时候暂停一下主函数了。接下来,我们需要通过实际创建函数来更深入地了解订阅。
PubNub REST API
既然我们已经获得了密钥,那么让我们来学习如何在 Rust 中使用 PubNub 吧。
PubNub 提供 REST API,允许我们仅通过 HTTP GET 请求访问 Pub/Sub 等服务。有关更多信息,请参阅REST API 和 HTTP GET文档。
PubNub REST API 允许开发者使用 HTTP 请求发布消息和订阅频道。通过对请求进行底层控制,您可以决定订阅请求的发送频率以及是否异步发送。在这个聊天应用中,我们会在前一次订阅请求结束后立即发送下一次订阅请求,以便用户仍然可以与应用的其他部分进行交互。
请查看下图,了解我们应用程序中的信息流。
我们的应用会用到发布和订阅 URL,它们各自发挥着不同的作用。我先讲解一下如何订阅,然后我们可以使用一些类似的概念来进行发布。
使用 Rust 和 REST API 订阅频道
本节将指导您如何向 PubNub 创建 Pub/Sub 订阅请求。通过本节内容,我们将更好地理解 PubNub 如何与 Rust 协同工作。我们还将学习如何将数据从该线程发送到用户界面。
函数定义
正如我们之前看到的,我们的函数会借用时间、频道发送者和频道名称。它返回一个 Result 对象,该对象包含一个字符串或一个 ChatError 对象,后者我们在项目开始时就已经定义好了。这使我们能够定义代码中可能出现的问题,以及在每种情况下应该如何处理。
fn subscribe(time: &str, msg_sender: &mut Sender<String>, channel: &str ) -> Result<String, ChatError> {
//...
//Subscribe Function
}
创建 URL 字符串
这些函数的第一步是创建 URL 字符串。为此,我们使用宏函数 `format` 和 crate `percent-encoding`。使用 `format` 可以更轻松地创建包含变量的 URL。`percent-encoding` 包允许我们将对象转换为 URL 编码的字符串。
每个请求中都有一个地方可以填写您的订阅密钥和频道名称。我们会在请求末尾添加一个时间参数,尽管我们并非总是需要它。我们并非总是将时间参数传递给订阅函数。但我们总会在 PubNub 的成功响应中收到时间参数。
//...
//In subscribe
//Format the URL
let url = format!(
"https://{host}/v2/subscribe/{subkey}/{channel}/0/{time}",
host = "ps.pndsn.com",
subkey = "INSERT_SUB_KEY_HERE",
channel = percent_encode(channel.as_bytes(), PATH_SEGMENT_ENCODE_SET),
time = percent_encode(time.as_bytes(), PATH_SEGMENT_ENCODE_SET),
);
调用 reqwest::get 函数
一旦我们获得了想要请求消息的 URL,我们就使用“Reqwest”来调用它。Reqwest 是对 Hyper 的抽象,Hyper 是一个用于发起网络请求的底层 Rust crate。这个函数可能会返回我们想要的信息,但也可能返回错误。这时,函数的 Result 返回类型就发挥作用了。如果出现错误,它将返回一个错误。
let mut resp = reqwest::get(&url)?;
使用 reqwest::get 时,我们在末尾留一个问号。这意味着如果此调用失败,订阅函数将返回一个“Err”值。这样,我们的函数就无需在内部自定义错误信息并返回“Err”值,而是会自动执行此操作。
成功响应 – JSON 转对象
如果响应状态为成功,我们就可以深入分析接收到的内容了。我们在文件开头定义了一些结构体,它们将帮助我们访问其中的信息!我们打包的一个 crate 让反序列化过程变得非常轻松。我们使用 serde_json 将响应文本转换为易于访问的对象。这个过程也可能失败,但我们已经使用 ChatError 处理了这种错误。
if resp.status().is_success() {
let deserialized: Response = serde_json::from_str( & resp.text()?).unwrap();
//...
//Rest of If
}
有时我们可能收不到响应中的某些消息,所以我们需要遍历响应中的 Vector。对于每条消息,我们将使用 Sender 传递消息信息。我将从每条消息中获取的两个值(UUID 和文本)创建一个字符串。之后请务必解包该语句。遍历完所有消息后,我们可以返回收到的新时间,并以“Ok”响应的形式返回。
//...
//In if statement
for m in deserialized.m {
//Send the new message to the UI above.
msg_sender
.send(format!("{} : {}", m.d.uuid, m.d.text))
.unwrap();
}
return Ok(deserialized.t.t);
//End of if
如果响应失败,则在订阅函数末尾添加一个“Ok”结果。该结果应为传入的原始时间,该时间会被转换为字符串。
Ok(time.to_string())
//End of subscribe
错误
你注意到我们没有添加任何 Err 语句吗?我们所有的错误都由 ChatError 处理。我们确实添加了一些已填充的“Ok”语句,这允许我们将一些信息传递回调用函数,并告诉它“嘿,一切顺利!”
在 Rust 中发布消息
创建完订阅函数后,我们再创建一个发布到 PubNub 的函数。这个函数与上一个函数格式基本相同,但也有一些细微的差别。
函数定义
此函数接受三个字符串作为参数:要发送的文本、发送者的 UUID 和频道名称。与之前的函数一样,我们返回一个 Result 对象,但这次返回的是空的“Ok”或 ChatError Err。
fn publish(text: String, uuid: String, channel: String) -> Result<(), ChatError> {
//...
//PUBLISH FUNCTION
}
对象转 JSON
在这个函数的开头,我们创建一个包含几个已提供参数的消息对象。我们将执行与订阅函数相反的操作,但这次我们将对象转换为 JSON 字符串。这可能会返回错误,因此请在字符串末尾添加一个问号,ChatError 函数会处理它。
//...
//In publish
let message = Message { uuid, text };
let m_json = serde_json::to_string(&message)?;
创建 URL 字符串
这与订阅功能非常相似,只是添加和替换了一些参数。将“subscribe”替换为“publish”,包含您的发布密钥,并将时间替换为消息。频道和消息都需要进行 URL 编码。
let url = format!(
"https://{host}/publish/{pubkey}/{subkey}/0/{channel}/0/{message}",
host = "ps.pndsn.com",
pubkey = "INSERT_PUB_KEY_HERE",
subkey = "INSERT_SUB_KEY_HERE",
channel = percent_encode(channel.as_bytes(), PATH_SEGMENT_ENCODE_SET),
message = percent_encode(m_json.as_bytes(), PATH_SEGMENT_ENCODE_SET),
);
创建 GET 请求
发起一个 GET 请求,并在请求后添加一个问号。在下一行添加“Ok”结果。完成这些步骤后,我们的发布功能就完成了!
let _resp = reqwest::get(&url)?;
Ok(())
//End of publish
使用 Cursive 创建终端用户界面
什么是TUI?
我们已经学习了如何使用 PubNub 的 REST API,并且创建了一个单独的线程来长轮询 PubNub 以获取新消息。目前,我们会在频道中查找消息,并将这些新消息发送到某个地方。我们还没有定义从哪里获取频道以及新消息发送到哪里。这两点都与用户界面密切相关。
虽然可以创建基于命令行的聊天,但使用终端用户界面 (TUI) 能带来更简洁流畅的体验。我使用的是 Cursive,它是一款简单易用且功能丰富的 TUI,可以将输入和输出分离。
频道和用户名输入
要启动 Cursive,您需要创建一个可变的默认函数实例。
//Below end of _handle1 thread
let mut siv = Cursive::default();
Cursive 使用的格式与其他 UI 创建方法类似,即使用视图(View)。视图可以包含其他视图,并具有不同的用途。最底层的视图由图层(Layer)构成。这些图层就像可以添加或弹出的窗口。
创建一个图层,并向其传递一个对话框。对话框是一种可以容纳其他视图的窗口。它应该位于一个 LinearLayout 视图周围,我们可以将其设置为垂直方向。LinearLayout 非常适合将子视图组织成堆栈或行。您可以使用 `addChild`child“函数向其中添加子视图,并且可以链式调用任意次数。您还可以使用 `dive` 函数with“动态添加子视图。我们稍后会用到它,但现在,让我们先在 LinearLayout 内部设计连接层。
siv.add_layer(
Dialog::around(
LinearLayout::vertical()
//...
//LinearLayout's children
)
//...
//Title and buttons of Dialog
);
我们将使用几种不同的视图来创建这个图层。我们使用 TextView 来显示短文本行,例如标签。EditView 允许用户输入信息,我们稍后可以通过特定的 ID 访问这些信息。DummyView 可以用来分隔其他视图。您可以根据自己的需求设计这些视图,但我将 TextView 居中放置,并将 EditView 的宽度设置为 20。除了设置样式之外,请确保在每个 EditView 的 `<head>` 标签之后new()“、`<body>` 标签之前设置 ID fixed_wifth(20)“。如果 ID 不在这个位置,我们之后将无法引用该值。DummyView 可以用来分隔项目。
//...
//LinearLayout's children
.child(DummyView.fixed_height(1))
.child(TextView::new("Enter Username").h_align(HAlign::Center))
.child(EditView::new().with_id("username").fixed_width(20))
.child(DummyView.fixed_height(1))
.child(TextView::new("Enter Channel").h_align(HAlign::Center))
.child(EditView::new().with_id("channel").fixed_width(20))
LinearLayout 完成后,继续创建对话框。给对话框添加标题,创建一个带有回调函数的“确定”按钮和一个退出按钮。最后,将对话框居中对齐。接下来我们将讲解“确定”按钮的回调函数。
//...
//Attacched to Dialog
.title("PubNub Chat")
.button("Okay", CALLBACK )
.button("Quit", | s | s.quit())
.h_align(HAlign::Center),
连接到频道
在“确定”按钮内部,我们可以提供一个回调函数。当用户点击或按下回车键时,该回调函数就会运行。我们move“再次使用“关键字”来赋予该回调函数对其所需所有变量的所有权。
.button("Okay", move | s | {
//...
//Okay callback
}
让我们获取用户之前在 EditViews 中输入的值。
//Inside Okay
let channel = s
.call_on_id("channel", |view: &mut EditView| view.get_content())
.unwrap();
let username = s
.call_on_id("username", |view: &mut EditView| view.get_content())
.unwrap();
检查用户名是否为空,如果为空,则创建一个图层提示用户输入用户名。如果用户名不为空,则检查通道名称是否为空。如果为空,则将其默认值设置为“global”。
//Checking if username input is empty.
if username.is_empty() {
s.add_layer(Dialog::info("Please enter a username!".to_string()));
} else {
let new_channel =
if channel.is_empty() {
"global".to_string()
} else {
channel.to_string()
};
//...
//The rest of connecting to PubNub
}
继续执行之前的 else 语句,我们将拥有的通道(可以是“全局”通道或用户自定义通道)发送到订阅循环。完成此操作后,循环即可继续运行,并从 PubNub 请求消息。在加载下一个屏幕之前,弹出初始层。
channel_sender.send(new_channel).unwrap();
s.pop_layer();
创建聊天层
现在创建另一个图层,这是一个固定尺寸为 40x20 的 BoxView。在这个 BoxView 内放置一个带有标题的对话框,对话框包含一个内容字段。对话框将居中对齐,并带有“发送”和“退出”按钮。
//...
//Still in else statement
s.add_layer(BoxView::with_fixed_size((40, 20),
Dialog::new()
.title("PubNub Chat")
.content(
//...
//Chat view: Includes list of messages and EditView
)
.h_align(HAlign::Center)
.button("Send", CALLBACK)
.button("Quit", | s | s.quit()),
))
//End of UI design
我首先会演示如何设计内容,然后再演示如何发送消息。在对话框的内容函数中,插入一个 LinearLayout。这样做是为了将一个 ScrollView 和一个 EditView 堆叠在一起。在 ScrollView 内部,我们插入另一个 LinearLayout。这个额外的 LinearLayout 是为了方便我们在需要时删除多余的行。有些视图不提供此功能。当收到新消息时,使用 `.` 使 ScrollView 固定在底部 scroll_strategy。
//Inside Content
LinearLayout::vertical()
.child(
ScrollView::new(
LinearLayout::vertical()
//Children of the LinearLayout
)
.scroll_strategy(ScrollStrategy::StickToBottom),
)
//Next Child
在这个第二个 LinearLayout 内部,我们在顶部和底部放置了 DummyView,with子视图位于它们之间。它提供了对 LinearLayout 的引用,我们可以动态地添加子视图。添加一些 DummyView,我选择了 13 个,这个数字与我们的图层高度很匹配。我们添加这些空行,是为了当有新消息到达时,它们首先显示在底部。将 LinearLayout 的 ID 设置为“messages”或你想要的任何名称。
//Children of inner LinearLayout
.child(DummyView.fixed_height(1))
//Add in a certain amount of dummy views, to make the new messages appear at the bottom
.with( | messages | {
for _ in 0. .13 {
messages.add_child(DummyView.fixed_height(1));
}
})
.child(DummyView.fixed_height(1))
.with_id("messages"),
在第一个 LinearLayout 的末尾添加一个 ID 为“message”的 EditView 子元素。
//Child of first LinearLayout
.child(EditView::new().with_id("message")),
//End of content
发布消息
现在我们已经设计好了聊天视图,接下来让我们来发送一些消息!就像上一层中的“确定”按钮一样,我们需要一个回调函数,并将所有必需的变量都移到其中。
.button("Send", move |s| {
//...
//Rest of the callback
}
像之前一样,从 EditView 中获取消息,并检查它是否为空。如果为空,则提示用户输入消息。如果不为空,则像之前一样检查用户输入的频道是否为空或已满。
//Inside callback
let message = s
.call_on_id("message", |view: &mut EditView| view.get_content())
.unwrap();
if message.is_empty() {
s.add_layer(Dialog::info("Please enter a message!".to_string()))
} else {
let new_channel_2 = if channel.is_empty() {
"global".to_string()
} else {
channel.to_string()
};
//...
//Will handlee publishing messages
}
//End of callback
调用我们的发布函数,传入用户输入的消息、用户名和频道名称。如果发布失败,则告知用户发布过程中出现错误。如果发布成功,则清空编辑视图中的文本。
//In the else statement
let result = publish(message.to_string(), username.to_string(), new_channel_2 );
if result.is_err() {
//If there was an error then we say that there is one, and don't do anything.
s.add_layer(Dialog::info("Error Publishing".to_string()));
} else {
//Clear out the EditView.
s.call_on_id("message", |view: &mut EditView| {
view.set_content("")
}).unwrap();
}
//End of else + callback
在用户界面中插入新消息
通常情况下,我们会使用命令“siv.run()“启动用户界面”,但由于我们需要控制用户界面何时更新,所以我们改用“” siv.refresh()“。创建一个新变量来统计收到的消息数量,并在更新后刷新用户界面。
//...
//After creating the UI design/layers
let mut message_count = 0;
siv.refresh();
创建一个循环,首先调用“step”函数来遍历 UI 事件循环。检查此时 Cursive 是否正在运行,如果未运行,则跳出循环。创建一个变量来跟踪是否需要刷新 UI,并将其设置为 false。
loop {
siv.step();
if !siv.is_running() {
break;
}
let mut needs_refresh = false;
//...
//Handle new messages
}
现在,最后一个要求是检查通道队列中是否有消息等待接收。我们可以使用非阻塞方法来msg_receiver“检查是否有消息等待接收。如果有消息,它将返回一个迭代器。我们可以使用 Rust 的 for 循环来遍历这些消息。
for m in msg_receiver.try_iter() {
//...
//Adding each message "m" in
}
在这个 for 循环中,我们访问 ID 为“messages”的 LinearLayout。然后,我们可以将刷新布尔值设置为 true,消息计数加一,并将消息作为子元素添加到 LinearLayout 中。在这个循环中,你还可以移除第一个子元素。这样做是为了避免在消息超出屏幕范围之前出现滚动条!请务必检查消息计数是否小于或等于 DummyView 的数量加一。
//Inside of for loop
siv.call_on_id("messages", |messages: &mut LinearLayout| {
needs_refresh = true;
message_count += 1;
messages.add_child(TextView::new(m));
if message_count <= 14 {
messages.remove_child(0);
}
});
如果在循环结束时刷新布尔值为真,那么我们就可以刷新用户界面并显示更改。
if needs_refresh {
siv.refresh();
}
//End of loop
//End of Main
完成主要功能后,聊天应用即告结束。如果您cargo run在终端中输入该命令,将会弹出一个用户界面,要求您输入用户名和频道名称。输入至少一个用户名后,您就可以连接到您输入的频道,或者默认连接到“全局”频道。如果您输入消息并点击“发送”,消息将显示在您的屏幕上。您可以打开多个命令行窗口并在它们之间进行聊天,或者与其他拥有相同按键权限的用户聊天。
后续步骤
在本教程中,我们用 Rust 创建了一个终端聊天应用。它使用 PubNub 的 Pub/Sub 机制,通过 Cursive 终端界面发送和接收消息。这只是 Rust 和 PubNub 功能的一个示例。如果您正在使用 Rust 计算算法或运行游戏,并且需要进行一些全球范围内的 I/O 操作,PubNub 可以帮到您。
想尝试其他语言吗?PubNub 拥有超过75 个 SDK和数百个教程。
文章来源:https://dev.to/sambajahlo/rustifying-chat-create-a-terminal-ui-chat-app-in-rust-5bno



