How do I cleanly reconnect a boost::socket following a disconnect?
Asked Answered
I

6

32

My client application uses a boost::asio::ip::tcp::socket to connect to a remote server. If the app loses connection to this server (e.g. due to the server crashing or being shutdown) I would like it to attempt a re-connect at regular intervals until it succeeds.

What do I need to do on the client-side to cleanly handle a disconnect, tidy up and then repeatedly attempt reconnects?

Currently the interesting bits of my code look something like this.

I connect like this:

bool MyClient::myconnect()
{
    bool isConnected = false;

    // Attempt connection
    socket.connect(server_endpoint, errorcode);

    if (errorcode)
    {
        cerr << "Connection failed: " << errorcode.message() << endl;
        mydisconnect();
    }
    else
    {
        isConnected = true;

        // Connected so setup async read for an incoming message.
        startReadMessage();

        // And start the io_service_thread
        io_service_thread = new boost::thread(
            boost::bind(&MyClient::runIOService, this, boost::ref(io_service)));
    }
    return (isConnected)
}

Where the runIOServer() method is just:

void MyClient::runIOService(boost::asio::io_service& io_service)
{
    size_t executedCount = io_service.run();
    cout << "io_service: " << executedCount << " handlers executed." << endl;
    io_service.reset();
}

And if any of the async read handlers return an error then they just call this disconnect method:

void MyClient::mydisconnect(void)
{
    boost::system::error_code errorcode;

    if (socket.is_open())
    {
        // Boost documentation recommends calling shutdown first
        // for "graceful" closing of socket.
        socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, errorcode);
        if (errorcode)
        {
            cerr << "socket.shutdown error: " << errorcode.message() << endl;
        }

        socket.close(errorcode);
        if (errorcode)
        {
            cerr << "socket.close error: " << errorcode.message() << endl;
        }    

        // Notify the observer we have disconnected
        myObserver->disconnected();            
    }

..which attempts to gracefully disconnect and then notifies an observer, which will start calling connect() at five second intervals until it gets reconnected.

Is there anything else I need to do?

Currently this does seem to work. If I kill the server that it is connected to I get the expected "End of file" error at my read handlers and mydisconnect() is called without any issues.

But when it then attempts to re-connect and fails I see it report "socket.shutdown error: Invalid argument". Is this just because I am attempting to shutdown a socket that has no read/writes pending on it? Or is it something more?

Iridosmine answered 17/6, 2010 at 14:55 Comment(4)
What is your rationale for calling shutdown if you've already detected the other end of the connection being closed?Village
@samm: Is that not recommended? I thought there may be pending operations on the socket which need to be cancelled with a shutdown(). I mainly just do it for simplicity though: the same mydisconnect() method is called if I want to disconnect normally, or if any async operations return an error.Iridosmine
I'm not sure if it's recommended or not. Where would the pending data or operations go? The other end of the connection isn't there.Village
@SamMiller It isn't necessary. It's recommended in a widely misunderstood MSDN article whose purpose is to show you how to achieve synchronized closes by both peers, but in that case you should only shutdown for output. close() is all that is required. Data still in flight is still delivered.Tillman
S
29

You need to create a new boost::asio::ip::tcp::socket each time you reconnect. The easiest way to do this is probably to just allocate the socket on the heap using a boost::shared_ptr (you could probably also get away with scoped_ptr if your socket is entirely encapsulated within a class). E.g.:

bool MyClient::myconnect()
{
    bool isConnected = false;

    // Attempt connection
    // socket is of type boost::shared_ptr<boost::asio::ip::tcp::socket>
    socket.reset(new boost::asio::ip::tcp::socket(...));
    socket->connect(server_endpoint, errorcode);
    // ...
}

Then, when mydisconnect is called, you could deallocate the socket:

void MyClient::mydisconnect(void)
{
    // ...
    // deallocate socket.  will close any open descriptors
    socket.reset();
}

The error you're seeing is probably a result of the OS cleaning up the file descriptor after you've called close. When you call close and then try to connect on the same socket, you're probably trying to connect an invalid file descriptor. At this point you should see an error message starting with "Connection failed: ..." based on your logic, but you then call mydisconnect which is probably then attempting to call shutdown on an invalid file descriptor. Vicious cycle!

Shotton answered 18/6, 2010 at 22:6 Comment(4)
Attempting to discourage portability is never a good idea, especially when you don't even know the targeted OS or the one used for development, for that matter!!Wideeyed
Thanks this approach worked well for me. I modified connect to always create a new socket and then call .reset on the shared_ptr as you describe. If connection attempt fails I just call socket->close() and leave it at that. I left the disconnect method as it was with the polite shutdown call.Iridosmine
I do not see a "reset" method for boost::asio::ip::tcp::socket?? Please see boost.org/doc/libs/1_55_0/doc/html/boost_asio/reference/ip__tcp/…Nosing
@Nosing It's a method of std::shared_ptr that replaces the managed object with the argument: en.cppreference.com/w/cpp/memory/shared_ptr/resetFathometer
I
9

For the sake of clarity here is the final approach I used (but this is based on bjlaub's answer, so please give any upvotes to him):

I declared the socket member as a scoped_ptr:

boost::scoped_ptr<boost::asio::ip::tcp::socket> socket;

Then I modified my connect method to be:

bool MyClient::myconnect()
{
    bool isConnected = false;

    // Create new socket (old one is destroyed automatically)
    socket.reset(new boost::asio::ip::tcp::socket(io_service));

    // Attempt connection
    socket->connect(server_endpoint, errorcode);

    if (errorcode)
    {
        cerr << "Connection failed: " << errorcode.message() << endl;
        socket->close();
    }
    else
    {
        isConnected = true;

        // Connected so setup async read for an incoming message.
        startReadMessage();

        // And start the io_service_thread
        io_service_thread = new boost::thread(
            boost::bind(&MyClient::runIOService, this, boost::ref(io_service)));
    }
    return (isConnected)
}

Note: this question was originally asked and answered back in 2010, but if you are now using C++11 or later then std::unique_ptr would normally be a better choice than boost::scoped_ptr

Iridosmine answered 25/6, 2010 at 11:49 Comment(2)
I think it would be nice to recommend use std::unique_ptr instead of boost::scoped_ptr as it is a better option and C++11 is widely available.Halfcaste
@Ivan_Bereziuk: I’ve edited my answer. It was originally posted back in 2010, before C++11 was widely available.Iridosmine
V
2

I've done something similar using Boost.Asio in the past. I use the asynchronous methods, so a reconnect is typically letting my existing ip::tcp::socket object go out of scope, then creating a new one for calls to async_connect. If async_connect fails, I use a timer to sleep a bit then retry.

Village answered 17/6, 2010 at 19:24 Comment(2)
thanks, so do you just call socket.close() when you detect an error, and then create a new one?Iridosmine
the descriptor is closed when the ip::tcp::socket object goes out of scope.Village
A
2

Since C++11 you can write:

decltype(socket)(std::move(socket));
// reconnect socket.

The above creates a local instance of socket's type move constructing it from socket.

Before the next line, the unnamed local instance is destructed, leaving socket in a "freshly constructed from io_service" state.

See: https://www.boost.org/doc/libs/1_63_0/doc/html/boost_asio/reference.html#boost_asio.reference.basic_stream_socket.basic_stream_socket.overload5

Agglutination answered 16/8, 2018 at 10:34 Comment(2)
kindly consider adding more information in your answerPantograph
While I think this represents the documented guarantee correctly, I would consider this exceptionally bad coding style and never let it pass code reviewEwan
B
2

In general, owning an asio resource via a smart pointer indicates a design error.

asio::tcp::socket has both a close() method and an is_open() method.

This is incredibly easy:

#include <boost/asio.hpp>
#include <fmt/format.h>

namespace asio = boost::asio;
using boost::system::error_code;

struct myClient
{
    myClient(asio::any_io_executor ex, asio::ip::tcp::endpoint server_endpoint)
    : socket(ex)
    , server_endpoint(server_endpoint)
    {
    }

    /// [re] open the connection to the server, closing the previous connection
    /// if necessary
    /// @returns true if connected successfuly, false on connection error.
    /// @note Wther successful or not, any previous connection is closed.
    bool
    myconnect()
    {
        // ensure closed
        mydisconnect();

        // reconnect
        socket.connect(server_endpoint, errorcode);
        fmt::print("connected: {}\n", errorcode.message());
        return !errorcode;
    }

    void
    mydisconnect()
    {
        // The conditional is not strictly necessary since errors from closing
        // an already closed socket will be swallowed.
        if (socket.is_open())
        {
            error_code ignore;
            socket.shutdown(asio::ip::tcp::socket::shutdown_both, ignore);
            fmt::print("shutdown: {}\n", ignore.message());
            socket.close(ignore);
            fmt::print("close: {}\n", ignore.message());
        }
    }

    asio::ip::tcp::socket   socket;
    asio::ip::tcp::endpoint server_endpoint;
    error_code              errorcode;
};

void
mock_server(asio::ip::tcp::acceptor &acc)
{
    error_code ec;
    for (int i = 0; i < 2 && !ec; ++i)
    {
        asio::ip::tcp::socket sock(acc.get_executor());
        acc.accept(sock, ec);
    }
}

int
main()
{
    asio::io_context        ioc_client, ioc_server;
    asio::ip::tcp::acceptor acc(
        ioc_server,
        asio::ip::tcp::endpoint(asio::ip::address_v4::loopback(), 0));

    auto t = std::thread(std::bind(mock_server, std::ref(acc)));

    myClient client(ioc_client.get_executor(), acc.local_endpoint());
    client.myconnect();
    client.myconnect();
    t.join();
}

Expected output:

connected: Success
shutdown: Success
close: Success
connected: Success
Barramunda answered 7/9, 2022 at 12:3 Comment(2)
What changes we need in that code when the tcp::socket is wrapped in a ssl::stream?Rossuck
openssl sessions may be re-used if (and only if) they have been successfully shut down without comms errors. For expediency, I would replace the entire ssl::stream with something like this: my_stream = ssl::stream<tcp::socket>(my_executor, my_ssl_context);Barramunda
I
-1

I have tried both the close() method and the shutdown method and they are just to tricky for me. Close() can throw an error that you need to catch and is the rude way to do what you want :) and shutdown() seems to be best but on multithreaded software, I find it can be fussy. So the best way is, as Sam said, to let it go out of scope. If the socket is a member of the class you can 1) redesign so that the class uses a 'connection' object to wrap the socket and let it go out of scope or 2) wrap it in a smart pointer and reset the smart pointer. If you using boost, including the shared_ptr is cheap and works like a charm. Never had a socket clean up issue doing it with a shared_ptr. Just my experience.

Ineducable answered 7/1, 2013 at 23:48 Comment(1)
You must call close(), 'tricky' or not. Otherwise you have a resource leak. There is nothing 'rude' about it. And shutdown() is not an alternative for close().Tillman

© 2022 - 2024 — McMap. All rights reserved.