How do you correctly close sockets in boost::asio?

18,119

Solution 1

You can do socket_.close(); almost any time you want, but you should keep in mind some moments:

  • If you have threads, this call should be wrapped with strand or you can crash. See boost strand documentation.
  • Whenever you do close keep in mind that io_service can already have queued handlers. And they will be called anyway with old state/error code.
  • close can throw an exception.
  • close does NOT includes ip::tcp::socket destruction. It just closes the system socket.
  • You must manage object lifetime yourself to ensure objects will be destroyed only when there is no more handlers. Usually this is done with enable_shared_from_this on your Connection or socket object.

Solution 2

Invoking socket.close() does not destroy the socket. However, the application may need to manage the lifetime of objects for which the operation and completion handlers depend upon, but this is not necessarily the socket object itself. For instance, consider a client class that holds a buffer, a socket, and has a single outstanding read operation with a completion handler of client::handle_read(). One can close() and explicitly destroy the socket, but the buffer and client instance must remain valid until at least the handler is invoked:

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_;
  ...
}

The application is responsible for managing the lifetime of objects upon which the operation and handlers are dependent. The chat client example accomplishes this by guaranteeing that the chat_client instance is destroyed after it is no longer in use, by waiting for the io_service.run() to return within the thread join():

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

One common idiom is to managing object lifetime is to have the I/O object be managed by a single class that inherits from enable_shared_from_this<>. When a class inherits from enable_shared_from_this, it provides a shared_from_this() member function that returns a valid shared_ptr instance managing this. A copy of the shared_ptr is passed to completion handlers, such as a capture-list in lambdas or passed as the instance handle to bind(), causing the lifetime of the I/O object to be extended to at least as long as the handler. See the Boost.Asio asynchronous TCP daytime server tutorial for an example using this approach.

Share:
18,119
Julian Stecklina
Author by

Julian Stecklina

OS, network and virtualization hacker.

Updated on June 09, 2022

Comments

  • Julian Stecklina
    Julian Stecklina almost 2 years

    I am trying to wrap my head around resource management in boost::asio. I am seeing callbacks called after the corresponding sockets are already destroyed. A good example of this is in the boost::asio official example: http://www.boost.org/doc/libs/1_60_0/doc/html/boost_asio/example/cpp11/chat/chat_client.cpp

    I am particularly concerned with the close method:

    void close()
    {
       io_service_.post([this]() { socket_.close(); });
    }
    

    If you call this function and afterwards destruct chat_client instance that holds socket_, socket_ will be destructed before the close method is called on it. Also any pending async_* callbacks can be called after the chat_client has been destroyed.

    How would you correctly handle this?

  • sqr163
    sqr163 about 7 years
    It's too complicated, ordinary mortal programmers should avoid closing the boost asio sockets, better not open them at all.
  • Tonia Sanzo
    Tonia Sanzo over 2 years
    In the enable_shred_from_this<> example they don't explicitly call the socket_.close() method. Is it safe to assume that the socket close is handled automatically?