» Rust: Make Web Chat App with Socket.IO » 2. Development » 2.2 Contacts

Contacts

Use type UserStore = HashMap<String, User> (map of user structs) to store all the users in the memory.

You need to use databases for large-scale projects.

Add serde to serialize/deserialize data:

cargo add serde

Update src/main.rs:

@@ -1,20 +1,66 @@
-use socketioxide::{extract::SocketRef, SocketIo};
+use std::collections::HashMap;
+
+use serde::{Deserialize, Serialize};
+use socketioxide::{
+    extract::{Data, SocketRef, State},
+    SocketIo,
+};
+use tokio::{net::TcpListener, sync::RwLock};
 use tower::ServiceBuilder;
 use tower_http::cors::CorsLayer;
 
 const PORT: i32 = 4000;
 
+#[derive(Serialize, Deserialize, Debug, Clone)]
+struct User {
+    name: String,
+    emoji: String,
+    sid: Option<String>,
+}
+
+type UserStore = HashMap<String, User>;
+
+async fn on_connect(s: SocketRef) {
+    println!("A user connected {}", s.id);
+
+    s.on(
+        "user-join",
+        |s: SocketRef, Data::<User>(user), users: State<RwLock<UserStore>>| async move {
+            if user.name == "" {
+                return;
+            }
+            println!("User {} => {} {} joined", s.id, user.emoji, user.name);
+            let new_user = User {
+                name: user.name,
+                emoji: user.emoji,
+                sid: Some(s.id.to_string()),
+            };
+            let mut binding = users.write().await;
+            binding.insert(s.id.to_string(), new_user);
+            let items: Vec<(String, User)> = binding
+                .iter()
+                .map(|(k, v)| (k.clone(), v.clone()))
+                .collect();
+            // If you provide array-like data (tuple, vec, arrays), it will be considered as multiple arguments.
+            // Therefore if you want to send an array as the first argument of the payload, you need to wrap it in an array or a tuple.
+            s.broadcast().emit("contacts", (items.clone(),)).ok();
+            s.emit("contacts", (items,)).ok(); // Emit to itself as well
+        },
+    )
+}
+
 #[tokio::main]
 async fn main() -> Result<(), Box<dyn std::error::Error>> {
-    let (layer, io) = SocketIo::builder().build_layer();
-    io.ns("/", |s: SocketRef| println!("A user connected {}", s.id));
+    let start_users: RwLock<UserStore> = HashMap::new().into();
+    let (layer, io) = SocketIo::builder().with_state(start_users).build_layer();
+    io.ns("/", on_connect);
 
     let app = axum::Router::new().layer(
         ServiceBuilder::new()
             .layer(CorsLayer::permissive())
             .layer(layer),
     );
-    let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", PORT))
+    let listener = TcpListener::bind(format!("0.0.0.0:{}", PORT))
         .await
         .unwrap();
     println!("Chat server serving at localhost:{}", PORT);
  • .with_state(...) makes the HashMap<String, User> a global state for all socket handlers.
  • items: Vec<(String, User)> is a vector of tuples, which is easier for the js client to parse as a js Map.
  • s.broadcast().emit(...) will broadcast the latest contact list to all the users except the sender socket itself.
PrevNext