Proxy server in Rust (part 2)
Let’s listen over TCP
This is how a draft of the main source file looks like (explanations below):
// main.rs
use std::net;
const PROXY_PORT: u16 = 4000;
fn main() {
let listener = net::TcpListener::bind(("127.0.0.1", PROXY_PORT)).unwrap();
match listener.accept() {
Ok((sock, _)) => handle_connection(sock),
Err(e) => panic!("Error while accepting connection: {}", e),
}
}
fn handle_connection(tcp: net::TcpStream) {
println!("Opened connection: {:?}", tcp)
}
Listening
We’re going to use the std::net
crate (you can think of crates as libraries):
use std::net;
std::net::TcpListener::bind
function is used here to start listening on port 4000 of the localhost.
const PROXY_PORT: u16 = 4000;
let listener = net::TcpListener::bind(("127.0.0.1", PROXY_PORT)).unwrap();
u16
corresponds to uint16
known from other languages, so const PROXY_PORT: u16 = 4000;
is a definition of a PROXY_PORT
constant 16-bit integer equal to
4000.
What about the mysterious unwrap()
at the end? Rust is a language designed
with safety with minimal runtime overhead in mind. How is this achieved in this
case? bind()
could’ve simply returned TcpListener
, but instead it returns
std::io::Result<TcpListener>
.
What’s the difference?
Something may go wrong while trying to bind the socket (i.e. the port can be already in use). This can be handled in many different ways (all with different trade-offs):
- throwing an exception (Java, C++?),
- returning a pointer (C, C++),
- returning two values
(TcpListener, bool)
(Go), std::optional
(C++17).
Throwing an exception does not make the programmer handle it. Returning a
pointer or a bool
value does not help here either. std::optional
can be
simply ignored using *
. Rust tries to follow a different path. Instead of the
abovementioned solutions, an object is returned in a wrapping (Result
). This
type can be one of the two: the expected value of type TcpListener
or an error
(Error
)!
Since binding a socket to a port is vital for this program to run, the only
thing I do here is unwrap()
the result.
What does this method do? If there is no error - the value is returned. If an
error happened, panic!
is
called (similar to panic
known from Go).
Accepting a connection
match listener.accept() {
Ok((sock, _)) => handle_connection(sock),
Err(e) => panic!("Error while accepting connection: {}", e),
}
If everything went smoothly, call handle_connection(sock)
which will care take
of the rest.
If not, panic!
with an appropriate error message.
Pattern matching (match
)
match
is a language construct used mostly in functional languages (like
OCaml, Haskell, Lisp) rather than imperative ones (C, Python, Java, C++) and
that’s why I’d like to say a few words about it.
Pattern matching is used for:
- checking whether the object is what we think it is (in the above code, whether
it’s
Ok
(a value) orErr
(an error)) - dissecting it (
Ok
is here made of two parts, the first one is the TCP socket, the second is the address; I’m ignoring the adress (using_
), but binding thesock
variable to the socket).
For example, if we were to write a simple calculator based on trees of arithmetic expressions, a part of code might have looked like this:
match expression {
Add(x, y) => x + y,
Sub(x, y) => x - y,
Mul(x, y) => x * y,
Div(x, y) => x / y,
}
This language construct is possible thanks to types that are an disjunction of different possible values. In this case it means that the expression can be i.e. an addition or substraction. The information about the kind of expression is stored and retrieved at runtime. What’s important is the fact that the compiler can check whether we’ve covered all the possible kinds (and warn us if we forget about one).
We could’ve simulated it in C++ in a following way:
enum Type { Add, Sub, Mul, Div };
struct Expression {
Type type;
union {
Add add;
Sub sub;
Mul mul;
Div div;
} expr;
};
switch expr.type {
case Type::Add: return expr.add.x + expr.add.y; break;
case Type::Sub: return expr.add.x - expr.add.y; break;
case Type::Mul: return expr.add.x * expr.add.y; break;
case Type::Div: return expr.add.x / expr.add.y; break;
}
As you can see, pattern matching is pretty convenient. In the languages that support it natively, the implementation is better than what I have shown here in C++. Unfortunately, I don’t know enough about Rust to talk about its internals in this case.
panic!
panic!
is a macro (right now we can think of it as a function, but !
in Rust
is an indicator of a macro call) used for critical program errors.
panic!
receives an argument list similar to printf
known from other
programming languages, and {}
is used to print the values of any type (not
exactly, more on that later).
What’s coming up?
In the next posts I plan to talk about:
- handling HTTP requests in different threads
- ownership and borrowing (the main characteristic of Rust that differentiates it from popular modern programming languages)
- creating structures
- methods
- traits
- expressions and statements
- and many, many more.