Rust Day 7: Tokio — Simple TCP Server
Hurray! We have completed 1 week with RUST :)
Today, I plan to cover building a simple tcp_server using tokio.
RUST CODE:
Previously we used use tokio::io::AsyncWriteExt;
. This time we are using both use tokio::io::{AsyncReadExt, AsyncWriteExt};
as we are planning to read and write using the socket.
AsyncReadExt
is an extension trait (interface) of AsyncRead
from futures
crate.
- Instead of TcpStream, we are using
use tokio::net::TcpListener;
- We are also using
use std::env;
to read command-line arguments. let addr = env::args()
.nth(1)
.unwrap_or_else(|| “127.0.0.1:8080”.to_string());
We use this nth(x) to fetch the 1st (ie nth) argument from the args.
5. let listener = TcpListener::bind(&addr).await?;
: Used to bind to the address in the machine.
Going back to my old java HTTP server example,
The standard syntax involves
- binding a port address
- Run an infinite loop.
socket.accept()
wouldwait()
thethread
ormain thread
until a new connection request is established. - When a new incoming connection request comes,
socket.accept()
would return a socket that will have an input stream and an output stream to communicate (io) with the client. Note that, we would get input stream and output stream as Sockets arebidirectional
. - The new request can be handled in a new thread, without halting the listening.
6. loop{}
is used similar to while(true){}
7. let (mut socket, _) = listener.accept().await?;
is used to accept a new inbound connection.
8. tokio::spawn( Task )
is used to start a new thread ie async task.
Instead of spawning a new task, we can also make it sequential. Here the socket established is passed on to the process function.
Or similar to our original code, we can try
9. move
: converts any variables captured by reference or mutable reference to variables captured by value. move
is often used when threads are involved.
In our case, it is used mainly to pass the ownership of the socket to the inside thread.
Without Move:
We get the output as 3 ie the length of the vector.
With Move:
The error occurs as the value is not existential after the move.
10. vec![0; 1024];
This syntax is used to declare a vector of size 1024 initializing all the values as 0.
From SO:
11. In the new thread that we have now, we will continuously read from the socket and write the same content back to the socket.
let n = socket
.read(&mut buf)
.await
.expect("failed to read data from socket");
n
denotes how many bytes were read.
read(&mut buf)
read content into the buffer vector.
12. .expect(“error message”)
From SO:
I can “expect” a result to be
Ok
, I can "expect" an Optional value to beSome
. I can also write in test, where I "expect" a web request to be successful. I would writedo_request().expect("Expected request to be successful")
. This gives me a clear and concise message of what went wrong instead of having to go through the stacktrace when usingunwrap
.
13. Writing it back:
socket
.write_all(&buf[0..n])
.await
.expect("failed to write data to socket");
We are writing the buf
value from 0 to n (ie all the read values) and sending it back to the client.
Conclusion:
This was a relatively simpler session. In the next session, I plan to cover tinydb
using tokio, which uses Concurrent HashMap.
Found it Interesting?
Please show your support by 👏.