如果您希望构建既快速又可靠的实时聊天应用程序,请考虑使用 Rust 和 React。Rust 以其速度和可靠性着称,而React 是最流行的用于构建用户界面的前端框架之一。
在本文中,我们将演示如何使用 Rust 和 React 构建一个实时聊天应用程序,该应用程序提供聊天、检查用户状态和指示用户何时输入的功能。我们将使用 WebSockets 启用双向客户端-服务器通信。
跳跃前进:
实时聊天应用简介
WebSocket 简介
入门
设计实时聊天应用架构
在 Rust 中构建 WebSocket 服务器创建路线处理用户会话
使用 SQLite 准备数据库生成模式创建结构设置查询通过电话号码查找用户添加新用户查找聊天室和参与者
使用 React 构建客户端 UI头像组件登录组件房间组件会话组件使用Websocket Hook使用LocalStorage Hook使用会话挂钩
构建聊天应用程序
实时聊天应用简介
实时聊天应用程序允许用户通过文本、语音或视频彼此实时交流。这种类型的应用程序允许比其他类型的通信(例如电子邮件或 IM)更即时的消息传递。
聊天应用程序必须实时工作有几个原因:
改进的性能:更直接的通信允许更自然的对话
更强的响应能力:实时功能可改善用户体验
卓越的可靠性:通过实时功能,消息丢失或延迟的机会更少
WebSocket 简介
WebSockets 在实时聊天应用程序中启用客户端和服务器之间的双向通信。使用 Rust 构建 WebSocket 服务器将使服务器能够处理大量连接而不会降低速度。这是由于 Rust 的速度和可靠性。
现在我们对 WebSockets 有了更好的了解,让我们开始构建我们的实时聊天应用程序吧!
入门
首先,让我们回顾一些先决条件:
Rust:确保您的计算机上安装了 Rust。如果没有,请使用以下命令安装它:卷曲-原型'=https' - tlsv1 。2 - sSf https : //sh.rustup.rs | sh // 如果你在 Windows 中,请在此处查看更多安装方法 https : //forge.rust-lang.org/infra/other-installation-methods.html
React:确保您的环境已准备好进行 React 开发;如果您还没有安装 React,请使用以下命令之一来安装它:// 在苹果机上 酿造安装节点 // 在 linux nvm install v14上。10 .0 // 在 Windows 上,您可以在此处下载 nodejs 安装程序 https : //nodejs.org/en/download/
接下来,运行以下命令来验证所有内容是否已安装并正常工作:
rustc——版本_
货物——版本
节点——版本
npm——版本_
设计实时聊天应用架构
让我们为我们的实时聊天应用程序创建一些设计架构。我们将构建一个简单的服务器;我们的应用程序架构将涵盖以下功能:
聊天:两个用户之间通过直接消息传递
打字指示器:当用户开始向他们输入聊天内容时通知收件人
用户状态:表示用户在线还是离线
实时聊天应用系统架构。
此架构非常简单且易于遵循。它仅由几个组件组成:
WebSocket 服务器:这是我们应用程序中最重要的组件;它处理客户和房间之间的所有通信
房间管理器:该组件负责管理我们应用程序中的所有房间。它将创建、更新和删除房间。该组件将位于 HTTP 服务器上
用户管理器:该组件负责管理我们应用程序中的所有用户。它将创建、更新和删除用户。该组件也将位于 HTTP 服务器上
消息管理器:该组件负责管理我们应用程序中的所有消息。它将创建、更新和删除消息。这个组件一将在 WebSocket 服务器和 HTTP 服务器上。它将用于存储来自 WebSockets 的传入消息,并在用户通过 Rest API 打开聊天室时检索数据库中已有的所有消息
在 Rust 中构建 WebSocket 服务器
我们可以使用许多包在 Rust 中编写 WebSocket 服务器。对于本教程,我们将使用Actix Web;它是一个成熟的软件包并且易于使用。
首先,使用以下命令创建一个 Rust 项目:
cargo new rust -反应-聊天
接下来,将这个包添加到文件中:Cargo.toml
[ package ]
name = "rust-react-chat"
version = "0.1.0"
edition = "2021"
[依赖项]
actix = “0.13.0”
actix - files = “0.6.2”
actix - web = “4.2.1”
actix - web - actors = “4.1.0”
rand = “0.8.5”
serde = “1.0 .147"
serde_json = "1.0.88"
现在,安装diesel_cli;我们将使用它作为我们的 ORM:
cargo install diesel_cli -- no - default - features -- features sqlite
项目的结构应该如下所示:
. ├──货物。lock
├── Cargo . toml
├──自述文件。md
├──聊天。分贝
├── . 环境
└──源
├──数据库. rs
├──主要。rs
├──模型. rs
├──路线. rs
├──架构. rs
├──服务器. rs
└──会话. rs
└──
静态└──用户界面
现在,这里有一些关于文件夹的信息:
src:此文件夹包含我们所有的 Rust 代码
static:此文件夹包含我们所有的静态资产、HTML 文件、JavaScript 文件和图像
ui:这个文件夹包含我们的 React 代码;我们稍后将其编译为static文件并将其导出到static文件夹
接下来,让我们编写 WebSocket 服务器的入口点:
// src/main.rs #[ macro_use ] extern crate diesel ;
使用 actix ::*;
使用 actix_cors :: Cors ;
使用 actix_files ::文件;
使用 actix_web ::{ web , http , App , HttpServer };
使用 diesel ::{ prelude ::*, r2d2 ::{ self , ConnectionManager }, };
模组数据库;
模组模型;
模组路线;
模式架构;
模组服务器;
模组会话;#[ actix_web :: main ] async fn main () -> std :: io :: Result <()> { let server = server :: ChatServer :: new (). 开始();让conn_spec = "chat.db" ; 让manager = ConnectionManager :: < SqliteConnection > :: new ( conn_spec );
let pool = r2d2::Pool::builder().build(manager).expect("Failed to create pool.");
let server_addr = "127.0.0.1";
let server_port = 8080;
let app = HttpServer::new(move || {
let cors = Cors::default()
.allowed_origin("http://localhost:3000")
.allowed_origin("http://localhost:8080")
.allowed_methods(vec!["GET", "POST"])
.allowed_headers(vec![http::header::AUTHORIZATION, http::header::ACCEPT])
.allowed_header(http::header::CONTENT_TYPE)
.max_age(3600);
App::new()
.app_data(web::Data::new(server.clone()))
.app_data(web::Data::new(pool.clone()))
.wrap(cors)
.service(web::resource("/").to(routes::index))
.route("/ws", web::get().to(routes::chat_server))
.service(routes::create_user)
.service(routes::get_user_by_id)
.service(routes::get_user_by_phone)
.service(routes::get_conversation_by_id)
.service(routes::get_rooms)
.service(Files::new( "/" , "./static" )) }) 。工人( 2 ) 。绑定((服务器地址,服务器端口))?. 运行();
println !( "服务器运行在 http://{server_addr}:{server_port}/" );
应用程序。等待}
以下是有关我们正在使用的软件包的一些信息:
actix_cors: 将用于调试 UI;我们将接受来自或的POST 和 GET 请求localhost:3000localhost:8080
actix_web:对于 Actix Web 包中所有与 HTTP 相关的功能
actix_files:用于将静态文件嵌入到我们的路由之一
diesel:将用于从我们的 SQLite 数据库中查询数据。如果您愿意,可以将其更改为 Postgres 或 MySQL
serde_json:将用于解析我们将发送到 React 应用程序的 JSON 数据
创建路线
现在让我们为我们的服务器创建路由。由于我们将使用 REST HTTP 和 WebSocket 服务器,我们可以轻松地将所有内容放在一个文件中。
首先,添加我们需要的所有包:
// src/routes.rs
使用 std :: time :: Instant ;
使用 actix ::*;
使用 actix_files :: NamedFile ;
使用 actix_web ::{ get , post , web , Error , HttpRequest , HttpResponse , Responder };
使用 actix_web_actors :: ws ;
使用 diesel ::{ prelude ::*, r2d2 ::{ self , ConnectionManager }, };
使用 serde_json ::
JSON ;
使用 uuid :: Uuid ;
使用 crate :: db ;
使用板条箱::模型;
使用箱子::服务器;
使用 crate :: session ;
输入DbPool = r2d2 :: Pool < ConnectionManager < SqliteConnection >>;
然后,添加一个用于将主页嵌入到根 URL 的路由:
// src/routes.rs
pub async fn index () -> impl Responder { NamedFile :: open_async ( "./static/index.html" ). 等待。展开()}
这是我们的 WebSocket 服务器的入口点。现在它在路线上,但您可以将其更改为您喜欢的任何路线名称。由于我们已经在文件中注册了我们需要的所有依赖项,我们可以将依赖项传递给函数参数,如下所示:/wsmain.rs
// src/routes.rs
pub async fn chat_server (
req : HttpRequest , stream : web :: Payload , pool : web :: Data < DbPool >, srv : web :: Data < Addr < server :: ChatServer >>, ) ->结果< HttpResponse ,错误> { ws ::开始(
会话::
WsChatSession { id : 0 , hb : Instant :: now (), room : "main" . to_string (),名称:无,地址:srv 。get_ref ()。克隆(), db_pool :池, }, & req ,
溪流
) }
接下来,我们需要向我们的路由添加一个 REST API,以便获取必要的数据来使我们的聊天正常进行:
// src/routes.rs #[ post ( "/users/create" )]
pub async fn create_user (
pool : web :: Data < DbPool >, form : web :: Json < models :: NewUser >, ) ->结果< HttpResponse ,错误> { let user = web :: block ( move || { let mut conn =
游泳池。得到()?;
db :: insert_new_user (& mut conn , & form . username , & form . phone ) }) 。等待?. map_err ( actix_web :: error :: ErrorUnprocessableEntity )?; 好的( HttpResponse :: Ok () .json ( user )) } #[ get ( "/users/{user_id}" )]
pub
async fn get_user_by_id (
pool : web :: Data < DbPool >, id : web :: Path < Uuid >, ) ->结果< HttpResponse , Error > {让user_id = id 。to_owned (); let user = web :: block (移动|| { let mut conn = pool . get
()?;
db::find_user_by_uid(&mut conn, user_id)
})
.await?
.map_err(actix_web::error::ErrorInternalServerError)?;
if let Some(user) = user {
Ok(HttpResponse::Ok().json(user))
} else {
let res = HttpResponse::NotFound().body(
json!({
"error": 404,
"message": format!("No user found with phone: {id}")
})
.to_string(),
);
Ok(res)
}
}
#[get("/conversations/{uid}")]
pub async fn get_conversation_by_id(
pool: web::Data<DbPool>,
uid: web::Path<Uuid>,
) -> Result<HttpResponse, Error> {
let room_id = uid.to_owned();
let conversations = web::block(move || {
let mut conn = pool.get()?;
db::get_conversation_by_room_uid(&mut conn, room_id)
})
.await?
.map_err(actix_web::error::ErrorInternalServerError)?;