I/O Overview
The Rust standard library provides support for networking and I/O, suchas TCP connections, UDP sockets, reading from and writing to files, etc.However, those operations are all synchronous, or blocking, meaningthat when you call them, the current thread may stop executing and go tosleep until it is unblocked. For example, the read
method instd::Read
will block until there is data to read. In the worldof futures, that behavior is unfortunate, since we would like tocontinue executing other futures we may have while waiting for the I/Oto complete.
To enable this, Tokio provides non-blocking versions of many standardlibrary I/O resources, such as file operations and TCP, UDP, andUnix sockets. They return futures for long-running operations (likeaccepting a new TCP connection), and implement non-blocking variants ofstd::Read
and std::Write
called AsyncRead
andAsyncWrite
.
Non-blocking reads and writes do not block if, for example, there is nomore data available. Instead, they return immediately with aWouldBlock
error, along with a guarantee (like Future::poll
) thatthey have arranged for the current task to be woken up when they canlater make progress, such as when a network packet arrives.
By using the non-blocking Tokio I/O types, a future that performs I/Ono longer blocks execution of other futures if the I/O they wish toperform cannot be performed immediately. Instead, it simply returnsNotReady
, and relies on a task notification to cause poll
to becalled again, and which point its I/O should succeed without blocking.
Behind the scenes, Tokio uses mio
and tokio-fs
to keep track ofthe status of the various I/O resources that different futures arewaiting for, and is notified by the operating system whenever the statusof any of them change.
An example server
To get a sense of how this fits together, consider this echoserver implementation:
# extern crate tokio;
use tokio::prelude::*;
use tokio::net::TcpListener;
# fn main() {
// Set up a listening socket, just like in std::net
let addr = "127.0.0.1:12345".parse().unwrap();
let listener = TcpListener::bind(&addr)
.expect("unable to bind TCP listener");
// Listen for incoming connections.
// This is similar to the iterator of incoming connections that
// .incoming() from std::net::TcpListener, produces, except that
// it is an asynchronous Stream of tokio::net::TcpStream instead
// of an Iterator of std::net::TcpStream.
let incoming = listener.incoming();
// Since this is a Stream, not an Iterator, we use the for_each
// combinator to specify what should happen each time a new
// connection becomes available.
let server = incoming
.map_err(|e| eprintln!("accept failed = {:?}", e))
.for_each(|socket| {
// Each time we get a connection, this closure gets called.
// We want to construct a Future that will read all the bytes
// from the socket, and write them back on that same socket.
//
// If this were a TcpStream from the standard library, a read or
// write here would block the current thread, and prevent new
// connections from being accepted or handled. However, this
// socket is a Tokio TcpStream, which implements non-blocking
// I/O! So, if we read or write from this socket, and the
// operation would block, the Future will just return NotReady
// and then be polled again in the future.
//
// While we *could* write our own Future combinator that does an
// (async) read followed by an (async) write, we'll instead use
// tokio::io::copy, which already implements that. We split the
// TcpStream into a read "half" and a write "half", and use the
// copy combinator to produce a Future that asynchronously
// copies all the data from the read half to the write half.
let (reader, writer) = socket.split();
let bytes_copied = tokio::io::copy(reader, writer);
let handle_conn = bytes_copied.map(|amt| {
println!("wrote {:?} bytes", amt)
}).map_err(|err| {
eprintln!("I/O error {:?}", err)
});
// handle_conn here is still a Future, so it hasn't actually
// done any work yet. We *could* return it here; then for_each
// would wait for it to complete before it accepts the next
// connection. However, we want to be able to handle multiple
// connections in parallel, so we instead spawn the future and
// return an "empty" future that immediately resolves so that
// Tokio will _simultaneously_ accept new connections and
// service this one.
tokio::spawn(handle_conn)
});
// The `server` variable above is itself a Future, and hasn't actually
// done any work yet to set up the server. We need to run it on a Tokio
// runtime for the server to really get up and running:
tokio::run(server);
# }
More examples can be found here.
Next up: Reading and Writing Data