forked from boostorg/beast
Documentation work
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
[/
|
||||
Copyright (c) 2013-2017 Vinnie Falco (vinnie dot falco at gmail dot com)
|
||||
|
||||
Distributed under the Boost Software License, Version 1.0. (See accompanying
|
||||
file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
|
||||
]
|
||||
|
||||
[section:tutorial Writing Composed Operations]
|
||||
|
||||
To illustrate the usage of the asynchronous helpers in the core section of
|
||||
this library, we will develop a simple asynchronous composed operation called
|
||||
[*echo]. This operation will read up to the first newline on a stream, and
|
||||
then write the same line including the newline back on the stream.
|
||||
|
||||
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 is usually included in
|
||||
all completion handler signatures.
|
||||
|
||||
```
|
||||
#include <beast/core.hpp>
|
||||
#include <boost/asio.hpp>
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
// Read a line and echo it back
|
||||
//
|
||||
template<class AsyncStream, class CompletionToken>
|
||||
beast::async_return_type<CompletionToken, void(beast::error_code)>
|
||||
async_echo(AsyncStream& stream, CompletionToken&& token)
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
template<class AsyncStream, class Handler>
|
||||
class echo_op; // This is our composed operation implementation
|
||||
|
||||
// Read a line and echo it back
|
||||
//
|
||||
template<class AsyncStream, class CompletionToken>
|
||||
beast::async_return_type<CompletionToken, void(beast::error_code)>
|
||||
async_echo(AsyncStream& stream, CompletionToken&& token)
|
||||
{
|
||||
// Make sure stream meets the requirements. We use static_assert
|
||||
// to cause a friendly message instead of an error novel.
|
||||
//
|
||||
static_assert(beast::is_async_stream<AsyncStream>::value,
|
||||
"AsyncStream 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.
|
||||
//
|
||||
beast::async_completion<CompletionToken, void(beast::error_code)> init{token};
|
||||
|
||||
// Create the composed operation and launch it. This is a constructor
|
||||
// call followed by invocation of operator(). We use handler_type
|
||||
// to convert the completion token into the correct handler type,
|
||||
// allowing user defined specializations of the async result template
|
||||
// to take effect.
|
||||
//
|
||||
echo_op<AsyncStream, beast::handler_type<CompletionToken, void(beast::error_code)>>{
|
||||
stream, init.completion_handler}(beast::error_code{}, 0);
|
||||
|
||||
// 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
|
||||
// return an error code if CompletionToken specifies a coroutine.
|
||||
//
|
||||
return init.result.get();
|
||||
}
|
||||
```
|
||||
|
||||
The initiation 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.
|
||||
|
||||
First we will create boilerplate which is present in all composed operations
|
||||
written in this style:
|
||||
|
||||
```
|
||||
// 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;
|
||||
|
||||
// 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.
|
||||
// Note that we use a custom allocator for performance,
|
||||
// this allows the implementation of the io_service to
|
||||
// make efficient re-use of memory allocated by composed
|
||||
// operations during continuations.
|
||||
//
|
||||
boost::asio::basic_streambuf<beast::handler_alloc<char, Handler>> 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& handler, AsyncStream& stream_)
|
||||
: stream(stream_)
|
||||
, buffer((std::numeric_limits<std::size_t>::max)(),
|
||||
beast::handler_alloc<char, Handler>{handler})
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
// This smart pointer container allocates our state using the
|
||||
// memory allocation hooks associated with the final completion
|
||||
// handler, manages the lifetime of that handler for us, and
|
||||
// enforces the destroy-before-invocation requirement on memory
|
||||
// allocated using the hooks.
|
||||
//
|
||||
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)
|
||||
{
|
||||
}
|
||||
|
||||
// Determines if the next asynchronous operation represents a
|
||||
// continuation of the asynchronous flow of control associated
|
||||
// with the final handler. If we are past step one, it means
|
||||
// we have performed an asynchronous operation therefore any
|
||||
// subsequent operation would represent a continuation.
|
||||
// Otherwise, we propagate the handler's associated value of
|
||||
// is_continuation. Getting this right means the implementation
|
||||
// may schedule the invokation of the invoked functions more
|
||||
// efficiently.
|
||||
//
|
||||
friend bool asio_handler_is_continuation(echo_op* op)
|
||||
{
|
||||
// This next call is structured to permit argument
|
||||
// dependent lookup to take effect.
|
||||
using boost::asio::asio_handler_is_continuation;
|
||||
|
||||
// Always use std::addressof to pass the pointer to the handler,
|
||||
// otherwise an unwanted overload of operator& may be called instead.
|
||||
return op->p_->step > 1 ||
|
||||
asio_handler_is_continuation(std::addressof(op->p_.handler()));
|
||||
}
|
||||
|
||||
// Handler hook forwarding. These free functions invoke the hooks
|
||||
// associated with the final completion handler. In effect, they
|
||||
// make the Asio implementation treat our composed operation the
|
||||
// same way it would treat the final completion handler for the
|
||||
// purpose of memory allocation and invocation.
|
||||
//
|
||||
// Our implementation just passes through the call to the hook
|
||||
// associated with the final handler.
|
||||
|
||||
friend void* asio_handler_allocate(std::size_t size, echo_op* op)
|
||||
{
|
||||
using boost::asio::asio_handler_allocate;
|
||||
return asio_handler_allocate(size, std::addressof(op->p_.handler()));
|
||||
}
|
||||
|
||||
friend void asio_handler_deallocate(void* p, std::size_t size, echo_op* op)
|
||||
{
|
||||
using boost::asio::asio_handler_deallocate;
|
||||
return asio_handler_deallocate(p, size, std::addressof(op->p_.handler()));
|
||||
}
|
||||
|
||||
template<class Function>
|
||||
friend void asio_handler_invoke(Function&& f, echo_op* op)
|
||||
{
|
||||
using boost::asio::asio_handler_invoke;
|
||||
return asio_handler_invoke(f, std::addressof(op->p_.handler()));
|
||||
}
|
||||
|
||||
// Our main entry point. This will get called as our
|
||||
// intermediate operations complete. Definition below.
|
||||
//
|
||||
void operator()(beast::error_code ec, std::size_t bytes_transferred);
|
||||
};
|
||||
```
|
||||
|
||||
We have the common boilerplate for a composed operation and now we just need
|
||||
to implement the function call operator. Our strategy is to make our composed
|
||||
object meet the requirements of a completion handler by being copyable (also
|
||||
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:
|
||||
```
|
||||
// We are 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()(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 buffer_prefix to make sure we only send one line
|
||||
return boost::asio::async_write(p.stream,
|
||||
beast::buffer_prefix(bytes_transferred, p.buffer.data()), std::move(*this));
|
||||
|
||||
case 2:
|
||||
p.buffer.consume(bytes_transferred);
|
||||
break;
|
||||
}
|
||||
|
||||
// Invoke the final handler. If we wanted to pass any arguments
|
||||
// which come from our state, they would have to be moved to the
|
||||
// stack first, since the `handler_ptr` guarantees that the state
|
||||
// is destroyed before the handler is invoked.
|
||||
//
|
||||
p_.invoke(ec);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
A complete, runnable version of this example may be found in the examples
|
||||
directory.
|
||||
|
||||
[endsect]
|
||||
Reference in New Issue
Block a user