Looking at Swift and some of its more Rust-like features, e.g., tuples, reminded me that its been over a year since I last looked at Rust.
In that time its gone from version 0.07 to 0.11 and there have been lots of changes including the disappearance of all the socket functions.
In their place we have the structs
-
TcpListener
-
TcpAcceptor
-
TcpStream
These are all in the std::io::net::tcp
module.
Listening For Connections
To listen for incoming connections you call the static method
TcpListener::bind
passing it an address and a port, like so (this and the other code snippets are all from examples given in the documentation)
...
let listener = TcpListener::bind("127.0.0.1", 80);
...
This creates a TcpListener
instance and binds it to the given address and port.
You can then listen for incoming connections by calling the listen
method
...
let mut acceptor = listener.listen();
...
This creates a TcpAcceptor
instance which can be used to accept incoming connections
Accepting Connections
Once you have a TcpAcceptor
you can accept a single connection by calling the method accept
.
Alternatively, you can call the method incoming
which returns an iterator which can be used to loop over incoming connections as they are made.
...
for stream in acceptor.incoming() {
match stream {
Err(e) => { /* connection failed */ }
Ok(stream) => spawn(proc() {
// connection succeeded
handle_client(stream)
})
}
}
...
The IoResult<T> Type And IO Traits
There is one thing about the example given in the documentation which is at first glance rather puzzling.
The method
TcpListener::bind
is declared to return
IOResult<TcpListener>
The type IoResult
is declared as
type IoResult<T> = Result<T, IoError>
and
Result<T, E>
is a variant enum.
So the method TcpListener::bind
actually returns an enum and yet in the example the method listen
is called directly on the return value, i.e. the enum.
The same is true for the method listen which is defined to return
IOResult<TcpAcceptor>
So why does the code even compile ?
The answer is that IoResult
implements all the methods defined for the traits
Acceptor
Listener
Reader
Seek
Writer
For example, this is the definition of the IOResult
implementation of the Listener
trait
impl<T, A: Acceptor<T>, L: Listener<T, A>> Listener<T, A> for IoResult<L> {
fn listen(self) -> IoResult<A> {
match self {
Ok(listener) => listener.listen(),
Err(e) => Err(e),
}
}
}
which can be found in the file
$(RUST_SRC)/src/libstd/io/result.rs
As you can see if the IOResult
contains a Listener
the result of calling the method listen on it is returned, otherwise the IOResult
must contain an error and that is returned.
Whilest not having to explicitly unwrap the result of each method which returns an IOResult
is certainly convenient there can be a potential drawback in some cases.
If you run the following, which is based on the example code from the documentation, under Mac OS X
fn main()
{
// bind the listener to the specified address
let listener = TcpListener::bind("127.0.0.1", 80);
match listener
{
Ok(_) =>
{
println!("bind: have listener");
}
Err(_) =>
{
println!("bind: have error");
}
}
let mut acceptor = listener.listen();
match acceptor
{
Ok(_) =>
{
println!("listen: have acceptor");
}
Err(e) =>
{
println!("listen: have error");
}
}
// accept connections and process them, spawning a new tasks for each one
for stream in acceptor.incoming()
{
match stream
{
Err(e) =>
{
/* connection failed */
println!("incoming: have error {}", e);
}
Ok(stream) => spawn(proc()
{
// connection succeeded
handle_client(stream)
})
}
}
// close the socket server
drop(acceptor);
}
you get
bind: have error
listen: have error
because by default the program cannot open port 80, followed by
incoming: have error permission denied (Permission denied)
repeated ad infinitum, or at least until the point that you hit Ctrl-C, as the program screams around in a tight loop doing absolutely nothing.
In non-example code the failure case within the loop would probably cause the loop to exit or the task to terminate so it wouldn’t loop indefinitely but even so it would probably be better to check whether the initial call to bind actually worked before doing anything else.
Explicit Error Checking
The problem with explicit error checking is that it can get quite messy quite fast.
For example
fn main()
{
match TcpListener::bind("127.0.0.1", 80)
{
Ok(listener) =>
{
println!("bind: have listener");
match listener.listen()
{
Ok(mut acceptor) =>
{
println!("listen: have acceptor");
for stream in acceptor.incoming()
{
match stream
{
Err(e) =>
{
/* connection failed */
println!("incoming: have error");
}
Ok(stream) => spawn(proc()
{
// connection succeeded
handle_client(stream)
})
}
}
// close the socket server
drop(acceptor);
}
Err(_) =>
{
println!("listen: have error");
}
}
}
Err(_) =>
{
println!("bind: have error");
}
}
}
Whilest not completely impenetrable its not the kind of code you would want to have to maintain if you could possibly avoid it.
The try Macro
There is a utility macro
try
which is intended to help in this situation.
It is defined like this
macro_rules! try(
($e:expr) => (match $e { Ok(e) => e, Err(e) => return Err(e) })
)
The definition can be found in the file
$(RUST_SRC)/rust-0.11.0/src/libcore/macros.rs
As you can see, it works by returning from the function immediately if an error occurs, so you can do something like this
...
let listener = try!(TcpListener::bind("127.0.0.1", 80));
...
and if the call to TcpListener::bind
fails the calling function returns immediately
We can then do the same thing for the call to listen
...
let mut acceptor = try!(listener.listen());
...
so we end up with something like this
fn run(host:&str, port: u16) -> IoResult<????>
{
// bind the listener to the specified address
let listener = try!(TcpListener::bind(host, port));
println!("listen: have listener");
let mut acceptor = try!(listener.listen());
println!("listen: have acceptor");
// accept connections and process them, spawning a new tasks for each one
for stream in acceptor.incoming()
{
match stream
{
Err(e) =>
{
/* connection failed */
println!("incoming: have error");
}
Ok(stream) => spawn(proc()
{
// connection succeeded
handle_client(stream)
})
}
}
// close the socket server
drop(acceptor);
return ????;
}
Note that the function has to have a non-void return type and in this case it has to be
IoResult<????>
or
Result<????, IoError>
if you prefer, where ???? is the type of whatever the function returns in the non-error case.
Since there really isn’t anything to return from the function unless something goes wrong the easiest thing to do is reflect that in the return type and declare it to be
IoResult<()>
i.e., the empty tuple type, so we now have
fn run(host:&str, port: u16) -> IoResult<()>
{
// bind the listener to the specified address
let listener = try!(TcpListener::bind(host, port));
println!("listen: have listener");
let mut acceptor = try!(listener.listen());
println!("listen: have acceptor");
// accept connections and process them, spawning a new tasks for each one
for stream in acceptor.incoming()
{
match stream
{
Err(e) =>
{
/* connection failed */
println!("incoming: have error");
}
Ok(stream) => spawn(proc()
{
// connection succeeded
handle_client(stream)
})
}
}
// close the socket server
drop(acceptor);
return ();
}
which is definitely an improvement upon the deeply nested version above.
Copyright (c) 2014 By Simon Lewis. All Rights Reserved.
Unauthorized use and/or duplication of this material without express and written permission from this blog’s author and owner Simon Lewis is strictly prohibited.
Excerpts and links may be used, provided that full and clear credit is given to Simon Lewis and justanapplication.wordpress.com with appropriate and specific direction to the original content.