Enhance WebSocket support with unresponsive-peer detection and documentation updates

- Added `set_websocket_max_missed_pongs` method to configure unresponsive-peer detection.
- Updated README and documentation to clarify WebSocket limitations and features.
- Introduced tests for detecting non-responsive peers and ensuring responsive peers do not trigger timeouts.
This commit is contained in:
yhirose
2026-04-11 22:17:38 -04:00
parent b4eec3ee77
commit 6bdd657713
6 changed files with 199 additions and 12 deletions

View File

@@ -136,6 +136,86 @@ TEST_F(WebSocketServerPingIntervalTest, ServerRuntimeInterval) {
client.close();
}
// Verify that the client detects a non-responsive peer via unacked-ping count.
// Setup: the server's heartbeat is disabled AND its handler never calls
// read(), so no automatic Pong reply is ever produced. The client sends
// pings but receives no pongs, and should close itself once the unacked
// ping count reaches max_missed_pongs.
class WebSocketPongTimeoutTest : public ::testing::Test {
protected:
void SetUp() override {
svr_.set_websocket_ping_interval(0);
svr_.WebSocket("/ws", [this](const Request &, ws::WebSocket &) {
std::unique_lock<std::mutex> lock(handler_mutex_);
handler_cv_.wait(lock, [this]() { return release_; });
});
port_ = svr_.bind_to_any_port("localhost");
thread_ = std::thread([this]() { svr_.listen_after_bind(); });
svr_.wait_until_ready();
}
void TearDown() override {
{
std::lock_guard<std::mutex> lock(handler_mutex_);
release_ = true;
}
handler_cv_.notify_all();
svr_.stop();
thread_.join();
}
Server svr_;
int port_;
std::thread thread_;
std::mutex handler_mutex_;
std::condition_variable handler_cv_;
bool release_ = false;
};
TEST_F(WebSocketPongTimeoutTest, ClientDetectsNonResponsivePeer) {
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
client.set_websocket_max_missed_pongs(2);
ASSERT_TRUE(client.connect());
ASSERT_TRUE(client.is_open());
// Client pings every 1s (compile-time default in this test file).
// With max_missed_pongs = 2, the heartbeat thread should self-close within
// roughly 3s. Poll is_open() up to 6s.
auto start = std::chrono::steady_clock::now();
while (client.is_open() &&
std::chrono::steady_clock::now() - start < std::chrono::seconds(6)) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
EXPECT_FALSE(client.is_open());
}
// Verify that a responsive peer does NOT trigger the pong-timeout mechanism,
// even with a small max_missed_pongs budget. This is the positive counterpart
// of ClientDetectsNonResponsivePeer: the client must actively drive read() so
// that incoming Pong frames are consumed and the unacked counter is reset.
TEST_F(WebSocketHeartbeatTest, ResponsivePeerNeverTimesOut) {
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
client.set_websocket_max_missed_pongs(2);
ASSERT_TRUE(client.connect());
// Interactive loop over ~6s, longer than 2 ping intervals, so the
// pong-timeout mechanism would trigger if pongs weren't being consumed.
// Each iteration's read() also drains any pending Pong frame.
for (int i = 0; i < 6; i++) {
std::string text = "keepalive" + std::to_string(i);
ASSERT_TRUE(client.send(text));
std::string msg;
ASSERT_TRUE(client.read(msg));
EXPECT_EQ(text, msg);
std::this_thread::sleep_for(std::chrono::seconds(1));
}
EXPECT_TRUE(client.is_open());
client.close();
}
// Verify that multiple heartbeat cycles work
TEST_F(WebSocketHeartbeatTest, MultipleHeartbeatCycles) {
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");