此示例开发了一个名为 echo 的启动函数。 此操作将读取流上的第一个换行符,然后将同一行(包括换行符)写回流。 首先,我们定义输入参数和结果,然后声明我们的启动函数。 对于我们的 echo 操作,唯一的输入是流和完成令牌。 输出是错误代码,通常包含在所有完成处理程序签名中。
/** Asynchronously read a line and echo it back. This function is used to asynchronously read a line ending 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 written back on the stream @li An error occurs. 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. 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 <em>AsyncReadStream</em> and @AsyncWriteStream @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 takes ownership of the handler by performing a decay-copy. The handler must be invocable with this signature: @code void handler( 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 `net::post`. */ 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;
现在我们有了声明,我们将定义函数的主体。 我们希望实现以下目标:对输入参数执行静态类型检查,按照 N3747 设置返回值,并通过构造中间的、有状态的完成处理程序并调用它来启动组合操作。
启动函数包含几个相对简单的部分。 有返回值类型的自定义、静态类型检查、使用辅助工具构建返回值类型,以及创建和启动 echo_op
组合操作对象。
实现策略是使组合对象满足作为完成处理程序的要求,即可移动,并且可调用,以便它可以作为其启动的异步操作的延续使用。 我们将简单地传递 std::move(*this)
作为我们启动的任何操作的完成处理程序参数,而不是使用 std::bind
或 boost::bind
,这会破坏类型信息,从而破坏关联器。 为了使移动正确工作,必须注意确保在移动发生后不访问数据成员。 这是我们的组合操作的完整实现
template<class AsyncStream, class Handler> class echo_op; // 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> // Read a line and echo it back // 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{ // 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 type requirements not met"); static_assert( net::is_dynamic_buffer<DynamicBuffer>::value, "DynamicBuffer type requirements not met"); // 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_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. It also maintains a `net::executor_work_guard` // for the executor associated with the stream. This work guard is // inexpensive, and prevents the execution context from running out // of work. It is usually necessary although rarely it can be skipped // depending on the operation (this echo example needs it because it // performs more than one asynchronous operation in a row). // We declare this type alias to make the code easier to read. using base_type = beast::async_base< handler_type,
beast::executor_type<AsyncStream>
>; // This nested class implements the echo composed operation as a // stateful completion handler. We derive from `async_base` to // take care of boilerplate and we derived from asio::coroutine to // allow the reenter and yield keywords to work. struct echo_op : base_type, boost::asio::coroutine { AsyncStream& stream_; DynamicBuffer& buffer_; echo_op( AsyncStream& stream, DynamicBuffer& buffer, handler_type&& handler) : base_type( std::move(handler),
stream.get_executor())
, 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); } // 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(typename DynamicBuffer::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< typename DynamicBuffer::const_buffers_type>::begin(buffers); auto end = net::buffers_iterator< typename DynamicBuffer::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 we need to use // `net::post` to cause us to be called again after the initiating // function. The function `async_base::invoke` takes care of // calling the final completion handler, using post if the // first argument is false, otherwise invoking it directly. this->complete(cont, ec); } } }; // 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(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 net::use_future, or this might // return an error code if CompletionToken specifies a coroutine. return init.result.get(); } // Including this file undefines the macros used by the stackless fauxroutines. #include <boost/asio/unyield.hpp>
编写组合操作时应避免一些常见错误
net::io_context
上运行的线程。executor_type
和 get_executor
。 这将导致未定义的行为。 例如,如果有人使用 strand 包装的函数对象调用启动函数,并且在 net::io_context
上运行多个线程,则可能会以违反安全保证的方式访问底层流。 Beast 提供了类模板来处理这些样板代码。get_executor
成员函数返回的执行器类型创建 net::executor_work_guard
类型的对象。net::post
来调用最终处理程序。 这违反了以下启动函数保证:无论异步操作是否立即完成,都不会从该函数内部调用处理程序。 处理程序的调用方式将等同于使用 net::post
。 提供了函数 bind_handler
以实现此目的。此示例的完整可运行版本的清单在 echo_op.cpp 中。