Skip to content

A hyper without tokio-proto #1342

Closed
Closed
@seanmonstar

Description

@seanmonstar

It would be nice to have hyper working without internally using tokio-proto. It should be possible to do this in a backwards-compatible way at first, such that the server still provides a ServerProto implementation that makes use of the internal stuff, and that can be deprecated in the future.

Motivation

  • The tokio-proto crate is being de-emphasized.
  • We can do some things better, specifically for HTTP
  • Errors that are encountered reading, parsing, and writing are kind of just lost.
    • They are given back to tokio-proto, but it's pretty tricky for a user to get those errors.
  • It doesn't allow hyper to currently spawn a server that can handle both HTTP/1 and HTTP/2

API

Http::bind_connection(socket, service) -> Connection

Compared to the current bind_connection:

  • The socket is T: AsyncRead + AsyncWrite.
  • The service is currently a S: Service.
  • The addr is not needed anymore. Anyone wanting to know the address can store it on their Service.
  • The handle is not provided, since it's only purpose was to spawn a task on it. With the desire to separate tokio from being a task executor, we can plan ahead.

The returned type is a Connection<T, S>, which implements Future. Instead of taking a Handle, a user can instead receive this future and spawn it on any executor they desire.

Of course, since bind_connection already exist, this would need a new name. It could be bind_connection2, with the previous deprecated, with the intent to finish the replacement in 0.12.

Or maybe bind_connection itself isn't the best of names. The action of this method is to apply the Http protocol to a connection, aided by the Service to respond to requests.

Connection

This is returned by Http::bind_connection, and implements Future.

  • Item = ()
  • Error = hyper::Error

If there are never any errors when processing the connection, the future yields Ok(Async::Ready) when Http says it should close.

If there are errors encountered that would require tearing down the connection, the future returns the Err. Importantly, this allows users to very easily know if there was an error reading or writing the HTTP protocol.

Some unresolved questions:

  • This could perhaps have the Item be the socket (T) instead, in case there is desire to be able to do something with the socket after HTTP is "done" (perhaps due to a protocol upgrade).
  • When HTTP/2 is introduced, it is possible to have stream errors which aren't fatal to the connection, but do abort a stream. It wouldn't be feasible to make those stream errors be returned from the Connection future, as it should keep working.
    • Perhaps Connection shouldn't be a Future directly, but more like a Stream... of Errs...
    • The Connection could alternatively grow a on_stream_error<F>(callback: F) where F: Fn(hyper::Error) or something...

Server

The current Server type is supposed to be an "easy mode" for starting up a TCP listener and accepting plain text HTTP requests. There is a related proposal (#1322) to make Server accept any kind of listener, and be a Future so users can execute it with their own executor.

Perhaps that proposal should be updated to support this new Connection future:

  • impl Stream for Server instead of Future.
    • The Server could be a Stream of already bound Connections.
    • This would allow someone to use the Server to automatically call Http::bind_connection with a service, and manage graceful shutdown, while still being able to watch for errors that happen in that connection.

We could add some convenience methods to Server, like run_ignore_errors() -> impl Future, so a user could easily just "run" the server without worrying about it being a Stream of Connections.

Usage

Bare bones bind_connection

let http = Http::new();

// we've received a socket and made a service somewhere else
let conn = http.bind_connection(socket, service);
// whatever your executor is
executor.spawn(conn.map_err(move |err| {
    println!("connection [{}] error: {}", addr, err);
}));

Fuller example with a tokio Core and hyper Server

let mut core = Core::new()?;
let handle = core.handle();

let tcp = TcpListener::bind(addr, &handle).map(|listener| {
  listener.incoming()
});
let tls = tcp.and_then(|tcp_streams| {
    tcp_streams.and_then(|(socket, _addr)| {
        TlsAcceptorThing::handshake(socket)
    })
});

let server = tls.map(|tls_streams| {
    let http = Http::new();
    http.serve(tls_streams, || Ok(Echo))
});

let connections = server.and_then(move |srv| {
    // srv is `Server`, so it is a `Stream<Item=Connection>`
    srv.for_each(move |conn| {
        // each connection should be its own task, so spawn it on the executor
        handle.spawn(conn.map_err(|err| {
            println!("http error: {}", err);
        }));
        Ok(())
    })
});

core.run(connections)?;

Meta unresolved questions

  • Perhaps the naming of types could be improved as well? (Of course, only finish renaming in 0.12)
    • Currently:
      • Http is more like a ServerBuilder or ConnectionBuilder...
      • If the Http were named a builder, Http could replace Connection? Such that Server is a Stream<Item=Http>?
    • Many people are used to reaching for a Server type, but hyper currently asks you to grab the Http to configure a Server.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-serverArea: server.C-featureCategory: feature. This is adding a new feature.E-hardEffort: hard. Likely requires a deeper understanding of how hyper's internals work.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions