Search code examples
boost-beastboost-beast-websocket

boost::beast::websocket server doesn't react to client close()


I have a boost::beast HTTP server that streams data to a connected client. I'm happy to ignore anything they send to me, so I constantly call async_write without calling async_receive. But I think this causes a problem where I don't notice the client's request to close the socket.

Because I don't ever call async_receive(), I need to set timeout.idle_timeout = none() because writes do not reset the idle timer. timeout.handshake_timeout is 30s by default, and this seems appropriate.

void Session::Accept(Request&& req){

    auto timeout = boost::beast::websocket::stream_base::timeout::suggested(
        boost::beast::role_type::server // sets handshake_timeout = 30s
    );
    timeout.idle_timeout = boost::beast::websocket::stream_base::none();
    m_client.set_option( timeout );

    // accept stuff

    Write();
}

void Session::Write(){
    m_client.async_write(
        m_buffer.data(),
        boost::beast::bind_front_handler(
            &Session::OnWrite,
            shared_from_this()
        )
    );
}

void Session::OnWrite(
    const boost::beast::error_code ec, 
    std::size_t bytes_transferred
){
    if (ec){
        std::cerr << "client disconnected with: " << ec.message() << std::endl;
        return;
    }

    PopulateBuffer();
    Write();
}

When I use a Javascript client to close the socket, there is a delay (up to 30s) between socket.close() and the socket.onclose function. That's the problem I'm trying to solve.

socket = new WebSocket('ws://...');
socket.onopen = function() {
    ...
}
socket.onmessage = function(event) {
    if (...)
        socket.close()
}
socket.onclose = function(event) {
    console.log(event)
}

When the socket.onclose slot is finally called, the exit code is 1006 (abnormal -- Connection closed without receiving a close frame). That's a reserved code that cannot be sent along the wire. Therefore it's something the client decided, not something the server sent.

My hypothesis is the client's socket.close() causes something to be sent to the server, and then stops the handshakes, but because I never async_read(), beast's websocket doesn't process the close-request, and it keeps sending data until the handshakes timeout (30s). Upon timeout, it sends a 1006 close reason to the client, and invokes my async_write() handler with a "broken pipe" error-code.


What's the best way to deal with this?

I don't want to call async_receive() because I can't call async_write() again until the async_receive() has finished. And blocking (almost) forever on the useless read will prevent me from sending my steam.

If there is a fast way of detecting of data is available for reading, I could certainly do that. I'm just not sure what method is available.


Solution

  • In this case, the server only async_write()s to the websocket and never does an async_read().

    async_read() handled any client-initiated close-requests (returning error code boost::beast::websocket::error::closed), and take care of resetting the idle_timeout, therefore it's important to always have a pending async_read().

    It is reasonable to have simultaneous async_read() and async_write() calls pending at the same time. The only requirement is to ensure these are called from the same boost::asio::io_context::strand to avoid multi-threading issues. If io_context::run() was only called from one thread, don't worry about it, but if you call io_context::run() in several threads, then you'll need to own a strand and ensure you only ever call async_* from it.

    If you do this, you can also remove the explicit setting of idle_timeout and use the suggested server settings of 5min.

    Here's how it can work:

    void Session::OnAccept(...) {
        Write(); // Starts the write-loop
        Read(); // Starts the read-loop
    }
    
    void Session::Write(){
        PopulateBuffer(); // Sets writebuffer
        m_socket.async_write(
            m_writebuffer,
            [self = shared_from_this()](auto ec, auto bytes){
                self->m_strand.post([self, ec, bytes](){ // Make sure it runs in the strand
                    self->OnWrite(ec, bytes);
                }
            }
        );
    }
    
    void Session::OnWrite(std::error_code ec, std::size_t bytes){
        if (ec)
            return;
    
        m_writebuffer.consume(m_writebuffer.size());
    
        Write();
    }
    
    void Session::Read(){
        m_socket.async_read(
            m_readbuffer,
            [self = shared_from_this()](auto ec, auto bytes){
                self->m_strand.post([self, ec, bytes](){ // Make sure it runs in the strand
                    self->OnRead(ec, bytes);
                }
            }     
        );
    }
    
    void Session::OnRead(std::error_code ec, std::size_t bytes){
        if (ec)
            return;
    
        m_readbuffer.consume(m_readbuffer.size());
    
        Read();
    }