How it works...

In main, we first parse a string representing our IPv6 loopback address (think localhost) as an std::net::SocketAddr, which is a type holding an IP address and a port [13]. Granted, we could have used a constant for our address, but we are showing how to parse it from a string, because in a real application you will probably fetch the address from an environment variable, as shown in Chapter 1, Learning the Basics; Interacting with environment variables.

We then run our hyper server, which we create in run_with_service_function [17]. Let's take a look at that function by learning a bit about hyper.

The most fundamental trait in hyper is the Service. It is defined as follows:

pub trait Service where
<Self::Future as Future>::Item == Self::Response,
<Self::Future as Future>::Error == Self::Error, {
type Request;
type Response;
type Error;
type Future: Future;
fn call(&self, req: Self::Request) -> Self::Future;
}

It should be easy to read the signature of call: It takes a Request and returns a Future of a Response. hyper uses this trait to answer to an incoming request. We generally have two ways to define a Service:

  • Manually create a struct that implements Service, explicitly setting its associated types to whatever call returns
  • Let a Service be built for you by passing a closure that returns a Result to service_fn, which you wrap in a const_service

Both variants result in the exact same thing, so this example contains both versions to give you a taste of them.

run_with_service_function uses the second style [22]. It returns a Result of a Response, which service_fn converts to a Future of Response because Result implements Future. service_fn then does some type deduction for us and creates a kind of Service. But we aren't done yet. You see, when hyper receives a new connection, it will not call our Service directly with the Request, but first makes a copy of it in order to handle every connection with its very own Service. This means that our Service must have the ability to create new instances of itself, which is indicated by the NewService trait. Luckily, we don't need to implement it ourselves either. The closure at the heart of our Service doesn't manage any state, so we can call it a constant function. Constants are very easy to copy, as all copies are guaranteed to be identical. We can mark our Service as constant by calling const_service on it, which basically just wraps the Service in an Arc and then implements NewService by simply returning a copy of it. But what exactly is our Service returning anyways?

Response<hyper::Body> creates a new HTTP response [25] and manages its body as a hyper::Body, which is a future Stream<Chunk>. A Chunk is just a piece of an HTTP message. This Response is a builder, so we can change the contents of it by calling various methods. In our code, we set its Content-Type header to plaintext, which is a hyper shortcut for the MIME type text/plain [27].

A MIME type is a label for data served over HTTP. It tells the client how to treat the data it receives. For example, most browsers will not render the message <p>Hello World!</p> as HTML unless it comes with the header Content-Type: text/html.

We also set its Content-Length header to the length (in bytes) of our message so the client knows how much data they should expect [29]. Finally, we set the message's body to the message, which then gets sent to the client as "Hello World!" [31].

Our service can now be bound to a new instance of hyper::server::Http, which we then run [34 and 35]. You can now open your browser of choice and point it to http://localhost:3000. If everything went right, you should be greeted by a Hello World! message.

The same thing would happen if we called run_with_service_struct instead, which uses a manually created Service instead [40]. A quick inspection of its implementation shows us the key differences to the last approach [45 to 63]:

struct HelloWorld;
impl Service for HelloWorld {
type Request = Request;
type Response = Response;
type Error = hyper::Error;
type Future = Box<Future<Item = Self::Response, Error =
Self::Error>>;

fn call(&self, _: Request) -> Self::Future {
Box::new(futures::future::ok(
Response::new()
.with_header(ContentType::plaintext())
.with_header(ContentLength(MESSAGE.len() as u64))
.with_body(MESSAGE),
))
}
}

As you can see, we need to explicitly specify the concrete type of basically everything [48 to 52]. We also can't simply return a Result in our call method and need to return the actual Future, wrapped in a Box [56], so we don't need to think about which exact flavor of Future we are using.

On the other hand, this approach has one big advantage over the other: It can manage state in the form of members. Because all hyper recipes in this chapter work with constant Services, that is Services that will return the same Response to equal Requests, we will use the first variant to create Services. This is simply a stylistic decision based on simplicity, as they are all small enough that it wouldn't be worth it to extract them into an own struct. In your projects, use whichever form suits the current use case best.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.150.142