Rewrite the echo-op example

fix #1450
This commit is contained in:
Vinnie Falco
2019-02-13 12:16:17 -08:00
parent 96b2944f70
commit ebc5246c47
3 changed files with 299 additions and 266 deletions

View File

@@ -2,6 +2,7 @@ Version 214:
* Handler binders use the associated allocator
* Add detail::bind_continuation
* Rewrite the echo-op example
--------------------------------------------------------------------------------

View File

@@ -18,13 +18,19 @@ informed of the asynchronous operation result. __Asio__ comes with the
special tokens __use_future__ and __yield_context__ for using futures
and coroutines respectively. This system of customizing the return value
and method of completion notification is known as the
['Extensible Asynchronous Model] described in __N3747__, and a built in
['Universal Asynchronous Model] described in __N3747__, and a built in
to __NetTS__. Here is an example of an initiating function which reads a
line from the stream and echoes it back. This function is developed
further in the next section:
[example_core_echo_op_1]
[tip
This initiating function receives the dynamic buffer by lvalue-reference,
instead of by rvalue-reference as specified in networking. An explanation
for this difference may be found in
[@http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1100r0.html [P1100R0] Efficient composition with DynamicBuffer].
]
Authors using Beast can reuse the library's primitives to create their
own initiating functions for performing a series of other, intermediate
asynchronous operations before invoking a final completion handler.
@@ -113,8 +119,6 @@ composed operations:
This example develops an initiating function called [*echo].
The operation will read up to the first newline on a stream, and
then write the same line including the newline back on the stream.
The implementation performs both reading and writing, and has a
non-trivially-copyable state.
First we define the input parameters and results, then declare our
initiation function. For our echo operation the only inputs are the
stream and the completion token. The output is the error code which
@@ -125,47 +129,25 @@ is usually included in all completion handler signatures.
Now that we have a declaration, we will define the body of the function.
We want to achieve the following goals: perform static type checking on
the input parameters, set up the return value as per __N3747__, and launch
the composed operation by constructing the object and invoking it.
[example_core_echo_op_3]
the composed operation by constructing an intermediate, stateful completion
handler and invoking it.
The initiating function contains a few relatively simple parts. There is
the customization of the return value type, static type checking, building
the return value type using the helper, and creating and launching the
composed operation object. The [*`echo_op`] object does most of the work
here, and has a somewhat non-trivial structure. This structure is necessary
to meet the stringent requirements of composed operations (described in more
detail in the __Asio__ documentation). We will touch on these requirements
without explaining them in depth.
`echo_op` composed operation object.
Here is the boilerplate present in all composed operations written
in this style:
The implementation strategy is to make the composed object meet the
requirements of a completion handler by being movable, and by making it
invocable so it can be used as a continuation for the asynchronous operations
it launches. Rather than using `std::bind` or `boost::bind`, which destroys
the type information and therefore breaks the allocation and invocation hooks,
we will simply pass `std::move(*this)` as the completion handler parameter for
any operations that we initiate. For the move to work correctly, care must be
taken to ensure that no access to data members are made after the move takes
place. Here is the complete implementation of our composed operation:
[example_core_echo_op_4]
Next is to implement the function call operator. Our strategy is to make our
composed object meet the requirements of a completion handler by being movable,
and by providing the function call operator with the correct signature. Rather
than using `std::bind` or `boost::bind`, which destroys the type information
and therefore breaks the allocation and invocation hooks, we will simply pass
`std::move(*this)` as the completion handler parameter for any operations that
we initiate. For the move to work correctly, care must be taken to ensure that
no access to data members are made after the move takes place. Here is the
implementation of the function call operator for this echo operation:
[example_core_echo_op_5]
This is the most important element of writing a composed operation, and
the part which is often neglected or implemented incorrectly. It is the
forwarding of the final handler's associated allocator and associated
executor to the composed operation.
Our composed operation stores the final handler and performs its own
intermediate asynchronous operations. To ensure that I/O objects, in this
case the stream, are accessed safely it is important to use the same
executor to invoke intermediate handlers as that used to invoke the final
handler. Similarly, for memory allocations our composed operation should
use the allocator associated with the final handler.
[example_core_echo_op_3]
There are some common mistakes that should be avoided when writing
composed operations:
@@ -184,7 +166,8 @@ composed operations:
if someone calls the initiating function with a strand-wrapped
function object, and there is more than thread running on the
__io_context__, the underlying stream may be accessed in a fashion
that violates safety guarantees.
that violates safety guarantees. Beast provides class templates
to take care of this boilerplate for you.
* Forgetting to create an object of type __executor_work_guard__ with the
type of executor returned by the stream's `get_executor` member function.

View File

@@ -14,233 +14,86 @@
#include <memory>
#include <utility>
namespace net = boost::asio;
namespace beast = boost::beast;
//[example_core_echo_op_1
template<
class AsyncStream,
class DynamicBuffer,
class CompletionToken>
auto
async_echo (AsyncStream& stream, CompletionToken&& token)
async_echo (AsyncStream& stream, DynamicBuffer& buffer, CompletionToken&& token)
//]
-> BOOST_ASIO_INITFN_RESULT_TYPE(CompletionToken, void(boost::beast::error_code));
->
typename net::async_result<
typename std::decay<CompletionToken>::type,
void(beast::error_code)>::return_type;
//------------------------------------------------------------------------------
//[example_core_echo_op_2
/** Asynchronously read a line and echo it back.
This function is used to asynchronously read a line ending
in a newline ("LF") from the stream, and then write
it back. The function call always returns immediately. The
asynchronous operation will continue until one of the
following conditions is true:
in a newline (`"\n"`) from the stream, and then write
it back.
This call always returns immediately. The asynchronous operation
will continue until one of the following conditions is true:
@li A line was read in and sent back on the stream
@li A line was read in and written back on the stream
@li An error occurs.
This operation is implemented in terms of one or more calls to
the stream's `async_read_some` and `async_write_some` functions,
and is known as a <em>composed operation</em>. The program must
ensure that the stream performs no other operations until this
operation completes. The implementation may read additional octets
that lie past the end of the line being read. These octets are
silently discarded.
The algorithm, known as a <em>composed asynchronous operation</em>,
is implemented in terms of calls to the stream's `async_read_some`
and `async_write_some` function. The program must ensure that no
other reads or writes are performed until this operation completes.
@param The stream to operate on. The type must meet the
Since the length of the line is not known ahead of time, the
implementation may read additional characters that lie past the
first line. These characters are stored in the dynamic buffer_.
The same dynamic buffer must be presented again in each call,
to provide the implementation with any leftover bytes.
@param stream The stream to operate on. The type must meet the
requirements of @b AsyncReadStream and @AsyncWriteStream
@param token The completion token to use. If this is a
completion handler, copies will be made as required.
The equivalent signature of the handler must be:
@param buffer A dynamic buffer to hold implementation-defined
temporary data. Ownership is not transferred; the caller is
responsible for ensuring that the lifetime of this object is
extended least until the completion handler is invoked.
@param token The handler to be called when the operation completes.
The implementation will take ownership of the handler by move
construction. The handler must be invocable with this signature:
@code
void handler(
error_code ec // result of operation
beast::error_code error // Result of operation.
);
@endcode
Regardless of whether the asynchronous operation completes
immediately or not, the handler will not be invoked from within
this function. Invocation of the handler will be performed in a
manner equivalent to using `boost::asio::io_context::post`.
Regardless of whether the asynchronous operation completes immediately or
not, the handler will not be invoked from within this function. Invocation
of the handler will be performed in a manner equivalent to using
`net::post`.
*/
template<
class AsyncStream,
class DynamicBuffer,
class CompletionToken>
BOOST_ASIO_INITFN_RESULT_TYPE( /*< `BOOST_ASIO_INITFN_RESULT_TYPE` customizes the return value based on the completion token >*/
CompletionToken,
void (boost::beast::error_code)) /*< This is the signature for the completion handler >*/
auto
async_echo (
AsyncStream& stream,
CompletionToken&& token);
//]
//[example_core_echo_op_4
// This composed operation reads a line of input and echoes it back.
//
template <class AsyncStream, class Handler>
class echo_op
{
// This holds all of the state information required by the operation.
struct state
{
// The stream to read and write to
AsyncStream& stream;
// Boost.Asio and the Networking TS require an object of
// type executor_work_guard<T>, where T is the type of
// executor returned by the stream's get_executor function,
// to persist for the duration of the asynchronous operation.
boost::asio::executor_work_guard<
decltype(std::declval<AsyncStream&>().get_executor())> work;
// Indicates what step in the operation's state machine
// to perform next, starting from zero.
int step = 0;
// The buffer used to hold the input and output data.
//
// We use a custom allocator for performance, this allows
// the implementation of the io_context to make efficient
// re-use of memory allocated by composed operations during
// a continuation.
//
boost::asio::basic_streambuf<typename std::allocator_traits<
boost::asio::associated_allocator_t<Handler> >::
template rebind_alloc<char> > buffer;
// handler_ptr requires that the first parameter to the
// contained object constructor is a reference to the
// managed final completion handler.
//
explicit state(Handler const& handler, AsyncStream& stream_)
: stream(stream_)
, work(stream.get_executor())
, buffer((std::numeric_limits<std::size_t>::max)(),
boost::asio::get_associated_allocator(handler))
{
}
};
// The operation's data is kept in a cheap-to-copy smart
// pointer container called `handler_ptr`. This efficiently
// satisfies the CopyConstructible requirements of completion
// handlers with expensive-to-copy state.
//
// `handler_ptr` uses the allocator associated with the final
// completion handler, in order to allocate the storage for `state`.
//
boost::beast::handler_ptr<state, Handler> p_;
public:
// Boost.Asio requires that handlers are CopyConstructible.
// In some cases, it takes advantage of handlers that are
// MoveConstructible. This operation supports both.
//
echo_op(echo_op&&) = default;
echo_op(echo_op const&) = default;
// The constructor simply creates our state variables in
// the smart pointer container.
//
template<class DeducedHandler, class... Args>
echo_op(AsyncStream& stream, DeducedHandler&& handler)
: p_(std::forward<DeducedHandler>(handler), stream)
{
}
// Associated allocator support. This is Asio's system for
// allowing the final completion handler to customize the
// memory allocation strategy used for composed operation
// states. A composed operation should use the same allocator
// as the final handler. These declarations achieve that.
using allocator_type =
boost::asio::associated_allocator_t<Handler>;
allocator_type
get_allocator() const noexcept
{
return (boost::asio::get_associated_allocator)(p_.handler());
}
// Executor hook. This is Asio's system for customizing the
// manner in which asynchronous completion handlers are invoked.
// A composed operation needs to use the same executor to invoke
// intermediate completion handlers as that used to invoke the
// final handler.
using executor_type = boost::asio::associated_executor_t<
Handler, decltype(std::declval<AsyncStream&>().get_executor())>;
executor_type get_executor() const noexcept
{
return (boost::asio::get_associated_executor)(
p_.handler(), p_->stream.get_executor());
}
// The entry point for this handler. This will get called
// as our intermediate operations complete. Definition below.
//
void operator()(boost::beast::error_code ec, std::size_t bytes_transferred);
};
//]
//[example_core_echo_op_5
// echo_op is callable with the signature void(error_code, bytes_transferred),
// allowing `*this` to be used as both a ReadHandler and a WriteHandler.
//
template<class AsyncStream, class Handler>
void echo_op<AsyncStream, Handler>::
operator()(boost::beast::error_code ec, std::size_t bytes_transferred)
{
// Store a reference to our state. The address of the state won't
// change, and this solves the problem where dereferencing the
// data member is undefined after a move.
auto& p = *p_;
// Now perform the next step in the state machine
switch(ec ? 2 : p.step)
{
// initial entry
case 0:
// read up to the first newline
p.step = 1;
return boost::asio::async_read_until(p.stream, p.buffer, "\n", std::move(*this));
case 1:
// write everything back
p.step = 2;
// async_read_until could have read past the newline,
// use buffers_prefix to make sure we only send one line
return boost::asio::async_write(p.stream,
boost::beast::buffers_prefix(bytes_transferred, p.buffer.data()), std::move(*this));
case 2:
p.buffer.consume(bytes_transferred);
break;
}
// Invoke the final handler. The implementation of `handler_ptr`
// will deallocate the storage for the state before the handler
// is invoked. This is necessary to provide the
// destroy-before-invocation guarantee on handler memory
// customizations.
//
// If we wanted to pass any arguments to the handler which come
// from the `state`, they would have to be moved to the stack
// first or else undefined behavior results.
//
// The work guard is moved to the stack first, otherwise it would
// be destroyed before the handler is invoked.
//
auto work = std::move(p.work);
p_.invoke(ec);
return;
}
DynamicBuffer& buffer, /*< Unlike Asio, we pass by non-const reference instead of rvalue-ref >*/
CompletionToken&& token) ->
typename net::async_result< /*< `async_result` deduces the return type from the completion handler >*/
typename std::decay<CompletionToken>::type,
void(beast::error_code) /*< The completion handler signature goes here >*/
>::return_type;
//]
//[example_core_echo_op_3
@@ -250,40 +103,235 @@ class echo_op;
// Read a line and echo it back
//
template<class AsyncStream, class CompletionToken>
BOOST_ASIO_INITFN_RESULT_TYPE(CompletionToken, void(boost::beast::error_code))
async_echo(AsyncStream& stream, CompletionToken&& token)
template<
class AsyncStream,
class DynamicBuffer,
class CompletionToken>
auto
async_echo(
AsyncStream& stream,
DynamicBuffer& buffer,
CompletionToken&& token) ->
typename net::async_result<
typename std::decay<CompletionToken>::type,
void(beast::error_code)>::return_type /*< The completion handler signature goes here >*/
{
// Make sure stream meets the requirements. We use static_assert
// to cause a friendly message instead of an error novel.
//
static_assert(boost::beast::is_async_stream<AsyncStream>::value,
// Perform some type checks using static assert, this helps
// with more friendly error messages when passing the wrong types.
static_assert(
beast::is_async_stream<AsyncStream>::value,
"AsyncStream requirements not met");
static_assert(
net::is_dynamic_buffer<DynamicBuffer>::value,
"DynamicBuffer requirements not met");
// This helper manages some of the handler's lifetime and
// uses the result and handler specializations associated with
// the completion token to help customize the return value.
//
boost::asio::async_completion<CompletionToken, void(boost::beast::error_code)> init{token};
// This class template deduces the actual handler type from a
// CompletionToken, captures a local reference to the handler,
// and creates the `async_result` object which becomes the
// return value of this initiating function.
net::async_completion<CompletionToken, void(beast::error_code)> init(token);
// The helper macro BOOST_ASIO_HANDLER_TYPE converts the completion
// token type into a concrete handler type of the correct signature.
using handler_type = BOOST_ASIO_HANDLER_TYPE(CompletionToken, void(beast::error_code));
// The class template `async_op_base` holds the caller's completion
// handler for us, and provides all of the boilerplate for forwarding
// the associated allocator and associated executor from the caller's
// handler to our operation. We declare this type alias to make the
// code easier to read.
using base_type = beast::async_op_base<
handler_type, /*< The type of the completion handler obtained from the token >*/
beast::executor_type<AsyncStream> /*< The type of executor used by the stream to dispatch asynchronous operations >*/
>;
// This example uses the Asio's stackless "fauxroutines", implemented
// using a macro-based solution. It makes the code easier to write and
// easier to read. This include file defines the necessary macros and types.
#include <boost/asio/yield.hpp>
// This nested class implements the echo composed operation as a
// stateful completion handler. We derive from `async_op_base` to
// take care of boilerplate and we derived from net::coroutine to
// allow the reenter and yield keywords to work.
struct echo_op : base_type, net::coroutine
{
AsyncStream& stream_;
DynamicBuffer& buffer_;
echo_op(
AsyncStream& stream,
DynamicBuffer& buffer,
handler_type&& handler)
: base_type(
std::move(handler), /*< The `async_op_base` helper takes ownership of the handler, >*/
stream.get_executor()) /*< and also needs to know which executor to use. >*/
, stream_(stream)
, buffer_(buffer)
{
// Launch the operation directly from the constructor. We
// pass `false` for `cont` to indicate that the calling
// thread does not represent a continuation of our
// asynchronous control flow.
(*this)({}, 0, false);
}
// This is the type of buffer sequence used to represent the
// readable bytes of the dynamic buffer_. We declare a type alias
// to make the code easier to read.
using const_buffers_type = typename DynamicBuffer::const_buffers_type;
// If a newline is present in the buffer sequence, this function returns
// the number of characters from the beginning of the buffer up to the
// newline, including the newline character. Otherwise it returns zero.
std::size_t
find_newline(const_buffers_type const& buffers)
{
// The `buffers_iterator` class template provides random-access
// iterators into a buffer sequence. Use the standard algorithm
// to look for the new line if it exists.
auto begin = net::buffers_iterator<const_buffers_type>::begin(buffers);
auto end = net::buffers_iterator<const_buffers_type>::end(buffers);
auto result = std::find(begin, end, '\n');
if(result == end)
return 0; // not found
return result + 1 - begin;
}
// This is the entry point of our completion handler. Every time an
// asynchronous operation completes, this function will be invoked.
void
operator()(
beast::error_code ec,
std::size_t bytes_transferred = 0,
bool cont = true) /*< Second and subsequent invocations will seee `cont=true`. */
{
// The `reenter` keyword transfers control to the last
// yield point, or to the beginning of the scope if
// this is the first time.
reenter(*this)
{
for(;;)
{
std::size_t pos;
// Search for a newline in the readable bytes of the buffer
pos = find_newline(buffer_.data());
// If we don't have the newline, then read more
if(pos == 0)
{
std::size_t bytes_to_read;
// Determine the number of bytes to read,
// using available capacity in the buffer first.
bytes_to_read = std::min<std::size_t>(
std::max<std::size_t>(512, // under 512 is too little,
buffer_.capacity() - buffer_.size()),
std::min<std::size_t>(65536, // and over 65536 is too much.
buffer_.max_size() - buffer_.size()));
// Read some data into our dynamic buffer_. We transfer
// ownership of the composed operation by using the
// `std::move(*this)` idiom. The `yield` keyword causes
// the function to return immediately after the initiating
// function returns.
yield stream_.async_read_some(
buffer_.prepare(bytes_to_read), std::move(*this));
// After the `async_read_some` completes, control is
// transferred to this line by the `reenter` keyword.
// Move the bytes read from the writable area to the
// readable area.
buffer_.commit(bytes_transferred);
// If an error occurs, deliver it to the caller's completion handler.
if(ec)
break;
// Keep looping until we get the newline
continue;
}
// We have our newline, so send the first `pos` bytes of the
// buffers. The function `buffers_prefix` returns the front part
// of the buffers we want.
yield net::async_write(stream_,
beast::buffers_prefix(pos, buffer_.data()), std::move(*this));
// After the `async_write` completes, our completion handler will
// be invoked with the error and the number of bytes transferred,
// and the `reenter` statement above will cause control to jump
// to the following line. The variable `pos` is no longer valid
// (remember that we returned from the function using `yield` above)
// but we can use `bytes_transferred` to know how much of the buffer
// to consume. With "real" coroutines this will be easier and more
// natural.
buffer_.consume(bytes_transferred);
// The loop terminates here, and we will either deliver a
// successful result or an error to the caller's completion handler.
break;
}
// When a composed operation completes immediately, it must not
// directly invoke the completion handler otherwise it could
// lead to unfairness, starvation, or stack overflow. Therefore,
// if cont == false (meaning, that the call stack still includes
// the frame of the initiating function) then use `net::post`
// to cause us to be called again after the initiating function
// returns. The function `bind_handler` works similarly to
// `std::bind`, allowing bound arguments to be passed to our
// completion handler during the dispatch, but also takes care
// of forwarding the allocator and executor customization points
// in the returned call wrapper.
if(! cont)
yield net::post(beast::bind_handler(std::move(*this), ec));
// The function `async_op_base::invoke` takes care of calling
// the final completion handler.
this->invoke(ec);
}
}
};
// Including this file undefines the macros used by the stackless fauxroutines.
#include <boost/asio/yield.hpp>
// Create the composed operation and launch it. This is a constructor
// call followed by invocation of operator(). We use BOOST_ASIO_HANDLER_TYPE
// to convert the completion token into the correct handler type,
// allowing user-defined specializations of the async_result template
// to be used.
//
echo_op<
AsyncStream,
BOOST_ASIO_HANDLER_TYPE(
CompletionToken, void(boost::beast::error_code))>{
stream,
std::move(init.completion_handler)}({}, 0);
echo_op(stream, buffer, std::move(init.completion_handler));
// This hook lets the caller see a return value when appropriate.
// For example this might return std::future<error_code> if
// CompletionToken is boost::asio::use_future, or this might
// CompletionToken is net::use_future, or this might
// return an error code if CompletionToken specifies a coroutine.
//
return init.result.get();
}
@@ -294,7 +342,7 @@ struct move_only_handler
move_only_handler(move_only_handler&&) = default;
move_only_handler(move_only_handler const&) = delete;
void operator()(boost::beast::error_code ec)
void operator()(beast::error_code ec)
{
if(ec)
std::cerr << ": " << ec.message() << std::endl;
@@ -312,24 +360,25 @@ int main(int argc, char** argv)
return EXIT_FAILURE;
}
auto const address{boost::asio::ip::make_address(argv[1])};
namespace net = boost::asio;
auto const address{net::ip::make_address(argv[1])};
auto const port{static_cast<unsigned short>(std::atoi(argv[2]))};
using socket_type = boost::asio::ip::tcp::socket;
using endpoint_type = boost::asio::ip::tcp::endpoint;
using socket_type = net::ip::tcp::socket;
using endpoint_type = net::ip::tcp::endpoint;
// Create a listening socket, accept a connection, perform
// the echo, and then shut everything down and exit.
boost::asio::io_context ioc;
socket_type sock{ioc};
boost::asio::ip::tcp::acceptor acceptor{ioc};
net::io_context ioc;
net::ip::tcp::acceptor acceptor{ioc};
endpoint_type ep{address, port};
acceptor.open(ep.protocol());
acceptor.set_option(boost::asio::socket_base::reuse_address(true));
acceptor.set_option(net::socket_base::reuse_address(true));
acceptor.bind(ep);
acceptor.listen();
acceptor.accept(sock);
async_echo(sock, move_only_handler{});
auto sock = acceptor.accept();
beast::flat_buffer buffer;
async_echo(sock, buffer, move_only_handler{});
ioc.run();
return EXIT_SUCCESS;
}