Applied: Simple HTTP Server
Let's use async
/.await
to build an echo server!
To start, run rustup update stable
to make sure you've got stable Rust 1.39 or newer. Once you've done that, run
cargo new async-await-echo
to create a new project, and open up
the resulting async-await-echo
folder.
Let's add some dependencies to the Cargo.toml
file:
[dependencies]
# Hyper is an asynchronous HTTP library. We'll use it to power our HTTP
# server and to make HTTP requests.
hyper = "0.13"
# To setup some sort of runtime needed by Hyper, we will use the Tokio runtime.
tokio = { version = "0.2", features = ["full"] }
# (only for testing)
anyhow = "1.0.31"
reqwest = { version = "0.10.4", features = ["blocking"] }
Now that we've got our dependencies out of the way, let's start writing some code. We have some imports to add:
use {
hyper::{
// Following functions are used by Hyper to handle a `Request`
// and returning a `Response` in an asynchronous manner by using a Future
service::{make_service_fn, service_fn},
// Miscellaneous types from Hyper for working with HTTP.
Body,
Client,
Request,
Response,
Server,
Uri,
},
std::net::SocketAddr,
};
Once the imports are out of the way, we can start putting together the boilerplate to allow us to serve requests:
async fn serve_req(_req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
// Always return successfully with a response containing a body with
// a friendly greeting ;)
Ok(Response::new(Body::from("hello, world!")))
}
async fn run_server(addr: SocketAddr) {
println!("Listening on http://{}", addr);
// Create a server bound on the provided address
let serve_future = Server::bind(&addr)
// Serve requests using our `async serve_req` function.
// `serve` takes a type which implements the `MakeService` trait.
// `make_service_fn` converts a closure into a type which
// implements the `MakeService` trait. That closure must return a
// type that implements the `Service` trait, and `service_fn`
// converts a request-response function into a type that implements
// the `Service` trait.
.serve(make_service_fn(|_| async {
Ok::<_, hyper::Error>(service_fn(serve_req))
}));
// Wait for the server to complete serving or exit with an error.
// If an error occurred, print it to stderr.
if let Err(e) = serve_future.await {
eprintln!("server error: {}", e);
}
}
#[tokio::main]
async fn main() {
// Set the address to run our socket on.
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
// Call our `run_server` function, which returns a future.
// As with every `async fn`, for `run_server` to do anything,
// the returned future needs to be run using `await`;
run_server(addr).await;
}
If you cargo run
now, you should see the message "Listening on
http://127.0.0.1:3000" printed on your terminal. If you open that URL in your
browser of choice, you'll see "hello, world!" appear in your browser.
Congratulations! You just wrote your first asynchronous webserver in Rust.
You can also inspect the request itself, which contains information such as the request URI, HTTP version, headers, and other metadata. For example, we can print out the URI of the request like this:
println!("Got request at {:?}", _req.uri());
You may have noticed that we're not yet doing
anything asynchronous when handling the request-- we just respond immediately,
so we're not taking advantage of the flexibility that async fn
gives us.
Rather than just returning a static message, let's try proxying the user's
request to another website using Hyper's HTTP client.
We start by parsing out the URL we want to request:
let url_str = "http://www.rust-lang.org/en-US/";
let url = url_str.parse::<Uri>().expect("failed to parse URL");
Then we can create a new hyper::Client
and use it to make a GET
request,
returning the response to the user:
let res = Client::new().get(url).await?;
// Return the result of the request directly to the user
println!("request finished-- returning response");
Ok(res)
Client::get
returns a hyper::client::ResponseFuture
, which implements
Future<Output = Result<Response<Body>>>
(or Future<Item = Response<Body>, Error = Error>
in futures 0.1 terms).
When we .await
that future, an HTTP request is sent out, the current task
is suspended, and the task is queued to be continued once a response has
become available.
Now, if you cargo run
and open http://127.0.0.1:3000/foo
in your browser,
you'll see the Rust homepage, and the following terminal output:
Listening on http://127.0.0.1:3000
Got request at /foo
making request to http://www.rust-lang.org/en-US/
request finished-- returning response
Congratulations! You just proxied an HTTP request.