c++boostboost-beast

How to properly disconnect and release resources for a boost::beast websocket stream?


I have a class WsClient which wraps a boost::beast::websocket::stream

class WsClient
{
    using TcpStream = boost::beast::tcp_stream;
    using SslStream = boost::beast::ssl_stream<TcpStream>;
    using WsStream = boost::beast::websocket::stream<SslStream>;

    ...

    WsStream _stream;
};

This class is used in a single threaded manner (that is, all interactions with the websocket client is done in the same thread context that the asio::io_context is running in.

I would like to safely and completely shut the websocket client down, disable/disconnect all asynchronous operations, and free up the resources associated with it.

In the reference documentation for boost::beast::websocket::stream I find there is a function async_close.

The documentation says that async_close should be followed by a read until an error error::closed is returned, and at this point, the connection is successfully closed.

As such, I've written the following which intiates the async_close, and in the completion handler it repeatedly initiates an async_read until I get back error::closed.

void WsClient::close()
{
    auto self = shared_from_this();

    _stream.async_close(ws::close_code::normal,
                        [self](boost::system::error_code ec)
                        {
                            self->drainSocket({}, 0);
                        });
}

void WsClient::drainSocket(boost::system::error_code ec, std::size_t)
{
     if (ec != boost::beast::websocket::error::closed)
          _stream.async_read(_read_buf, std::bind_front(&WsClient::drainSocket, shared_from_this()));

     // else 
     //     we're done, callback ends, shared_ptr goes out of scope, resources deleted
}

It all seems rather contrived, and I am struggling to believe this is the best way to close a websocket connection, discard any and all subsequent data, and free the resources.

In an ideal world I'd be able to just initiate the close, and then deregister the stream object from the io_context and then delete it.

All the extra callbacks and reading the socket just to discard the data seems overkill.


Solution

  • I think we can ascribe a lot of it down to "WebSocket specs are a bit convoluted". There are many (server) implementations that don't quite stick to the letter. Even the implementations that do try to stick to the letter often get it tragically wrong (e.g. by inadvertently sending data past the close).

    In the general case playing fast and loose may have security implications. I feel that Beast intends to be a "straight up", "low-level" if you will, websocket implementation. If you want to have higher level abstractions, you're going to build on top of it. As such you would only have 1 place in your application or library where this extra async read happens.

    The recommendations as posted make sense as per-spec good-practice. For completeness, note that at least latest version has the following:

    Instead, the program should continue reading message data until an error occurs. A read returning error::closed indicates a successful connection closure

    So your handler is overly specific with regards to the error - which could create problems. I'd suggest the much simpler:

     if (!ec.failed())