Building a chat server in Rust

March 21, 2023

In this blog post, we’ll explore how to develop a simple chat server in Rust. We’ll be using Rust’s standard library and the Tokio library to build an asynchronous chat server that allows multiple users to join and chat in real-time. Let’s get started!

Step 1: Setting up the project

To begin, we need to create a new Rust project. We can use Cargo, Rust’s package manager, to do this. Open a terminal and run the following command:

cargo new rust-chat-server
cd rust-chat-server

This will create a new Rust project called “rust-chat-server” and change the current directory to the project directory.

Step 2: Adding dependencies

Next, we need to add the dependencies we’ll be using in our project. We’ll be using the Tokio library to build an asynchronous chat server, so we need to add it to our Cargo.toml file:

[dependencies]
tokio = { version = "1", features = ["full"] }

This tells Cargo to download and include the Tokio library in our project.

Step 3: Writing the server code

Now that we have our project set up and dependencies added, we can start writing the server code. Create a new file called src/main.rs and add the following code:

use std::collections::HashMap;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{mpsc, Mutex};
use std::sync::Arc;

#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
    println!("Server listening on port 8080");

    let (tx, mut rx) = mpsc::channel::<String>(10);
    let connections = Arc::new(Mutex::new(HashMap::new()));

    tokio::spawn(async move {
        loop {
            let message = rx.recv().await.unwrap();
            let connections = connections.lock().await;
            for (_, sender) in connections.iter() {
                sender.send(message.clone()).await.unwrap();
            }
        }
    });

    loop {
        let (mut socket, _) = listener.accept().await.unwrap();
        let connections = Arc::clone(&connections);
        let tx = tx.clone();

        tokio::spawn(async move {
            let (reader, mut writer) = socket.split();
            let mut reader = BufReader::new(reader);
            let mut username = String::new();
            let mut welcome_message = String::new();

            writer.write(b"Enter your username: ").await.unwrap();
            reader.read_line(&mut username).await.unwrap();
            let username = username.trim().to_owned();
            welcome_message.push_str("Welcome, ");
            welcome_message.push_str(&username);
            welcome_message.push_str("!\n");

            {
                let mut connections = connections.lock().await;
                connections.insert(username.clone(), tx);
            }

            tx.send(welcome_message.clone()).await.unwrap();

            let mut line = String::new();
            loop {
                line.clear();
                if reader.read_line(&mut line).await.unwrap() == 0 {
                    break;
                }

                let message = format!("{}: {}", username, line.trim());

                tx.send(message.clone()).await.unwrap();
            }

            {
                let mut connections = connections.lock().await;
                connections.remove(&username);
            }

            let message = format!("{} has left the chat.\n", username);
            tx.send(message).await.unwrap();
        });
    }
}

Let’s break down what this code does.

First, we create a TcpListener instance to listen for incoming connections on port 8080. We also create an mpsc channel that will be used to send messages from connected clients to all other connected clients. We also create a HashMap to store the connections of all connected clients.

Next, we spawn a new task that listens for incoming messages on the mpsc channel. Whenever a new message is received, the task iterates over all connected clients and sends the message to each of them.

After that, we enter into a loop that listens for incoming connections. Whenever a new client connects, we spawn a new task to handle that connection. In this task, we split the incoming socket into a reader and a writer, create a BufReader to read lines from the socket, and prompt the user to enter their username.

Once the user enters their username, we add their connection to the HashMap of connections and send a welcome message to the client. We then enter into a loop that reads lines from the socket and sends them to all other connected clients.

When the client disconnects, we remove their connection from the HashMap and send a message to all other connected clients that the user has left the chat.

Step 4: Testing the server

Now that we have our server code written, let’s test it out. Open two or more terminals and run the following command in each of them:

cargo run

This will start the chat server on port 8080. Once the server is running, open a telnet client in each terminal by running the following command:

telnet 127.0.0.1 8080

This will connect to the chat server on port 8080. Enter a username when prompted, and start chatting!

Congratulations, you’ve just built a simple chat server in Rust using the Tokio library! This example demonstrates the power of Rust’s asynchronous programming capabilities and the ease of use of the Tokio library.

Copyright (c) 2018, all rights reserved.