Introduction
Rust is a low level language, but you can choose your level of low. You certainly don't need to implement TCP from scratch. The standard library affords several tools that make your networking journey easy to start. In this quick tour, we're going to:
- learn what elements from the standard library we can use
- learn how to make a host, which consists of an address and port
- learn how to handle errors, at least in a rudimentary way
- understand where to go to build out from this foundation
Let's go!
The Code
Here is the code, then I'll break it down step by step:
// src/main.rs
// dependencies
use std::net::{Ipv4Addr, TcpListener};
// main function
fn main() {
// create an IPv4 address
let address = Ipv4Addr::new(127, 0, 0, 1);
// create a port, per the TCP spec it must be a 16-bit unsigned integer
let port = 8000;
// create a listener and give it the address and port, the compiler will infer that we want a u16 type for the port
let listener = match TcpListener::bind((address, port)) {
Ok(ltnr) => ltnr,
Err(e) => panic!(
"Unable to obtain a listener on which to receive incoming connections: {}",
e
),
};
// start a loop and accept incoming connections, printing a success message to stdout or an error to stderr
for stream in listener.incoming() {
match stream {
Ok(_stream) => {
println!("Connection established!");
}
Err(e) => eprintln!("Connection failed: {}", e),
}
}
}
Alright, what are we doing here.
- bring into scope some dependencies from the standard library, namely:
- the struct type Ipv4Addr, gives us tools to create an IpV4 address
- the TcpListener type
- we the start up a main function and create an Ipv4 address which we bind to a variable called
address
- we need a port, with the value 8000, and bind to a variable called
port
- armed with an address and port that can listen for things, we create a
listener
by using thebind()
method available for theTcplistener
type, thebind
method takes our address and port variables as a tuple - the bind method can fail, so we match on it
- success (the
Ok
arm of the match statement) binds the returned listener to a variable calledltnr
- the error path (the
Err
arm of the match statement) causes the program to panic with an error message. In reality, you'll want to handle errors potentially more gracefully, although there's something to be said from just quitting, because with out a listener to receive data, there's not much point in continuing
- success (the
- now that we have something to listen on, we start up a loop and use the
incoming()
method on our listener, which accepts connections in a blocking manner - we bind the incoming data to a variable called
stream
- we have to match here again, because the
incoming()
method could fail- success (the
Ok
arm) we just print out a message confirming we've established a connnection - the error path (the
Err
arm) we print out a message tostderr
giving a reason why the connection failed
- success (the
Where to Go Next
This program we've created isn't particularly useful. Yes, we accept incoming connections and we somewhat handle errors, but we don't do anything with the information coming to us. Where we can we go from here?
Well, we can build out some functions which take the stream of data and do something with it. In a future article, I'll illustrate how to do that.
I alluded above to the fact this code is blocking. This means every request will block the next until the first is fully processed. You can extend this code by creating a simple event loop, using the tokio
crate, to handle connections in an asynchronous manner. Another option is to implement a thread pool, as done in the multi-threaded web server project from The Rust Book.
Conclusion
Thanks for reading, I hope you see now that networking in Rust is easy to get started. The standard library affords just enough so that we're not needing to implement a TCP stack from scratch.
If you have any feedback for me on this piece, feel free to sound off by send me a comment through the form on my contact page.