Hello World!
To kick off our tour of Tokio, we will start with the obligatory “hello world” example. This program will create a TCP stream and write “hello, world!” to the stream. The difference between this and a Rust program that writes to a TCP stream without Tokio is that this program won’t block program execution when the stream is created or when our “hello, world!” message is written to the stream.
Before we begin you should have a very basic understanding of how TCP streams work. Having an understanding of Rust’s standard library implementation is also helpful.
Let’s get started.
First, generate a new crate.
$ cargo new hello-world
$ cd hello-world
Next, add the necessary dependencies in Cargo.toml
:
[dependencies]
tokio = "0.1"
and the crates and types into scope in main.rs
:
# #![deny(deprecated)]
extern crate tokio;
use tokio::io;
use tokio::net::TcpStream;
use tokio::prelude::*;
# fn main() {}
Here we use Tokio’s own io
and net
modules. These modules provide the same
abstractions over networking and I/O-operations as the corresponding modules in std
with a small difference: all actions are performed asynchronously.
Creating the stream
The first step is to create the TcpStream
. We use the TcpStream
implementation
provided by Tokio.
# #![deny(deprecated)]
# extern crate tokio;
#
# use tokio::net::TcpStream;
fn main() {
// Parse the address of whatever server we're talking to
let addr = "127.0.0.1:6142".parse().unwrap();
let client = TcpStream::connect(&addr);
// Following snippets come here...
}
Next, we’ll add some to the client
TcpStream
. This asynchronous task now creates
the stream and then yields it once it’s been created for additional processing.
# #![deny(deprecated)]
# extern crate tokio;
#
# use tokio::net::TcpStream;
# use tokio::prelude::*;
# fn main() {
# let addr = "127.0.0.1:6142".parse().unwrap();
let client = TcpStream::connect(&addr).and_then(|stream| {
println!("created stream");
// Process stream here.
Ok(())
})
.map_err(|err| {
// All tasks must have an `Error` type of `()`. This forces error
// handling and helps avoid silencing failures.
//
// In our example, we are only going to log the error to STDOUT.
println!("connection error = {:?}", err);
});
# }
The call to TcpStream::connect
returns a Future
of the created TCP stream.
We’ll learn more about Futures
later in the guide, but for now you can think of
a Future
as a value that represents something that will eventually happen in the
future (in this case the stream will be created). This means that TcpStream::connect
does
not wait for the stream to be created before it returns. Rather it returns immediately
with a value representing the work of creating a TCP stream. We’ll see down below when this work
actually gets executed.
The and_then
method yields the stream once it has been created. and_then
is an
example of a combinator function that defines how asynchronous work will be processed.
Each combinator function takes ownership of necessary state as well as the
callback to perform and returns a new Future
that has the additional “step”
sequenced. A Future
is a value representing some computation that will complete at
some point in the future.
It’s worth reiterating that returned futures are lazy, i.e., no work is performed when
calling the combinator. Instead, once all the asynchronous steps are sequenced, the
final Future
(representing the entire task) is ‘spawned’ (i.e., run). This is when
the previously defined work starts getting run. In other words, the code we’ve written
so far does not actually create a TCP stream.
We will be digging more into futures (and the related concepts of streams and sinks) later on.
It’s also important to note that we’ve called map_err
to convert whatever error
we may have gotten to ()
before we can actually run our future. This ensures that
we acknowledge errors.
Next, we will process the stream.
Writing data
Our goal is to write "hello world\n"
to the stream.
Going back to the TcpStream::connect(addr).and_then
block:
# #![deny(deprecated)]
# extern crate tokio;
#
# use tokio::io;
# use tokio::prelude::*;
# use tokio::net::TcpStream;
# fn main() {
# let addr = "127.0.0.1:6142".parse().unwrap();
let client = TcpStream::connect(&addr).and_then(|stream| {
println!("created stream");
io::write_all(stream, "hello world\n").then(|result| {
println!("wrote to stream; success={:?}", result.is_ok());
Ok(())
})
})
# ;
# }
The io::write_all
function takes ownership of stream
, returning a
Future
that completes once the entire message has been written to the
stream. then
is used to sequence a step that gets run once the write has
completed. In our example, we just write a message to STDOUT
indicating that
the write has completed.
Note that result
is a Result
that contains the original stream (compare to
and_then
, which passes the stream without the Result
wrapper). This allows us
to sequence additional reads or writes to the same stream. However, we have
nothing more to do, so we just drop the stream, which automatically closes it.
Running the client task
So far we have a Future
representing the work to be done by our program, but we
have not actually run it. We need a way to ‘spawn’ that work. We need an executor.
Executors are responsible for scheduling asynchronous tasks, driving them to completion. There are a number of executor implementations to choose from, each have different pros and cons. In this example, we will use the default executor of the Tokio runtime.
# #![deny(deprecated)]
# extern crate tokio;
# extern crate futures;
#
# use tokio::prelude::*;
# use futures::future;
# fn main() {
# let client = future::ok(());
println!("About to create the stream and write to it...");
tokio::run(client);
println!("Stream has been created and written to.");
# }
tokio::run
starts the runtime, blocking the current thread until all spawned tasks
have completed and all resources (like files and sockets) have been dropped.
So far, we only have a single task running on the executor, so the client
task
is the only one blocking run
from returning. Once run
has returned we can be sure
that our Future has been run to completion.
You can find the full example here.
Running the code
Netcat is a tool for quickly creating TCP sockets from the command line. The following command starts a listening TCP socket on the previously specified port.
$ nc -l -p 6142
The command above is used with the GNU version of netcat that comes stock on many unix based operating systems. The following command can be used with the NMap.org version:
$ ncat -l -p 6142
In a different terminal we’ll run our project.
$ cargo run
If everything goes well, you should see hello world
printed from Netcat.
Next steps
We’ve only dipped our toes into Tokio and its asynchronous model. The next page in the guide will start digging a bit deeper into Futures and the Tokio runtime model.