boost::asio中如何正确关闭套接字?

17

我试图理解boost::asio中的资源管理。我发现在对应套接字被销毁后仍会调用回调函数。其中一个很好的例子是boost::asio官方示例中的chat_client.cpp文件。我特别关注close方法:

void close()
{
   io_service_.post([this]() { socket_.close(); });
}
如果您调用此函数并在之后销毁持有socket_的chat_client实例,那么在对其调用close方法之前,socket_将被销毁。此外,在chat_client被销毁后,任何未决的async_*回调都可能会被调用。
如何正确处理此问题?

关于您现已删除的评论“关闭套接字绝对不会导致所有未决回调均以错误状态被调用”,您该如何重新表述? - sehe
2个回答

18

您可以随时执行socket_.close();,但应注意以下几点:

  • 如果您有线程,则应在strand中包装此调用,否则可能会崩溃。请参阅boost strand documentation
  • 每当您执行close时,请记住io_service可能已经排队了处理程序。并且它们将以旧状态/错误代码被调用。
  • close可能会抛出异常。
  • close不包括ip::tcp::socket的销毁。它只是关闭系统套接字。
  • 您必须自己管理对象生命周期,以确保仅在没有更多处理程序时才销毁对象。通常在您的Connectionsocket对象上使用enable_shared_from_this来完成这项工作。

37
太过复杂了,普通程序员应该避免关闭boost asio套接字,最好根本不要打开它们。 - sqr163
如果你有线程,这个调用应该使用strand进行封装,否则可能会崩溃。- 不准确,你可以在一个线程中使用close(),同时在另一个线程进行其他调用(读取/写入/连接)。是什么让你认为你不能这样做?但你需要小心,只调用一次close() - Arthur Tacca
@ArthurTacca “man close” 让我觉得同时与其他电话通话不是一个好主意。 - Galimov Albert

11
调用 socket.close() 不会销毁套接字。然而,应用程序可能需要管理操作和完成处理程序依赖的对象的生命周期,但这不一定是套接字对象本身。例如,考虑一个 client 类,它持有一个缓冲区、一个套接字,并具有一个带有完成处理程序 client::handle_read() 的单个未完成读取操作。可以 close() 并显式销毁套接字,但缓冲区和 client 实例必须保持有效,直到至少调用处理程序。
class client
{
  ...

  void read()
  {
    // Post handler that will start a read operation.
    io_service_.post([this]() {
      async_read(*socket, boost::asio::buffer(buffer_);
        boost::bind(&client::handle_read, this,
          boost::asio::placeholders::error,
          boost::asio::placeholders::bytes_transferred));
    });
  }

  void handle_read(
    const boost::system::error_code& error,
    std::size_t bytes_transferred
  )
  {
    // make use of data members...if socket_ is not used, then it
    // is safe for socket to have already been destroyed.
  }

  void close()
  {
    io_service_.post([this]() {
      socket_->close();
      // As long as outstanding completion handlers do not
      // invoke operations on socket_, then socket_ can be 
      // destroyed.
      socket_.release(nullptr);
    });
  }

private:
  boost::asio::io_service& io_service_;

  // Not a typical pattern, but used to exemplify that outstanding
  // operations on `socket_` are not explicitly dependent on the 
  // lifetime of `socket_`.
  std::unique_ptr<boost::asio::socket> socket_;
  std::array<char, 512> buffer_;
  ...
}

应用程序负责管理操作和处理程序所依赖的对象的生命周期。聊天客户端示例通过等待 io_service.run()join() 线程中返回,确保仅在不再使用时销毁chat_client实例。 参见聊天客户端示例
int main(...)
{
  try
  {
    ...

    boost::asio::io_service io_service;
    chat_client c(...);

    std::thread t([&io_service](){ io_service.run(); });

    ...

    c.close();
    t.join(); // Wait for `io_service.run` to return, guaranteeing
              // that `chat_client` is no longer in use.
  }           // The `chat_client` instance is destroyed.
  catch (std::exception& e)
  {
    ...
  }
}

管理对象生命周期的一种常见习语是,让I/O对象由一个继承自enable_shared_from_this<>的单个类来管理。当一个类继承自enable_shared_from_this时,它提供了一个shared_from_this()成员函数,返回一个有效的shared_ptr实例来管理this。将shared_ptr的副本传递给完成处理程序,例如lambda中的捕获列表或作为bind()的实例句柄进行传递,从而使I/O对象的生命周期至少延长到处理程序的生命周期。请参阅Boost.Asio异步TCP日间服务器教程,了解使用此方法的示例。


enable_shred_from_this<>示例中,他们没有明确调用socket_.close()方法。可以安全地假设套接字关闭是自动处理的吗? - Tonia Sanzo

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