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:
- How do I give the user a customization point in the
reconnecting_socket::handle_connect
method? The user could start anasync_read
loop, and maybe aasync_write
heartbeat loop.- Extending the class (and making
socket_
protected
) makes usingshared_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.
- Extending the class (and making
- 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 callclose()
twice, which would cause problems (twostart_connect
calls, multiple closes, etc.). Plus with this scheme the user has to remember to callclose()
. 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);
}