Skip to content
Merged
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"respawn",
"RESPAWNED",
"respawning",
"rvalues",
"scid",
"SCIDLEN",
"scids",
Expand Down
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ awaitable<void> do_session(Client& client, boost::urls::url url)
auto response = co_await request.async_get_response();
}
```

A plain GET needs none of those four steps spelled out. `async_get()` does the whole request as one
operation and hands back the response as a plain Beast message -- status, fields and the body as a
`std::string`:

```c++
auto session = co_await client.async_connect();
auto message = co_await session.async_get(url);
std::println("{}: {} bytes", message.result_int(), message.body().size());
```

The convenience is paid for with memory, as the body is buffered in full: anything that wants to
look at the body while it arrives, or to send a body of its own, still goes through
`async_submit()`.

# Implementation

The asynchronous operations exposed by server and client are [ASIO asynchronous operations](https://think-async.com/Asio/asio-1.30.2/doc/asio/reference/asynchronous_operations.html). As such, they support a range of [completion tokens](https://think-async.com/Asio/asio-1.30.2/doc/asio/overview/model/completion_tokens.html) like [use_awaitable](https://think-async.com/Asio/asio-1.30.2/doc/asio/reference/use_awaitable.html) or plain callbacks.
Expand Down
142 changes: 0 additions & 142 deletions include/anyhttp/any_async_stream.hpp

This file was deleted.

2 changes: 1 addition & 1 deletion include/anyhttp/buffer_array.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ class buffer_array
buffer_array(BS const& bs) noexcept : dummy_(0)
{
auto it = buffer_sequence_begin(bs);
auto const last = buffer_sequence_begin(bs);
auto const last = buffer_sequence_end(bs);
while (it != last && n_ < N)
{
value_type b(*it);
Expand Down
13 changes: 13 additions & 0 deletions include/anyhttp/client.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#include <boost/asio/buffer.hpp>

#include <boost/beast/core/stream_traits.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>

#include <boost/url.hpp>

Expand Down Expand Up @@ -34,6 +36,17 @@ struct Config

// =================================================================================================

//
// A response received in one piece: status, header fields and the whole body as a string.
//
// This is a plain Beast message, with nothing of anyhttp left in it -- what \c Session::async_get()
// hands back. Its version is 11 whatever the protocol was: HTTP/2 and HTTP/3 have no version on
// the wire, and a Beast message has nowhere else to put one.
//
using Message = boost::beast::http::response<boost::beast::http::string_body>;

// -------------------------------------------------------------------------------------------------

class Response
{
public:
Expand Down
4 changes: 2 additions & 2 deletions include/anyhttp/concepts.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ concept MutableBufferSequence = boost::asio::is_mutable_buffer_sequence<T>::valu


//
// https://think-async.com/Asio/asio-1.11.0/doc/asio/reference/AsyncReadStream.html
// https://think-async.com/Asio/asio-1.11.0/doc/asio/reference/AsyncWriteStream.html
// https://think-async.com/Asio/asio-1.38.2/doc/asio/reference/AsyncReadStream.html
// https://think-async.com/Asio/asio-1.38.2/doc/asio/reference/AsyncWriteStream.html
//

template <typename T>
Expand Down
162 changes: 162 additions & 0 deletions include/anyhttp/detail/any_async_stream.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#pragma once

//
// The type-erased async stream as its users see it. The implementation behind it is only forward
// declared here: it lives in anyhttp/detail/any_async_stream_impl.hpp, which src/
// any_async_stream_impl.cpp is the only place to include -- and to instantiate.
//

#include <anyhttp/buffer_array.hpp>

#include <boost/asio/any_completion_handler.hpp>
#include <boost/asio/any_io_executor.hpp>
#include <boost/asio/async_result.hpp>
#include <boost/asio/buffer.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/ssl/stream.hpp>

#include <boost/beast/core/buffer_traits.hpp>
#include <boost/beast/core/stream_traits.hpp>

#include <memory>
#include <type_traits>

namespace asio = boost::asio;
namespace ip = asio::ip;

namespace anyhttp
{

// =================================================================================================

using ReadWrite = void(boost::system::error_code, std::size_t);
using ReadWriteHandler = asio::any_completion_handler<ReadWrite>;

using ConstBufferVector = const_buffer_array<16>;
using MutableBufferVector = mutable_buffer_array<16>;

using Shutdown = void(boost::system::error_code);
using ShutdownHandler = asio::any_completion_handler<Shutdown>;

/**
* The socket underneath a stream, as far as the sessions care about it: enough for shutdown() and
* close(), which is all they do with it. A TLS stream hands out its \c lowest_layer(), which is
* this rather than the full ip::tcp::socket, so this is what the type-erased stream offers, too.
*/
using TcpSocketBase = asio::basic_socket<ip::tcp, asio::any_io_executor>;

/// The TLS stream the server and client run on, spelled out often enough to deserve a name.
using SslStream = asio::ssl::stream<ip::tcp::socket>;

/**
* Attempt to create a type-erased async stream with ASIO.
*
* The difficult part here is to type-erase the buffer sequences. The buffers are copied into a
* buffer_array, a fixed-capacity, non-allocating array of buffer descriptors that is itself a
* buffer sequence. This seems to work reasonably well.
*
* There is also \c asio::buffer_sequence_adapter and \c linearise(), which seem to be used in ASIO
* SSL code as well. It merges a set of buffers into a new, contiguous buffer. But that is slow.
*/
class any_async_stream
{
public:
using executor_type = boost::asio::any_io_executor;

/// The type-erased stream itself, defined in anyhttp/detail/any_async_stream_impl.hpp.
class Impl;

explicit any_async_stream(std::unique_ptr<Impl> impl);
any_async_stream(any_async_stream&&) noexcept;
any_async_stream& operator=(any_async_stream&&) noexcept;
~any_async_stream();

executor_type get_executor() noexcept;
TcpSocketBase& get_socket();

//
// async_write_some
//
// The async operations of ASIO are designed to work with sequences of buffers. Those cannot
// easily be type-erased, so we copy the buffer descriptors into a fixed-capacity array.
//
// The requirements for ConstBufferSequence are defined here:
// https://live.boost.org/doc/libs/1_88_0/doc/html/boost_asio/reference/ConstBufferSequence.html
//
// The iterators returned by asio::buffer_sequence_{begin,end} must be 'bidirectional', but are
// not required to be 'contiguous'. So those iterators cannot be simply converted to a span.
//
// * https://en.cppreference.com/w/cpp/iterator/bidirectional_iterator
// * https://en.cppreference.com/w/cpp/iterator/contiguous_iterator.html
//
// Instead, we copy them into a buffer_array, which is itself a (contiguous) buffer sequence and
// can be passed on to the underlying stream unchanged. Nothing is merged or linearized, so
// scatter/gather I/O is preserved. Empty buffers are dropped while copying, and sequences longer
// than the array's capacity are truncated -- which is harmless for a "some" operation, as it
// just results in a shorter transfer.
//
template <typename ConstBufferSequence,
BOOST_ASIO_COMPLETION_TOKEN_FOR(ReadWrite)
CompletionToken = asio::default_completion_token_t<asio::any_io_executor>>
requires boost::beast::is_const_buffer_sequence<ConstBufferSequence>::value
auto async_write_some(const ConstBufferSequence& buffers,
CompletionToken&& token = CompletionToken())
{
return boost::asio::async_initiate<CompletionToken, ReadWrite>(
[this](ReadWriteHandler handler, ConstBufferVector buffers)
{ //
write_some(std::move(handler), std::move(buffers));
}, token, ConstBufferVector{buffers});
}

//
// async_read_some
//
template <typename MutableBufferSequence,
BOOST_ASIO_COMPLETION_TOKEN_FOR(ReadWrite)
CompletionToken = asio::default_completion_token_t<asio::any_io_executor>>
requires boost::beast::is_mutable_buffer_sequence<MutableBufferSequence>::value
auto async_read_some(const MutableBufferSequence& buffers,
CompletionToken&& token = CompletionToken())
{
return boost::asio::async_initiate<CompletionToken, ReadWrite>(
[this](ReadWriteHandler handler, MutableBufferVector buffers)
{ //
read_some(std::move(handler), std::move(buffers));
}, token, MutableBufferVector{buffers});
}

private:
//
// The initiations, with the buffer sequence already type-erased. Out of line, because this is
// where the implementation is dereferenced -- it is incomplete here.
//
void write_some(ReadWriteHandler handler, ConstBufferVector buffers);
void read_some(ReadWriteHandler handler, MutableBufferVector buffers);

std::unique_ptr<Impl> impl;
};

static_assert(boost::beast::is_async_stream<any_async_stream>::value);

// -------------------------------------------------------------------------------------------------

//
// The stream is moved into the type-erasing wrapper -- hence the rvalue reference, which the
// constraint keeps from matching an lvalue, just like the session factories do it.
//
// This is defined in anyhttp/detail/any_async_stream_impl.hpp and explicitly instantiated in
// src/any_async_stream_impl.cpp for each of the stream types below, so that the implementation
// is instantiated in that one place only.
//

template <typename Stream>
requires(!std::is_reference_v<Stream>)
any_async_stream make_any_async_stream(Stream&& stream);

extern template any_async_stream make_any_async_stream<ip::tcp::socket>(ip::tcp::socket&&);
extern template any_async_stream make_any_async_stream<SslStream>(SslStream&&);

// =================================================================================================

} // namespace anyhttp
Loading
Loading