2
\$\begingroup\$

I'm using (non-boost) Asio 1.18.1 and C++17. Forgive the boost tag, there wasn't a more specific one.

I have a async_connect_with_retry composed asynchronous operation:

/// Calls resolver.async_resolve(...), async_connect(socket, ...), and (on
/// failure) timer.async_wait(...) to start over.
template <typename Timer, typename Token>
auto async_connect_with_retry(Timer& timer, asio::ip::tcp::socket& socket,
                                 asio::ip::tcp::resolver& resolver,
                                 std::string_view host,
                                 std::string_view service,
                                 typename Timer::duration retry_delay,
                                 Token token);

I'm struggling to turn this into a reusable and extensible reconnecting_socket TCP client socket class (or possibly another composed op, if its more ergonomic). This is useful for connecting to embedded systems, for instance.

My main focus is ease of use for the person extending the class, especially for new-to-Asio users.

I am not interested in making everything generic yet (e.g. it's fine that I use ip::tcp::socket and not basic_socket<Protocol, Executor>). That can come later.

Feedback on the async_ methods is welcome, but I'm mostly interested in reconnecting_socket.h.

The biggest unsolved problems:

  1. How do I give the user a customization point in the reconnecting_socket::handle_connect method? The user could start an async_read loop, and maybe a async_write heartbeat loop.
    • Extending the class (and making socket_ protected) makes using shared_from_this() to extend the object's lifetime tricky to get right.
    • Providing a callback has the same lifetime issues, and now we need a getter for socket_ if they want to write anything.
  2. How does the "reconnect" get triggered? The user could call close() if they get an error, but then if they have a write loop and a read loop, and both fail, they'll probably call close() twice, which would cause problems (two start_connect calls, multiple closes, etc.). Plus with this scheme the user has to remember to call close(). Maybe that's an OK tradeoff, though.

reconnecting_socket.h

#pragma once

#include <iostream>

#include "asio.hpp"
#include "async_connect_with_retry.h"

class reconnecting_socket
    : public std::enable_shared_from_this<reconnecting_socket> {
 public:
  using ptr = std::shared_ptr<reconnecting_socket>;
  using tcp = asio::ip::tcp;
  using timer = asio::high_resolution_timer;

  reconnecting_socket(asio::io_context& io_context, timer::duration retry_delay)
      : io_context_(io_context),
        socket_(io_context_),
        resolver_(io_context_),
        reconnect_timer_(io_context_),
        retry_delay_(retry_delay) {}

  void start(std::string_view host, std::string_view service) {
    assert(!started_);
    started_ = true;
    std::cout << "starting\n";
    host_ = host;
    service_ = service;
    start_connect();
  }

  void stop() {
    end_ = true;
    close(false);
  }

 private:
  bool started_ = false;
  bool end_ = false;
  asio::io_context& io_context_;
  tcp::socket socket_;
  tcp::resolver resolver_;
  timer reconnect_timer_;
  timer::duration retry_delay_;
  std::string_view host_;
  std::string_view service_;

  void close(bool reconnect = true) {
    asio::post(io_context_, [self = shared_from_this(), reconnect] {
      std::error_code ignored;
      self->socket_.shutdown(tcp::socket::shutdown_both, ignored);
      self->socket_.close(ignored);
      if (reconnect) self->start_connect();
    });
  }

  void start_connect() {
    if (end_) return;
    async_connect_with_retry(
        reconnect_timer_, socket_, resolver_, host_, service_, retry_delay_,
        [self = shared_from_this()](std::error_code error, auto endpoint) {
          self->handle_connect(error, endpoint);
        });
  }

  void handle_connect(std::error_code error, tcp::endpoint endpoint) {
    if (!error) {
      std::cout << "connected to " << endpoint << "\n";
      // @todo how does the user customize this connection?
    }
  }
};

async_connect_with_retry.h

#pragma once

#include "asio.hpp" // @todo include what you use
#include "async_connect_to.h"

namespace detail {
template <typename Timer>
struct async_connect_with_retry_to_impl {
  Timer& timer_;
  asio::ip::tcp::socket& socket_;
  asio::ip::tcp::resolver& resolver_;
  std::string_view host_;
  std::string_view service_;
  typename Timer::duration retry_delay_;

  // overload: starting
  template <typename Self>
  void operator()(Self& self) {
    async_connect_to(socket_, resolver_, host_, service_, std::move(self));
  }

  // overload: async_connect_to handler
  template <typename Self>
  void operator()(Self& self, std::error_code error,
                  asio::ip::tcp::endpoint endpoint) {
    if (error) {
      timer_.expires_after(retry_delay_);
      timer_.async_wait(std::move(self));
    } else {
      self.complete(error, endpoint);
    }
  }

  // overload: async_wait handler
  template <typename Self>
  void operator()(Self& self, std::error_code error) {
    if (error) {
      self.complete(error, {});
    } else {
      async_connect_to(socket_, resolver_, host_, service_, std::move(self));
    }
  }
};
}  // namespace detail

/// Calls resolver.async_resolve(...), async_connect(socket, ...), and (on
/// failure) timer.async_wait(...) to start over.
template <typename Timer, typename Token>
auto async_connect_with_retry(Timer& timer, asio::ip::tcp::socket& socket,
                                 asio::ip::tcp::resolver& resolver,
                                 std::string_view host,
                                 std::string_view service,
                                 typename Timer::duration retry_delay,
                                 Token token) {
  return asio::async_compose<Token,
                             void(std::error_code, asio::ip::tcp::endpoint)>(
      detail::async_connect_with_retry_to_impl<Timer>{
          timer, socket, resolver, host, service, retry_delay},
      token);
}

async_connect_to.h

#pragma once

#include "asio.hpp" // @todo include what you use

namespace detail {
struct async_connect_to_impl {
  asio::ip::tcp::socket& socket_;
  asio::ip::tcp::resolver& resolver_;
  std::string_view host_;
  std::string_view service_;

  // overload: starting
  template <typename Self>
  void operator()(Self& self) {
    resolver_.async_resolve(host_, service_, std::move(self));
  }

  // overload: async_resolve handler
  template <typename Self>
  void operator()(Self& self, std::error_code error,
                  asio::ip::tcp::resolver::results_type results) {
    if (error) {
      self.complete(error, {});
    } else {
      asio::async_connect(socket_, results, std::move(self));
    }
  }

  // overload: async_connect handler
  template <typename Self>
  void operator()(Self& self, std::error_code error,
                  asio::ip::tcp::endpoint endpoint) {
    self.complete(error, std::move(endpoint));
  }
};
}  // namespace detail

/// Connect to a given address and port. Will resolve URIs.
template <typename Token>
auto async_connect_to(asio::ip::tcp::socket& socket,
                      asio::ip::tcp::resolver& resolver, std::string_view host,
                      std::string_view service, Token&& token) {
  return asio::async_compose<Token,
                             void(std::error_code, asio::ip::tcp::endpoint)>(
      detail::async_connect_to_impl{socket, resolver, host, service}, token);
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.