Boost.Asio source reading: services and asynchronous operations

Keywords: C++ socket Windows network REST

Based on Boost 1.69, this article cuts out some deprecated or different configured code blocks that are not related to the topic of this article when displaying the source code.

Service type

  • resolving service
  • socket/file operation
  • timer
  • ...

service resolution

Here, take the reactive socket service that provides services around reactor as an example to introduce service.

// file: <boost/asio/detail/reactive_socket_service.hpp>
...
template <typename Protocol>
class reactive_socket_service :
  public service_base<reactive_socket_service<Protocol> >,
  public reactive_socket_service_base
...

(service registration) service ID

The reactive socket service inherits the service base < reactive socket service < protocol > > as described earlier, which contains a static member id. Since the subclass is the parameter of the parent template (recursive template pattern), each class that inherits the service ﹣ base contains a different static member id. This id can be used to register the service.

// file: <boost/asio/io_context.hpp>
...
template <typename Type>
class service_base
  : public boost::asio::io_context::service
{
public:
  static boost::asio::detail::service_id<Type> id;
  // Constructor.
  service_base(boost::asio::io_context& io_context)
    : boost::asio::io_context::service(io_context){}
};
...

Main member function of a service

Reactive socket service inherits reactive socket service base. This parent class has implemented quite a lot of functions. The members of reactive socket service base are IO context reference and reactor reference. Its core member function is reactive socket service base start OP, which transfers the specific impl (native socket handle and its status, native socket handle related reactor data) and op (operation) to reactor. It is worth noting that if noop is true, it indicates that this is a general operation, and reactor will submit the task to the scheduler for completion. The concepts of OP and handler will be explained later in this article.

// file: <boost/asio/detail/impl/reactive_socket_service_base.ipp>
...
void reactive_socket_service_base::start_op(
    reactive_socket_service_base::base_implementation_type& impl,
    int op_type, reactor_op* op, bool is_continuation,
    bool is_non_blocking, bool noop)
{
  if (!noop)
  {
    if ((impl.state_ & socket_ops::non_blocking)
        || socket_ops::set_internal_non_blocking(
          impl.socket_, impl.state_, true, op->ec_))
    {
      reactor_.start_op(op_type, impl.socket_,
          impl.reactor_data_, op, is_continuation, is_non_blocking);
      return;
    }
  }

  reactor_.post_immediate_completion(op, is_continuation);
}
...

This paper introduces the reactive socket service base start op. let's take a look at a member function related to socket io operation reactive socket service base async send. The async send member function itself is very simple. Its parameters are mainly socket (impl), data (buffers) and callback (handler). By wrapping any type of handler into reactive socket (send) op, the task is passed to reactor by calling start Op. The last parameter of the start? OP member function is noop. When socket (impl) is stream? Oriented and buffers are empty (that is, noop=true), the task will be directly handed to the scheduler.

// file: <boost/asio/detail/impl/reactive_socket_service_base.ipp>
...
// Start an asynchronous send. The data being sent must be valid for the
// lifetime of the asynchronous operation.
template <typename ConstBufferSequence, typename Handler>
void async_send(base_implementation_type& impl,
    const ConstBufferSequence& buffers,
    socket_base::message_flags flags, Handler& handler)
{
bool is_continuation =
    boost_asio_handler_cont_helpers::is_continuation(handler);

// Allocate and construct an operation to wrap the handler.
typedef reactive_socket_send_op<ConstBufferSequence, Handler> op;
typename op::ptr p = { boost::asio::detail::addressof(handler),
    op::ptr::allocate(handler), 0 };
p.p = new (p.v) op(impl.socket_, impl.state_, buffers, flags, handler);

BOOST_ASIO_HANDLER_CREATION((reactor_.context(), *p.p, "socket",
        &impl, impl.socket_, "async_send"));

start_op(impl, reactor::write_op, p.p, is_continuation, true,
    ((impl.state_ & socket_ops::stream_oriented)
        && buffer_sequence_adapter<boost::asio::const_buffer,
        ConstBufferSequence>::all_empty(buffers)));
p.v = p.p = 0;
}
...

Using service

As a user of Boost.Asio, if you want to implement asynchronous tcp server, you can't do without the corresponding boost::asio::ip::tcp::socket (i.e. basic stream \ u socket < tcp >), here we will introduce the use of service. Looking at the source code, we can see that basic stream socket inherits basic socket and basic socket inherits basic IO object. Among them, boost ﹣ ASIO ﹣ SVC ﹣ is the type of service, which is defined as detail:: reactive ﹣ socket ﹣ service < protocol > in platforms other than windows.

// file: <boost/asio/basic_stream_socket.hpp>
...
template <typename Protocol
    BOOST_ASIO_SVC_TPARAM_DEF1(= stream_socket_service<Protocol>)>
class basic_stream_socket
  : public basic_socket<Protocol BOOST_ASIO_SVC_TARG>
...
// file: <boost/asio/basic_socket.hpp>
...
#if !defined(BOOST_ASIO_ENABLE_OLD_SERVICES)
# if defined(BOOST_ASIO_WINDOWS_RUNTIME)
#  include <boost/asio/detail/winrt_ssocket_service.hpp>
#  define BOOST_ASIO_SVC_T detail::winrt_ssocket_service<Protocol>
# elif defined(BOOST_ASIO_HAS_IOCP)
#  include <boost/asio/detail/win_iocp_socket_service.hpp>
#  define BOOST_ASIO_SVC_T detail::win_iocp_socket_service<Protocol>
# else
#  include <boost/asio/detail/reactive_socket_service.hpp>
#  define BOOST_ASIO_SVC_T detail::reactive_socket_service<Protocol>
# endif
#endif // !defined(BOOST_ASIO_ENABLE_OLD_SERVICES)
...
template <typename Protocol BOOST_ASIO_SVC_TPARAM>
class basic_socket
  : BOOST_ASIO_SVC_ACCESS basic_io_object<BOOST_ASIO_SVC_T>,
    public socket_base
...

Looking at the source code of the basic IO object, we can see that the basic IO object consists of two members: service and implementation. The implementation contains socket native handler and related state and handler related reactor data. The service is built by the boost:: ASIO:: use UU service < ioobjectservice > (IO context) function. The use UU service queries the service queue of the IO context to return the corresponding service If there is no corresponding service in the queue, IO context will build a new service.

// file: <boost/asio/basic_io_object.hpp>
...
#if !defined(BOOST_ASIO_HAS_MOVE) || defined(GENERATING_DOCUMENTATION)
template <typename IoObjectService>
#else
template <typename IoObjectService,
    bool Movable = detail::service_has_move<IoObjectService>::value>
#endif
class basic_io_object
{
public:
  /// The type of the service that will be used to provide I/O operations.
  typedef IoObjectService service_type;
  /// The underlying implementation type of I/O object.
  typedef typename service_type::implementation_type implementation_type;
...
protected:
...
  explicit basic_io_object(boost::asio::io_context& io_context)
    : service_(boost::asio::use_service<IoObjectService>(io_context))
  {
    service_.construct(implementation_);
  }
...
  /// Get the service associated with the I/O object.
  service_type& get_service()
  {
    return service_;
  }
...
  /// Get the underlying implementation of the I/O object.
  implementation_type& get_implementation()
  {
    return implementation_;
  }
...
private:
...
  // The service associated with the I/O object.
  service_type& service_;
  /// The underlying implementation of the I/O object.
  implementation_type implementation_;
};
...

Above, we learned how boost::asio::ip::tcp::socket gets services. Let's see how to use services. Take the async send of boost::asio::ip::tcp::socket as an example, and check the source code to see that async send calls the relevant async member functions of the service, and passes in the implementation, data and callback. The handling of the handler itself will be explained later.

// file: <boost/asio/basic_stream_socket.hpp>
...
template <typename ConstBufferSequence, typename WriteHandler>
  BOOST_ASIO_INITFN_RESULT_TYPE(WriteHandler,
      void (boost::system::error_code, std::size_t))
  async_send(const ConstBufferSequence& buffers,
      socket_base::message_flags flags,
      BOOST_ASIO_MOVE_ARG(WriteHandler) handler)
  {
    // If you get an error on the following line it means that your handler does
    // not meet the documented type requirements for a WriteHandler.
    BOOST_ASIO_WRITE_HANDLER_CHECK(WriteHandler, handler) type_check;
...
    async_completion<WriteHandler,
      void (boost::system::error_code, std::size_t)> init(handler);
    this->get_service().async_send(
        this->get_implementation(), buffers, flags,
        init.completion_handler);
    return init.result.get();
...
  }
...

Asynchronous operation

For asynchronous io, the user's "operation" of network data needs to be saved, and the "operation" will be called automatically after io reaches a certain state. For this process, I will briefly introduce related concepts and implementation, and list some documents for readers to understand and learn in depth.

Boost.Asio introduces a unified asynchronous model N3747. A Universal Model for Asynchronous Operations The concepts of asynchronous operation and unified model are discussed in detail. From article n3747, it can be seen that the asynchronous operation of C + + includes the types of callbacks, future, resumable functions and collaborations. Due to the limited space, this article does not repeat the conceptual content in the article N3747, but focuses on introducing the specific implementation of the model by analyzing the source code of Asio.

Internal assembly and life cycle management

First of all, from the perspective of Asio (scheduler operation queue, reactor operation queue, etc.), we can understand how asynchronous operations are implemented in Asio. First, we focus on two classes: scheduler? Operation and executor? Op. The scheduler operation is a common class, while the executor OP is a class template and inherits the scheduler operation by default (the inheritance relationship is important here). The combination of these two classes has an interesting effect, in which

  • Scheduler operation: callback interface, type erasure
  • Executor? OP: class template, responsible for generating diversity and passing it to the parent class scheduler? Operation

Specifically, the executor ﹣ OP contains a static member function do ﹣ complete. base (void *) cast is the executor ﹣ OP *, and the scheduler ﹣ operation has a data member function ﹣ of type function pointer. During the construction of executor OP, the address of do complete, a static member function, is assigned to the data member function pointer of the scheduler operation. When the scheduler operation executes the completion, it will call this function to restore itself (CAST) to the instantiated executor OP *. Through subclass template and subclass static method, and the data member function pointer of parent class and parent class, the function of type erasure and recurrence is realized.

Scheduler operation and executor OP, life cycle management. The member function complete of the parent scheduler operation destroys the object and completes the callback (func (owner,...)), and destroy only destroys the object (func (0,...)), while the destructor of the parent is not a virtual function and the function body is empty. View the static member function do? Complete of executor? OP, and its life management process is as follows:

  • Get the allocator of o (type executor? OP *)
  • Create a ptr class instance p to manage the o lifecycle, noting that the destructor of P automatically destroys o if it has not been destroyed before.
  • O's data member handler'u move to a local variable, p.reset() destroys o
  • Whether to invoke handler depends on the value of owner_
// file: <boost/asio/detail/executor_op.hpp>
...
template <typename Handler, typename Alloc,
    typename Operation = scheduler_operation>
class executor_op : public Operation
{
public:
  BOOST_ASIO_DEFINE_HANDLER_ALLOCATOR_PTR(executor_op);

  template <typename H>
  executor_op(BOOST_ASIO_MOVE_ARG(H) h, const Alloc& allocator)
    : Operation(&executor_op::do_complete),
      handler_(BOOST_ASIO_MOVE_CAST(H)(h)),
      allocator_(allocator)
  {
  }

  static void do_complete(void* owner, Operation* base,
      const boost::system::error_code& /*ec*/,
      std::size_t /*bytes_transferred*/)
  {
    // Take ownership of the handler object.
    executor_op* o(static_cast<executor_op*>(base));
    Alloc allocator(o->allocator_);
    ptr p = { detail::addressof(allocator), o, o };
    ...
    Handler handler(BOOST_ASIO_MOVE_CAST(Handler)(o->handler_));
    p.reset();

    // Make the upcall if required.
    if (owner)
    {
    ...
    }
  }

private:
  Handler handler_;
  Alloc allocator_;
};
...
// file: <boost/asio/detail/scheduler_operation.hpp>
...
class scheduler_operation BOOST_ASIO_INHERIT_TRACKED_HANDLER
{
public:
  typedef scheduler_operation operation_type;

  void complete(void* owner, const boost::system::error_code& ec,
      std::size_t bytes_transferred)
  {
    func_(owner, this, ec, bytes_transferred);
  }

  void destroy()
  {
    func_(0, this, boost::system::error_code(), 0);
  }

protected:
  typedef void (*func_type)(void*,
      scheduler_operation*,
      const boost::system::error_code&, std::size_t);

  scheduler_operation(func_type func)
    : next_(0),
      func_(func),
      task_result_(0)
  {
  }

  // Prevents deletion through this type.
  ~scheduler_operation()
  {
  }

private:
  friend class op_queue_access;
  scheduler_operation* next_;
  func_type func_;
protected:
  friend class scheduler;
  unsigned int task_result_; // Passed into bytes transferred.
};

Interface and unified model

In the previous section, we have roughly understood the method of Asio core part assembly asynchronous operation by introducing executor? op. other OPS, such as reactor? op and its subclasses, are basically similar to executor? op, and will not be explained here. The rest is user interface and unified model of asynchronous operation.

A typical interface is the boost::asio::post function, which submits asynchronous operations (of any type) to the specified IO context for asynchronous operation. In order to implement the unified model, the function must determine the return value and the true type of the asynchronous operation: the return value. If the user submits a general callback, the return value should be empty. If it is an asynchronous operation of future, it must return the corresponding object; the real type conversion of asynchronous operation, for callback, the type is functor, and the correlation is corresponding to the association. Asio must convert the user submitted parameters to the real asynchronous operation type.

By looking at the function body of boost::asio::post, we find that:

  • Async? Result? Helper is used to determine the type of boost::asio::post return value
  • Async Ou completion performs asynchronous type conversion in the function body to generate the return value

Reading the source code of async'u completion, we find that the async'u completion class contains the data members completion'u handler and result, and the types of these two members are determined by the async'u result class template (the type of result is also async'u result). By default, the async result return value type is empty, and the real asynchronous operation type is directly equal to the asynchronous operation class submitted by the user (the parameter CompletionToken of the boost::asio::post function template). Async ﹣ result does not have any data members by default, so async ﹣ completion's data member result can be optimized by the compiler as the return value of asynchronous operation (the type is async ﹣ result), which avoids additional selling for ordinary callback without return value.

In order to deal with the future type asynchronous operation (return value and real asynchronous operation type are required), you can define async result's specialization on future.

// file: <boost/asio/impl/post.hpp>
...
template <typename Executor, typename CompletionToken>
BOOST_ASIO_INITFN_RESULT_TYPE(CompletionToken, void()) post(
    const Executor& ex, BOOST_ASIO_MOVE_ARG(CompletionToken) token,
    typename enable_if<is_executor<Executor>::value>::type*)
{
  typedef BOOST_ASIO_HANDLER_TYPE(CompletionToken, void()) handler;
  async_completion<CompletionToken, void()> init(token);
  typename associated_allocator<handler>::type alloc(
      (get_associated_allocator)(init.completion_handler));
  ex.post(detail::work_dispatcher<handler>(init.completion_handler), alloc);
  return init.result.get();
}
...
// file: <boost/asio/async_result.hpp>
...
template <typename CompletionToken, typename Signature>
struct async_completion
{
  /// The real handler type to be used for the asynchronous operation.
  typedef typename boost::asio::async_result<
    typename decay<CompletionToken>::type,
      Signature>::completion_handler_type completion_handler_type;
...
  /// Constructor.
  /**
   * The constructor creates the concrete completion handler and makes the link
   * between the handler and the asynchronous result.
   */
  explicit async_completion(CompletionToken& token)
    : completion_handler(static_cast<typename conditional<
        is_same<CompletionToken, completion_handler_type>::value,
        completion_handler_type&, CompletionToken&&>::type>(token)),
      result(completion_handler)
  {
  }
...

  /// A copy of, or reference to, a real handler object.
  typename conditional<
    is_same<CompletionToken, completion_handler_type>::value,
    completion_handler_type&, completion_handler_type>::type completion_handler;
  ...
  /// The result of the asynchronous operation's initiating function.
  async_result<typename decay<CompletionToken>::type, Signature> result;
};
...
template <typename CompletionToken, typename Signature>
class async_result
{
public:
...
  /// The concrete completion handler type for the specific signature.
  typedef CompletionToken completion_handler_type;
...
  /// The return type of the initiating function.
  typedef void return_type;
...
  /// Construct an async result from a given handler.
  /**
   * When using a specalised async_result, the constructor has an opportunity
   * to initialise some state associated with the completion handler, which is
   * then returned from the initiating function.
   */
  explicit async_result(completion_handler_type& h)
#if defined(BOOST_ASIO_NO_DEPRECATED) || defined(GENERATING_DOCUMENTATION)
    // No data members to initialise.
  {
    (void)h;
  }
...
  /// Obtain the value to be returned from the initiating function.
  return_type get()
  {
  ...
    // Nothing to do.
  ...
  }
  // No data members.
};
...

Posted by phpforever on Tue, 10 Dec 2019 11:49:23 -0800