0

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.

1
  • I was working under the impression that you can't read/write at the same time (I seem to remember discovering this the hard way). But according to the author, all 5 websocket async_* operations (read,write,ping,pong,close) can be simultaneously pending, assuming they're in the same strand. Therefore, I'll try an async read() loop on Monday. Commented Jul 19, 2024 at 16:16

1 Answer 1

0

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();
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.