Tutorial: Chat Application (Client-Server) in Rust

Tutorial: Chat Application (Client-Server) in Rust

ยท

10 min read

Hello, amazing people and welcome back to my blog! Today we're going to learn how to make a simple chat application in Rust. The application will have two parts: 1. Client, 2.Server. You'll be able to type something on the client side and the server will receive it.

Server

Create a folder with two Rust projects, one folder will be the client and the other one the server.

On the server folder, and in my terminal I'm going to type cargo new and open my code editor. In the .toml file you don't have to change anything so let's start with importing stuff in the .main.

We're going to need standard io with the ErrorKind, which is an error message type, then we're going to need the Read and Write . We want to bring in standard net TcpListener, this will allow us to create our server and listen on a port. We also want to bring in our standard sync mpsc, which will allow us to spawn a channel. Finally, we want to bring in standard thread so that we can work with multiple threads.

use std::io::{ErrorKind, Read, Write};
use std::net::TcpListener;
use std::sync::mpsc;
use std::thread;

Next, we want to create two constants. The first one is called LOCAL and it will have our local host with a port in it and the second constant is called MSG_SIZE and this will be the buffer size of our messages, we want our messages to be at most 32 bit in size (of course you can experiment with it, and make it longer if you like).

const LOCAL: &str = "127.0.0.1:6000";
const MSG_SIZE: usize = 32;

Main Function

Inside the main() let's instantiate our server by saying let server = TcpListener::bind(LOCAL).expect("Listener failed to bind");. If it fails to bind, then we're going to return this message "Listener failed to bind" inside of a panic. Next, we want to push our server into what is called non-blocking mode server.set_nonblocking(true).expect("failed to initialize non-blocking"); and if this fails, then we're going to print out this string "failed to initialize non-blocking" inside of a panic. The non-blocking mode basically lets our server constantly check for messages.

fn main() {
    let server = TcpListener::bind(LOCAL).expect("Listener failed to bind");
    server.set_nonblocking(true).expect("failed to initialize non-blocking");

We want to create a mutable vector called clients that will allow us to put all of our clients, that way we can have multiple clients connecting to the server at once rather than just one or two. Then we want to instantiate our channel and assign it to a string type.

let mut clients = vec![];
let (tx, rx) = mpsc::channel::<String>();

Let's create a loop and inside of it let's add if let Ok((mut socket, addr)) = server.accept(). The server.accept is what allows us to accept connections to the server. If we get an okay, then it worked out.

if let Ok((mut socket, addr)) = server.accept() {
   println!("Client {} connected", addr);

Then we want to clone our tx or transmitter, we want to take our socket, try to clone it, and then push it into our clients vector. If this comes back and fails, then we're going to panic. The reason we're cloning our socket is so that we can push it into our thread.

let tx = tx.clone();
clients.push(socket.try_clone().expect("failed to clone client"));

Then we want to spawn our thread with a move closure inside of it and the first thing we do inside this move closure is to create another loop. We want to create a mutable buffer, which will be a vector with zeros inside of it with the message size.

        thread::spawn(move || loop {
                let mut buff = vec![0; MSG_SIZE];

                match socket.read_exact(&mut buff) {
                    Ok(_) => {
                        let msg = buff.into_iter().take_while(|&x| x != 0).collect::<Vec<_>>();
                        let msg = String::from_utf8(msg).expect("Invalid utf8 message");

                        println!("{}: {:?}", addr, msg);
                        tx.send(msg).expect("failed to send msg to rx");
                    }, 
                    Err(ref err) if err.kind() == ErrorKind::WouldBlock => (),
                    Err(_) => {
                        println!("closing connection with: {}", addr);
                        break;
                    }
                }

                sleep();
            });

We want to match socket.read_exact(&mut buff) and this will read our message into our buffer. We're going to assign msg to our buff.into_iter() . We're taking the message that we received, converting it into an iterator, and taking all of the characters that are not white space and collecting them inside our vector. Finally, we want to print out the address sent the message.

We're going to send our message through our transmitter to our receiver and if this fails, then we're going to panic and send back "failed to send msg to rx".

We're also going to check the actual error inside of our error and if err.kind() == ErrorKind::WouldBlock, is equal to an error that would block our non-blocking, then we want to send back a unit type. Otherwise, we want to check for another error and if we get an error, we don't care about what's inside of it, we simply say "closing connection with: {}" our client. Finally, break out of the loop.

Sleep Function

Alright, so we can leave the code as it is, but the problem is that our thread would be constantly looping around and it would be awkward. Let's create something that will allow our loop to sort of rest while it's not receiving messages. To do so, we have a new function called sleep which will allow our thread to sleep for a moment, and we can call it passing the time duration. (As you saw above it we call it from main().)

fn sleep() {
    thread::sleep(::std::time::Duration::from_millis(100));
}

Back to main():

We want to do another if let Ok(msg) = rx.try_recv(), this is for when our server receives a message through the channel. We want to collect all of the messages that we get through our channel and clients = clients.into_iter().filter_map(|mut client| , so our mutable vector set the buffer equal to msg.clone().into_bytes();. We're going to convert our messages into bytes, and then resize that buffer based on our message size, and finally, we're going to take our client and we're going to write_all of the entire buffer.

Last but not least, we're going to map it into our client and send it back. Then, we're going to collect it all into a vector.

This is all the code that we need for our server!

        if let Ok(msg) = rx.try_recv() {
            clients = clients.into_iter().filter_map(|mut client| {
                let mut buff = msg.clone().into_bytes();
                buff.resize(MSG_SIZE, 0);

                client.write_all(&buff).map(|_| client).ok()
            }).collect::<Vec<_>>();
        }

Client

Let's go to our client folder. Once again in the .toml file, we don't need any dependencies, but we do need to import a few things in the main.rs. Firstly, from the std::io we're importing self because we want to import the IO library itself and then we are going to import ErrorKind, Read and Write. We're going to import net::Tcp Stream , sync::mpsc::{self, TryRecvError} , thread and finally the standard library time Duration.

use std::io::{self, ErrorKind, Read, Write};
use std::net::TcpStream;
use std::sync::mpsc::{self, TryRecvError};
use std::thread;
use std::time::Duration;

Again, we want 2 constants here similar to the main.rs ones from the server side.

const LOCAL: &str = "127.0.0.1:6000";
const MSG_SIZE: usize = 32;

Main Function

Let's start working on the main(). We want to create a mutable client, which is a TcpStream and then we're going to connect it to LOCAL . If it doesn't work, we're going to panic and say "Stream failed to connect". We also want our client to be nonblocking so we're going to set the flag nonblocking to true. If it fails, then we're going to panic with "failed to initiate non-blocking".

fn main() {
    let mut client = TcpStream::connect(LOCAL).expect("Stream failed to connect");
    client.set_nonblocking(true).expect("failed to initiate non-blocking");

Next, we want to instantiate our channel, for that we're going to be passing strings (like we did in the server).

let (tx, rx) = mpsc::channel::<String>();

    thread::spawn(move || loop {
        let mut buff = vec![0; MSG_SIZE];
        match client.read_exact(&mut buff) {
            Ok(_) => {
                let msg = buff.into_iter().take_while(|&x| x != 0).collect::<Vec<_>>();
                println!("message recv {:?}", msg);
            },
            .
.
.
.

We want to spawn our thread and we want to create a move closure inside of it with a loop. We're going to create a mutable buffer with a vector with zeros and the message size. Then we want to match on client.read_exact(&mut buff), and read our message through the buffer. If we get back an Ok(_), we want to say let msg = buff.into_iter().take_while(|&x| x != 0).collect::<Vec<_>>(); which turns it into an iterator, and checks if the references inside of it is equal to zero. Then we're going to collect all of them inside of our vector (all the ones that are equal to zero, are going to be discarded).

        Err(ref err) if err.kind() == ErrorKind::WouldBlock => (),
            Err(_) => {
                println!("connection with server was severed");
                break;
            }
        }

Then we want to print out the message we received. Same as we did earlier we want to check if the err.kind is ErrorKind::WouldBlock, and then we're going to send back a unit type if it is. If we get another type of error, we're just going to break out of our loop. Before that, we're going to print println!("connection with server was severed");.

        match rx.try_recv() {
            Ok(msg) => {
                let mut buff = msg.clone().into_bytes();
                buff.resize(MSG_SIZE, 0);
                client.write_all(&buff).expect("writing to socket failed");
                println!("message sent {:?}", msg);
            }, 
            Err(TryRecvError::Empty) => (),
            Err(TryRecvError::Disconnected) => break
        }

        thread::sleep(Duration::from_millis(100));

Then we want to match rx.try_recv() so we want to see if the server sends back a message that says that it got the message from the client. If we do get back that message as an okay, then we want to clone it into bytes and put it inside of a buff variable like this: let mut buff = msg.clone().into_bytes();. We also want to resize our buffer by our message size and we want to write all of our buffers into our client client.write_all(&buff), If they fail, we're going to say expect("writing to socket failed");. Otherwise, we're going to print out that we sent the message and the message itself "message sent {:?}", msg.

We also want to check if Err(TryRecvError::Empty) => (), is empty and if it is, then we're just going to send back a unit type. Then we want to check if it's a disconnected type Err(TryRecvError::Disconnected) => break, in which case we want to break the loop.

Then like we did before, we want to have our thread sleep for a 100 milliseconds.

    println!("Write a Message:");
    loop {
        let mut buff = String::new();
        io::stdin().read_line(&mut buff).expect("reading from stdin failed");
        let msg = buff.trim().to_string();
        if msg == ":quit" || tx.send(msg).is_err() {break}
    }
    println!("bye bye!");

Last but not least, outside of our thread and everything, we want to create a print statement. We will ask the user to Write a message: and we'll create a new mutable string inside a loop. We want to read into that string from our standard input so essentially when the user types in something from the console, we want to read that and have it as a string. We're doing all of this in a loop so that the user can type multiple messages back to back. We also want to take our buffer, trim it and then use the to_string method to put it into a message variable: let msg = buff.trim().to_string();. Then we'll say if msg == ":quit" || tx.send(msg).is_err() {break}, we'll break out of our loop and we'll print out bye bye!.

That's all! If you're still with me, we did it! Now, let's run it. ๐Ÿƒโ€โ™‚๏ธ

How to Run it

To run this program you need to open 2 terminals. One for the client and one for the server.

  • In the server run cargo run.

  • Then do the same for your client. And this time you should see a message, write a message.

  • Type something and then you should see that in the server as well as the bytes received.

  • For example if you typed "hello", you should see in the server side "hello" in bytes too. For longer messages, it will cut them off at 32 bytes.

  • If you type :quit then the program will quit on the client and the server will say closing connection and the client name.

You can find all the code on GitHub.

https://github.com/EleftheriaBatsou/chat-app-client-server-rust/

Happy Rust Coding! ๐Ÿคž๐Ÿฆ€


๐Ÿ‘‹ Hello, I'm Eleftheria, Community Manager, developer, public speaker, and content creator.

๐Ÿฅฐ If you liked this article, consider sharing it.

๐Ÿ”— All links | X | LinkedIn

Did you find this article valuable?

Support Eleftheria Batsou by becoming a sponsor. Any amount is appreciated!

ย