websockets: Count pings from server as activity for idle_timeout

If the stream is receiving control packets like ping, don't count it as
idle. This means you can enable `timeout_opt.keep_alive_ping` on only
one side to get heartbeat.

Make tests that verify current behaviour.

Update documentation to match changes in PR #2718.

Addresses issue #2716
This commit is contained in:
Morten Minde Neergaard
2023-08-11 14:17:11 +02:00
committed by Mohammad Nejati
parent 84115e5b13
commit f9387fda71
5 changed files with 89 additions and 12 deletions

View File

@ -59,8 +59,8 @@ There are three timeout settings which may be set independently on the stream:
[[link beast.ref.boost__beast__websocket__stream_base__timeout.idle_timeout `timeout::idle_timeout`]]
[`duration`]
[
If no data is received from the peer for a time equal to the idle
timeout, then the connection will time out.
If no data or control frames are received from the peer for a time
equal to the idle timeout, then the connection will time out.
The value returned by
[link beast.ref.boost__beast__websocket__stream_base.none `stream_base::none()`]
may be assigned to disable this timeout.

View File

@ -439,12 +439,8 @@ struct stream<NextLayer, deflateSupported>::impl_type
if(timeout_opt.idle_timeout != none())
{
idle_counter = 0;
if(timeout_opt.keep_alive_pings)
timer.expires_after(
timeout_opt.idle_timeout / 2);
else
timer.expires_after(
timeout_opt.idle_timeout);
timer.expires_after(
timeout_opt.idle_timeout / 2);
BOOST_ASIO_HANDLER_LOCATION((
__FILE__, __LINE__,
@ -569,9 +565,9 @@ private:
if(impl.timeout_opt.idle_timeout == none())
return;
if( impl.timeout_opt.keep_alive_pings &&
impl.idle_counter < 1)
if( impl.idle_counter < 1 )
{
if( impl.timeout_opt.keep_alive_pings )
{
BOOST_ASIO_HANDLER_LOCATION((
__FILE__, __LINE__,

View File

@ -116,8 +116,10 @@ struct stream_base
An outstanding read operation must be pending, which will
complete immediately the error @ref beast::error::timeout.
@li When `keep_alive_pings` is `false`, the connection will be closed.
An outstanding read operation must be pending, which will
@li When `keep_alive_pings` is `false`, the connection will
be closed if there has been no activity. Both websocket
message frames and control frames count as activity. An
outstanding read operation must be pending, which will
complete immediately the error @ref beast::error::timeout.
*/
bool keep_alive_pings;

View File

@ -88,6 +88,79 @@ public:
se.code().message());
}
}
// inactivity timeout doesn't happen when you get pings
{
echo_server es{log};
stream<test::stream> ws{ioc_};
// We have an inactivity timeout of 2s, but don't send pings
ws.set_option(stream_base::timeout{
stream_base::none(),
std::chrono::milliseconds(2000),
false
});
ws.next_layer().connect(es.stream());
ws.handshake("localhost", "/");
flat_buffer b;
bool got_timeout = false;
ws.async_read(b,
[&](error_code ec, std::size_t)
{
if(ec != beast::error::timeout)
BOOST_THROW_EXCEPTION(
system_error{ec});
got_timeout = true;
});
// We are connected, base state
BEAST_EXPECT(ws.impl_->idle_counter == 0);
test::run_for(ioc_, std::chrono::milliseconds(1250));
// After 1.25s idle, no timeout but idle counter is 1
BEAST_EXPECT(ws.impl_->idle_counter == 1);
es.async_ping();
test::run_for(ioc_, std::chrono::milliseconds(500));
// The server sent a ping at 1.25s mark, and we're now at 1.75s mark.
// We haven't hit the idle timer yet (happens at 1s, 2s, 3s)
BEAST_EXPECT(ws.impl_->idle_counter == 0);
BEAST_EXPECT(!got_timeout);
test::run_for(ioc_, std::chrono::milliseconds(750));
// At 2.5s total; should have triggered the idle timer
BEAST_EXPECT(ws.impl_->idle_counter == 1);
BEAST_EXPECT(!got_timeout);
test::run_for(ioc_, std::chrono::milliseconds(750));
// At 3s total; should have triggered the idle timer again without
// activity and triggered timeout.
BEAST_EXPECT(got_timeout);
}
// inactivity timeout doesn't happen when you send pings
{
echo_server es{log};
stream<test::stream> ws{ioc_};
ws.set_option(stream_base::timeout{
stream_base::none(),
std::chrono::milliseconds(600),
true
});
unsigned n_pongs = 0;
ws.control_callback({[&](frame_type kind, string_view)
{
if (kind == frame_type::pong)
++n_pongs;
}});
ws.next_layer().connect(es.stream());
ws.handshake("localhost", "/");
flat_buffer b;
ws.async_read(b, test::fail_handler(asio::error::operation_aborted));
// We are connected, base state
test::run_for(ioc_, std::chrono::seconds(1));
// About a second later, we should have close to 5 pings/pongs, and no timeout
BEAST_EXPECTS(2 <= n_pongs && n_pongs <= 3, "Unexpected nr of pings: " + std::to_string(n_pongs));
}
}
void

View File

@ -134,6 +134,12 @@ public:
this));
}
void
async_ping()
{
ws_.async_ping("", [](error_code){});
}
void
async_close()
{