Search code examples
c++asynchronousboosttcpasio

Elegant way of reconnecting loop with boost::asio?


I am trying to write a very elegant way of handling a reconnect loop with boost async_connect(...). The problem is, I don't see a way how I could elegantly solve the following problem:

I have a TCP client that should try to connect asynchronously to a server, if the connection fails because the server is offline or any other error occurs, wait a given amount of time and try to reconnect. There are multiple things to take into consideration here:

  • Avoidance of global variables if possible
  • It has to be async connect

A very basic client is instantiated like so:

tcpclient::tcpclient(std::string host, int port) : _endpoint(boost::asio::ip::address::from_string(host), port), _socket(_ios) {
    logger::log_info("Initiating client ...");
}

Attempt to connect to the server:

void tcpclient::start() {

    bool is_connected = false;

    while (!is_connected) {
        _socket.async_connect(_endpoint, connect_handler);
        _ios.run();
    }

    // read write data (?)

}

The handler:

void tcpclient::connect_handler(const boost::system::error_code &error) {

    if(error){
        // trigger disconnect (?)
        logger::log_error(error.message());
        return;
    }

    // Connection is established at this point
    // Update timer state and start authentication on server ?

    logger::log_info("Connected?");

}

How can I properly start reconnecting everytime the connection fails (or is dropped)? Since the handler is static I can not modify a class attribute that indicates the connection status? I want to avoid using hacky global variable workarounds. How can I solve this issue in a proper way?

My attempt would be something like this:

tcpclient.h

enum ConnectionStatus{
    NOT_CONNECTED,
    CONNECTED
};


class tcpclient {

public:

    tcpclient(std::string host, int port);

    void start();

private:

    ConnectionStatus _status = NOT_CONNECTED;

    void connect_handler(const boost::system::error_code& error);

    boost::asio::io_service _ios;
    boost::asio::ip::tcp::endpoint _endpoint;
    boost::asio::ip::tcp::socket _socket;

};

tcpclient.cpp

#include "tcpclient.h"
#include <boost/chrono.hpp>
#include "../utils/logger.h"

tcpclient::tcpclient(std::string host, int port) : _endpoint(boost::asio::ip::address::from_string(host), port),
                                                   _socket(_ios) {
    logger::log_info("Initiating client ...");
    logger::log_info("Server endpoint: " + _endpoint.address().to_string());
}


void tcpclient::connect_handler(const boost::system::error_code &error) {

    if(!error){
        _status = CONNECTED;
        logger::log_info("Connected.");
    }
    else{
        _status = NOT_CONNECTED;
        logger::log_info("Failed to connect");
        _socket.close();
    }

}

void tcpclient::start() {

    while (_status == NOT_CONNECTED) {
        std::this_thread::sleep_for(std::chrono::milliseconds(2000));
        _socket.close();
        _socket.async_connect(_endpoint, std::bind(&tcpclient::connect_handler, this, std::placeholders::_1));
        _ios.run();
    }
}

The problem is that the reconnect is not working properly and the application seems to freeze for some reason? Aside from that reconnecting also seems problematic once a connection was established and is then dropped (e.g. due to the server crashing/closing).


Solution

  • std::this_thread::sleep_for(std::chrono::milliseconds(2000)); will freeze program for 2 seconds. What can you do here is to launch async timer when connection attempt fails:

    ::boost::asio::steady_timer m_timer{_ios, boost::asio::chrono::seconds{2}};
    
    void tcpclient::connect_handler(const boost::system::error_code &error)
    {
        if(!error)
        { 
            _status = CONNECTED;
            logger::log_info("Connected.");
        }
        else
        {
            _status = NOT_CONNECTED;
            logger::log_info("Failed to connect");
           _socket.close();
           m_timer.expires_from_now(boost::asio::chrono::seconds{2});
           m_timer.async_wait(std::bind(&tcpclient::on_ready_to_reconnect, this, std::placeholders::_1));
        }
    }
    
    void tcpclient::on_ready_to_reconnect(const boost::system::error_code &error)
    {
        try_connect();
    }
    
    void tcpclient::try_connect()
    {
        m_socket.async_connect(_endpoint, std::bind(&tcpclient::connect_handler, this, std::placeholders::_1));
    }
    
    void tcpclient::start()
    {
        try_connect();
        _ios.run();
    }
    

    There is also no need for while (_status == NOT_CONNECTED) loop, because io service will be busy and _ios.run(); won't return until connection is established.