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 Tokiois that this program won’t block program execution when the stream is created or whenour “hello, world!” message is written to the stream.
Before we begin you should have a very basic understanding of how TCP streams work. Havingan understanding of Rust’s standard library implementationis 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 sameabstractions 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
implementationprovided 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 createsthe 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 ofa Future
as a value that represents something that will eventually happen in thefuture (in this case the stream will be created). This means that TcpStream::connect
doesnot wait for the stream to be created before it returns. Rather it returns immediatelywith a value representing the work of creating a TCP stream. We’ll see down below when this workactually gets executed.
The and_then
method yields the stream once it has been created. and_then
is anexample of a combinator function that defines how asynchronous work will be processed.
Each combinator function takes ownership of necessary state as well as thecallback to perform and returns a new Future
that has the additional “step”sequenced. A Future
is a value representing some computation that will complete atsome point in the future.
It’s worth reiterating that returned futures are lazy, i.e., no work is performed whencalling the combinator. Instead, once all the asynchronous steps are sequenced, thefinal Future
(representing the entire task) is ‘spawned’ (i.e., run). This is whenthe previously defined work starts getting run. In other words, the code we’ve writtenso 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 errorwe may have gotten to ()
before we can actually run our future. This ensures thatwe 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 aFuture
that completes once the entire message has been written to thestream. then
is used to sequence a step that gets run once the write hascompleted. In our example, we just write a message to STDOUT
indicating thatthe write has completed.
Note that result
is a Result
that contains the original stream (compare toand_then
, which passes the stream without the Result
wrapper). This allows usto sequence additional reads or writes to the same stream. However, we havenothing 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 wehave 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 tocompletion. There are a number of executor implementations to choose from, each havedifferent pros and cons. In this example, we will use the default executor of theTokio 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 taskshave 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
taskis the only one blocking run
from returning. Once run
has returned we can be surethat 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 followingcommand 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 manyunix based operating systems. The following command can be used with theNMap.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 inthe guide will start digging a bit deeper into Futures and the Tokio runtime model.
Next up: Futures