| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309 |
- // Standalone test for WebSocket automatic heartbeat.
- // Compiled with a 1-second ping interval so we can verify heartbeat behavior
- // without waiting 30 seconds.
- #define CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND 1
- #define CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND 3
- #define CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND 3
- #include <httplib.h>
- #include "gtest/gtest.h"
- #include <future>
- using namespace httplib;
- class WebSocketHeartbeatTest : public ::testing::Test {
- protected:
- void SetUp() override {
- svr_.WebSocket("/ws", [](const Request &, ws::WebSocket &ws) {
- std::string msg;
- while (ws.read(msg)) {
- ws.send(msg);
- }
- });
- port_ = svr_.bind_to_any_port("localhost");
- thread_ = std::thread([this]() { svr_.listen_after_bind(); });
- svr_.wait_until_ready();
- }
- void TearDown() override {
- svr_.stop();
- thread_.join();
- }
- Server svr_;
- int port_;
- std::thread thread_;
- };
- // Verify that an idle connection stays alive beyond the read timeout
- // thanks to automatic heartbeat pings.
- TEST_F(WebSocketHeartbeatTest, IdleConnectionStaysAlive) {
- ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
- ASSERT_TRUE(client.connect());
- // Sleep longer than read timeout (3s). Without heartbeat, the connection
- // would time out. With heartbeat pings every 1s, it stays alive.
- std::this_thread::sleep_for(std::chrono::seconds(5));
- // Connection should still be open
- ASSERT_TRUE(client.is_open());
- // Verify we can still exchange messages
- ASSERT_TRUE(client.send("hello after idle"));
- std::string msg;
- ASSERT_TRUE(client.read(msg));
- EXPECT_EQ("hello after idle", msg);
- client.close();
- }
- // Verify that set_websocket_ping_interval overrides the compile-time default
- TEST_F(WebSocketHeartbeatTest, RuntimePingIntervalOverride) {
- // The server is already using the compile-time default (1s).
- // Create a client with a custom runtime interval.
- ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
- client.set_websocket_ping_interval(2);
- ASSERT_TRUE(client.connect());
- // Sleep longer than read timeout (3s). Client heartbeat at 2s keeps alive.
- std::this_thread::sleep_for(std::chrono::seconds(5));
- ASSERT_TRUE(client.is_open());
- ASSERT_TRUE(client.send("runtime interval"));
- std::string msg;
- ASSERT_TRUE(client.read(msg));
- EXPECT_EQ("runtime interval", msg);
- client.close();
- }
- // Verify that ping_interval=0 disables heartbeat without breaking basic I/O.
- TEST_F(WebSocketHeartbeatTest, ZeroDisablesHeartbeat) {
- ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
- client.set_websocket_ping_interval(0);
- ASSERT_TRUE(client.connect());
- // Basic send/receive still works with heartbeat disabled
- ASSERT_TRUE(client.send("no client ping"));
- std::string msg;
- ASSERT_TRUE(client.read(msg));
- EXPECT_EQ("no client ping", msg);
- client.close();
- }
- // Verify that Server::set_websocket_ping_interval works at runtime
- class WebSocketServerPingIntervalTest : public ::testing::Test {
- protected:
- void SetUp() override {
- svr_.set_websocket_ping_interval(2);
- svr_.WebSocket("/ws", [](const Request &, ws::WebSocket &ws) {
- std::string msg;
- while (ws.read(msg)) {
- ws.send(msg);
- }
- });
- port_ = svr_.bind_to_any_port("localhost");
- thread_ = std::thread([this]() { svr_.listen_after_bind(); });
- svr_.wait_until_ready();
- }
- void TearDown() override {
- svr_.stop();
- thread_.join();
- }
- Server svr_;
- int port_;
- std::thread thread_;
- };
- TEST_F(WebSocketServerPingIntervalTest, ServerRuntimeInterval) {
- ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
- ASSERT_TRUE(client.connect());
- // Server ping interval is 2s; client uses compile-time default (1s).
- // Both keep the connection alive.
- std::this_thread::sleep_for(std::chrono::seconds(5));
- ASSERT_TRUE(client.is_open());
- ASSERT_TRUE(client.send("server interval"));
- std::string msg;
- ASSERT_TRUE(client.read(msg));
- EXPECT_EQ("server interval", msg);
- 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());
- }
- // The compile-time client read timeout (3s here) was never asked for through
- // set_read_timeout(), so when it elapses read() reports Fail and closes the
- // connection rather than handing back a Timeout on a still-open one.
- TEST_F(WebSocketPongTimeoutTest, CompileTimeClientReadTimeoutIsFail) {
- ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
- client.set_websocket_ping_interval(0);
- ASSERT_TRUE(client.connect());
- // Server pings are off and its handler never sends, so nothing arrives.
- std::string msg;
- EXPECT_EQ(client.read(msg), ws::Fail);
- EXPECT_FALSE(client.is_open());
- }
- // The compile-time server read timeout (3s here) is a backstop that reclaims
- // the worker from a peer gone quiet, not a timeout the handler asked for. When
- // it elapses read() must return Fail, so a handler written as
- // `while (ws.read(msg))` ends instead of re-running its body with the previous
- // message still in `msg`.
- class WebSocketServerReadTimeoutTest : public ::testing::Test {
- protected:
- void SetUp() override {
- svr_.set_websocket_ping_interval(0);
- svr_.WebSocket("/ws", [this](const Request &, ws::WebSocket &ws) {
- std::string msg;
- while (ws.read(msg)) {
- iterations_++;
- ws.send(msg);
- }
- handler_done_.set_value();
- });
- port_ = svr_.bind_to_any_port("localhost");
- thread_ = std::thread([this]() { svr_.listen_after_bind(); });
- svr_.wait_until_ready();
- }
- void TearDown() override {
- svr_.stop();
- thread_.join();
- }
- Server svr_;
- int port_;
- std::thread thread_;
- std::atomic<int> iterations_{0};
- std::promise<void> handler_done_;
- };
- TEST_F(WebSocketServerReadTimeoutTest, BackstopEndsHandlerLoop) {
- ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
- client.set_websocket_ping_interval(0); // nothing reaches the server's read()
- client.set_read_timeout(10, 0); // fail rather than hang
- ASSERT_TRUE(client.connect());
- ASSERT_TRUE(client.send("hello"));
- std::string msg;
- ASSERT_EQ(client.read(msg), ws::Text);
- EXPECT_EQ("hello", msg);
- // The client now stays silent. The server's backstop elapses and the
- // handler returns, having run its loop body exactly once.
- auto done = handler_done_.get_future();
- ASSERT_EQ(done.wait_for(std::chrono::seconds(6)), std::future_status::ready);
- EXPECT_EQ(1, iterations_.load());
- EXPECT_EQ(client.read(msg), ws::Fail);
- 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");
- ASSERT_TRUE(client.connect());
- // Wait through several heartbeat cycles
- for (int i = 0; i < 3; i++) {
- std::this_thread::sleep_for(std::chrono::milliseconds(1500));
- ASSERT_TRUE(client.is_open());
- std::string text = "msg" + std::to_string(i);
- ASSERT_TRUE(client.send(text));
- std::string msg;
- ASSERT_TRUE(client.read(msg));
- EXPECT_EQ(text, msg);
- }
- client.close();
- }
|