如何在断开连接后清晰地重新连接boost::socket?

31

我的客户端应用程序使用boost::asio::ip::tcp::socket连接到远程服务器。 如果该应用程序失去与服务器的连接(例如由于服务器崩溃或关闭),我想让它定期尝试重新连接,直到成功为止。

在客户端上需要做什么才能清洁地处理断开连接、整理并重复尝试重新连接?

目前我的代码中有些有趣的部分看起来像这样。

我通过以下方式connect

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)
}

在这里,runIOServer() 方法只是:

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();
}

如果任何异步读取处理程序返回错误,它们只需调用此 disconnect 方法:

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();            
    }

...这个方法试图优雅地断开并通知一个观察者,在此期间观察者将会以五秒的间隔调用connect()方法,直到重新连接成功为止。

我还需要做什么吗?

目前看起来这个方法似乎可行。如果我杀掉它所连接的服务器,我会在读取处理程序中得到预期的"End of file"错误,并且mydisconnect()方法会被调用而没有任何问题。

但是当它尝试重新连接并失败时,我会看到它报告"socket.shutdown error: Invalid argument"的错误。这只是因为我试图关闭一个没有未完成读写操作的套接字吗?还是还有其他原因呢?


如果你已经检测到连接的另一端被关闭,你为什么要调用shutdown呢? - Sam Miller
@samm:这不是推荐的做法吗?我认为可能有待处理的套接字操作需要使用shutdown()取消。但我主要是出于简单考虑:如果我想正常断开连接,或者任何异步操作返回错误,都会调用相同的mydisconnect()方法。 - GrahamS
我不确定是否推荐这样做。未决数据或操作应该放在哪里?连接的另一端不存在。 - Sam Miller
@SamMiller 不是必须的。这是一个被广泛误解的MSDN文章中推荐的,其目的是向您展示如何通过两个对等方实现同步关闭,但在这种情况下,您只应该为输出关闭。close()是所需的全部。仍在传输中的数据仍将被传递。 - user207421
6个回答

29

每次重新连接时,您需要创建一个新的boost::asio::ip::tcp::socket。 最简单的方法可能是使用boost::shared_ptr在堆上分配套接字(如果您的套接字完全封装在类中,则可能也可以使用scoped_ptr)。 例如:

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);
    // ...
}

然后,当调用mydisconnect时,您可以释放该套接字:

void MyClient::mydisconnect(void)
{
    // ...
    // deallocate socket.  will close any open descriptors
    socket.reset();
}
你看到的错误可能是操作系统在你调用close之后清理文件描述符导致的。当你调用close然后尝试在同一套接字上连接时,你可能正在尝试连接一个无效的文件描述符。此时根据你的逻辑应该会看到以“Connection failed:...”开头的错误消息,但你随后调用mydisconnect,这很可能会尝试对无效的文件描述符调用shutdown。恶性循环!

试图阻止可移植性从来不是一个好主意,特别是当你甚至不知道目标操作系统或用于开发的操作系统时! - Geoff
谢谢,这种方法对我很有效。我修改了连接方式,始终创建一个新的套接字,然后按照您所描述的在shared_ptr上调用.reset。如果连接尝试失败,我只需调用socket->close()并保持不变。我将disconnect方法保留为原样,使用礼貌的 shutdown调用。 - GrahamS
boost::asio::ip::tcp::socket 没有“reset”方法吗?请参阅 http://www.boost.org/doc/libs/1_55_0/doc/html/boost_asio/reference/ip__tcp/socket.html。 - Anonymous
2
@匿名用户 这是std::shared_ptr的一种方法,用参数替换托管对象:http://en.cppreference.com/w/cpp/memory/shared_ptr/reset - robsn

9

为了更清晰明了,这里是我采用的最终方法(但这基于bjlaub的答案,请给他点赞):

我将socket成员声明为scoped_ptr

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

我修改了我的 connect 方法如下:

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)
}

注意:这个问题最初是在2010年提出并回答的,但如果你现在使用的是C++11或更高版本,则std::unique_ptr通常比boost::scoped_ptr更好。

1
我认为推荐使用std::unique_ptr而不是boost::scoped_ptr会更好,因为它是更好的选择,而且C++11已经广泛可用。 - Ivan_a_bit_Ukrainivan
2
@Ivan_Bereziuk:我已经编辑了我的答案。它最初是在2010年发布的,在C++11广泛可用之前。 - GrahamS

2

一般来说,通过智能指针拥有asio资源表示设计错误。

asio::tcp::socket同时具有close()方法和is_open()方法。

这非常容易实现:

#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();
}

预期输出:

connected: Success
shutdown: Success
close: Success
connected: Success

当tcp::socket被包装在ssl::stream中时,我们需要对代码进行哪些更改? - mzimbres
仅当openssl会话在没有通信错误的情况下成功关闭后,才能重新使用它们。为了加快速度,我建议用以下代码替换整个ssl::streammy_stream = ssl::stream<tcp::socket>(my_executor, my_ssl_context); - Richard Hodges

2

4
请考虑在您的回答中添加更多信息。 - Inder
虽然我认为这个文档保证被正确地表达了,但我认为这是极其糟糕的编码风格,绝不会让它通过代码审查。 - sehe

2

我以前使用过Boost.Asio做过类似的事情。我使用异步方法,因此重新连接通常是让我的现有ip::tcp::socket对象超出范围,然后为调用async_connect创建一个新对象。如果async_connect失败,我使用计时器等待一段时间然后重试。


1
谢谢,那么当您检测到错误时,您只需调用 socket.close() 然后创建一个新的吗? - GrahamS
当ip::tcp::socket对象超出作用域时,描述符将被关闭。 - Sam Miller

-1

我已经尝试了close()方法和shutdown()方法,但它们对我来说太棘手了。close()可能会抛出需要捕获的错误,这是你想要做的粗暴方式:)而shutdown()似乎是最好的选择,但在多线程软件中,我发现它可能会很麻烦。所以最好的方法是,正如Sam所说,让它超出作用域。如果套接字是类的成员,你可以1)重新设计使类使用“连接”对象来包装套接字并让它超出作用域,或者2)将其包装在智能指针中并重置智能指针。如果你使用boost,包括shared_ptr很便宜,并且像一个魅力一样工作。我从未遇到过使用shared_ptr时存在套接字清理问题。这是我的经验。


2
你必须调用 close(),无论它是否“棘手”。否则,你将会有资源泄漏。这并不是什么“粗鲁”的行为。而shutdown()也不能替代close() - user207421

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接