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.

  1. $ cargo new hello-world
  2. $ cd hello-world

Next, add the necessary dependencies in Cargo.toml:

  1. [dependencies]
  2. tokio = "0.1"

and the crates and types into scope in main.rs:

  1. # #![deny(deprecated)]
  2. extern crate tokio;
  3. use tokio::io;
  4. use tokio::net::TcpStream;
  5. use tokio::prelude::*;
  6. # 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 stdwith 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.

  1. # #![deny(deprecated)]
  2. # extern crate tokio;
  3. #
  4. # use tokio::net::TcpStream;
  5. fn main() {
  6. // Parse the address of whatever server we're talking to
  7. let addr = "127.0.0.1:6142".parse().unwrap();
  8. let client = TcpStream::connect(&addr);
  9. // Following snippets come here...
  10. }

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.

  1. # #![deny(deprecated)]
  2. # extern crate tokio;
  3. #
  4. # use tokio::net::TcpStream;
  5. # use tokio::prelude::*;
  6. # fn main() {
  7. # let addr = "127.0.0.1:6142".parse().unwrap();
  8. let client = TcpStream::connect(&addr).and_then(|stream| {
  9. println!("created stream");
  10. // Process stream here.
  11. Ok(())
  12. })
  13. .map_err(|err| {
  14. // All tasks must have an `Error` type of `()`. This forces error
  15. // handling and helps avoid silencing failures.
  16. //
  17. // In our example, we are only going to log the error to STDOUT.
  18. println!("connection error = {:?}", err);
  19. });
  20. # }

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:

  1. # #![deny(deprecated)]
  2. # extern crate tokio;
  3. #
  4. # use tokio::io;
  5. # use tokio::prelude::*;
  6. # use tokio::net::TcpStream;
  7. # fn main() {
  8. # let addr = "127.0.0.1:6142".parse().unwrap();
  9. let client = TcpStream::connect(&addr).and_then(|stream| {
  10. println!("created stream");
  11. io::write_all(stream, "hello world\n").then(|result| {
  12. println!("wrote to stream; success={:?}", result.is_ok());
  13. Ok(())
  14. })
  15. })
  16. # ;
  17. # }

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.

  1. # #![deny(deprecated)]
  2. # extern crate tokio;
  3. # extern crate futures;
  4. #
  5. # use tokio::prelude::*;
  6. # use futures::future;
  7. # fn main() {
  8. # let client = future::ok(());
  9. println!("About to create the stream and write to it...");
  10. tokio::run(client);
  11. println!("Stream has been created and written to.");
  12. # }

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.

  1. $ 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.

  1. $ 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