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 sayclosing connection
and theclient name
.
You can find all the code on GitHub.
Chat application in Rust
This is a simple chat application in Rust. The application has two parts: 1. Client, 2.Server You can type something on the client side and the server will receive 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.
…
Happy Rust Coding! 🤞🦀
👋 Hello, I'm Eleftheria, Community Manager, developer, public speaker, and content creator.
🥰 If you liked this article, consider sharing it.
Top comments (0)