Boost.Asio TCP 移动到套接字析构函数不足以干净地关闭?

Boost.Asio TCP moved-to socket destructor not enough to cleanly close?

考虑这个测试程序:

#include <boost/asio/io_service.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <functional>
#include <iostream>

static void callback (boost::asio::ip::tcp::socket && socket)
{
    //boost::asio::ip::tcp::socket new_socket = std::move(socket);
    std::cout << "Accepted" << std::endl;
}

static void on_accept (boost::asio::ip::tcp::acceptor &  acceptor,
                       boost::asio::ip::tcp::socket &    socket,
                       boost::system::error_code const & error)
{
    if (error)
    {
        std::cerr << error << ' ' << error.message() << std::endl;
        return ;
    }

    callback(std::move(socket));
    acceptor.async_accept
    (
        socket,
        std::bind(on_accept, std::ref(acceptor), std::ref(socket), std::placeholders::_1)
    );
}

int main ()
{
    boost::asio::io_service        service;
    boost::asio::io_service::work  work { service };
    boost::asio::ip::tcp::acceptor acceptor { service };
    boost::asio::ip::tcp::socket   socket { service };

    boost::asio::ip::tcp::endpoint endpoint { boost::asio::ip::tcp::v4(), 5555 };
    boost::system::error_code      ec;

    using socket_base = boost::asio::socket_base;
    auto option = socket_base::reuse_address { false };

    if (acceptor.open(endpoint.protocol(), ec) ||
        acceptor.set_option(option, ec) ||
        acceptor.bind(endpoint, ec) ||
        acceptor.listen(socket_base::max_connections, ec) ||
        acceptor.is_open() == false)
        return 1;

    acceptor.async_accept
    (
        socket,
        std::bind(on_accept, std::ref(acceptor), std::ref(socket), std::placeholders::_1)
    );

    service.run();
}

当我将客户端连接到它时,出现错误:

Accepted
system:1 Incorrect function

(当 callback() 函数中的 socket 对象被销毁时,调用 on_accept() 函数并显示错误代码。

此外,客户端根本没有断开连接。

如果我取消注释 callback() 函数中的行,一切正常,没有错误消息,并且客户端按预期断开连接。

现在进行环境设置,我在 Windows 8.1 下,使用 MinGW-w64 v4.9.2 编译器和使用相同编译器编译的 Boost.Asio v1.58.0。

用于编译文件的命令行如下:

$ g++ -std=c++14 -IC:/C++/boost/1.58.0 main.cpp -LC:/C++/boost/1.58.0/lib -lboost_system-mgw49-mt-1_58 -lwsock32 -lws2_32 -o test.exe

请注意,使用 Boost 1.57.0 会导致相同的行为。

我也可以完全删除注释行,然后使用这个:

static void callback (boost::asio::ip::tcp::socket && socket)
{
    std::cout << "Accepted" << std::endl;
    socket.shutdown(socket.shutdown_both);
    socket.close();
}

程序也能正常运行。


那么,为什么我需要添加额外的步骤才能在此处不出现错误? IIRC 几个月前,当我第一次尝试该程序时,这种行为并不存在。

代码只创建一个socket,它是一个自动变量,其生命周期将结束一次main() returns。 std::move(socket) 只有 return 一个可以提供给套接字 move constructor 的 xvalue;它不构造套接字。

要解决此问题,请考虑将 callback() 签名更改为通过值接受套接字,从而允许编译器在给定 xvalue 时为您调用移动构造函数。变化:

static void callback (boost::asio::ip::tcp::socket && socket)

至:

static void callback (boost::asio::ip::tcp::socket socket)

总的来说,代码的流程如下:

void callback(socket&&); // rvalue reference.

void on_accept(acceptor&, socket&, ...) // lvalue reference.
{
  ...
  callback(static_cast<socket&&>(socket)); // Cast to xvalue.
  acceptor.async_accept(socket,
    std::bind(&on_accept, std::ref:acceptor),
      std::ref(socket), // lvalue reference.
      ...);
}

int main()
{
  boost::asio::io_service io_service;
  boost::asio::io_service::work work(io_service);
  boost::asio::ip::tcp::acceptor acceptor(io_service);
  boost::asio::ip::tcp::socket socket(io_service); // Constructor.

  ...

  acceptor.async_accept(socket,
    std::bind(&on_accept, std::ref:acceptor),
      std::ref(socket), // lvalue reference.
      ...);
  io_service.run();
}

成功接受第一个连接后,main() 中的套接字打开。 on_accept() 函数使用 xvalue 调用 callback(),并且不会更改套接字的状态。使用已打开的套接字启动另一个 async_accept() 操作,立即导致操作失败。 async_accept() 操作失败,调用 on_accept() 将提前 return,停止其调用链。由于 io_service::work 附加到 io_service,执行永远不会从 io_service::run() 执行 return,从而防止 main() 从 returning 和销毁套接字。最终结果是不再接受连接(没有 async_accept() 操作)并且客户端没有断开连接(socket 永远不会被销毁)。

callback() 将套接字的状态更改为关闭时,问题不再存在,因为满足 async_accept() 的先决条件。其他示例满足此前提条件,因为:

  • 取消注释一行会导致调用移动构造函数。移出的套接字将具有与使用 socket(io_service&) 构造函数构建时相同的状态。
  • 套接字已通过 socket.close() 显式关闭。