test_websocket_heartbeat.cc 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. // Standalone test for WebSocket automatic heartbeat.
  2. // Compiled with a 1-second ping interval so we can verify heartbeat behavior
  3. // without waiting 30 seconds.
  4. #define CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND 1
  5. #define CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND 3
  6. #define CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND 3
  7. #include <httplib.h>
  8. #include "gtest/gtest.h"
  9. using namespace httplib;
  10. class WebSocketHeartbeatTest : public ::testing::Test {
  11. protected:
  12. void SetUp() override {
  13. svr_.WebSocket("/ws", [](const Request &, ws::WebSocket &ws) {
  14. std::string msg;
  15. while (ws.read(msg)) {
  16. ws.send(msg);
  17. }
  18. });
  19. port_ = svr_.bind_to_any_port("localhost");
  20. thread_ = std::thread([this]() { svr_.listen_after_bind(); });
  21. svr_.wait_until_ready();
  22. }
  23. void TearDown() override {
  24. svr_.stop();
  25. thread_.join();
  26. }
  27. Server svr_;
  28. int port_;
  29. std::thread thread_;
  30. };
  31. // Verify that an idle connection stays alive beyond the read timeout
  32. // thanks to automatic heartbeat pings.
  33. TEST_F(WebSocketHeartbeatTest, IdleConnectionStaysAlive) {
  34. ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
  35. ASSERT_TRUE(client.connect());
  36. // Sleep longer than read timeout (3s). Without heartbeat, the connection
  37. // would time out. With heartbeat pings every 1s, it stays alive.
  38. std::this_thread::sleep_for(std::chrono::seconds(5));
  39. // Connection should still be open
  40. ASSERT_TRUE(client.is_open());
  41. // Verify we can still exchange messages
  42. ASSERT_TRUE(client.send("hello after idle"));
  43. std::string msg;
  44. ASSERT_TRUE(client.read(msg));
  45. EXPECT_EQ("hello after idle", msg);
  46. client.close();
  47. }
  48. // Verify that set_websocket_ping_interval overrides the compile-time default
  49. TEST_F(WebSocketHeartbeatTest, RuntimePingIntervalOverride) {
  50. // The server is already using the compile-time default (1s).
  51. // Create a client with a custom runtime interval.
  52. ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
  53. client.set_websocket_ping_interval(2);
  54. ASSERT_TRUE(client.connect());
  55. // Sleep longer than read timeout (3s). Client heartbeat at 2s keeps alive.
  56. std::this_thread::sleep_for(std::chrono::seconds(5));
  57. ASSERT_TRUE(client.is_open());
  58. ASSERT_TRUE(client.send("runtime interval"));
  59. std::string msg;
  60. ASSERT_TRUE(client.read(msg));
  61. EXPECT_EQ("runtime interval", msg);
  62. client.close();
  63. }
  64. // Verify that ping_interval=0 disables heartbeat without breaking basic I/O.
  65. TEST_F(WebSocketHeartbeatTest, ZeroDisablesHeartbeat) {
  66. ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
  67. client.set_websocket_ping_interval(0);
  68. ASSERT_TRUE(client.connect());
  69. // Basic send/receive still works with heartbeat disabled
  70. ASSERT_TRUE(client.send("no client ping"));
  71. std::string msg;
  72. ASSERT_TRUE(client.read(msg));
  73. EXPECT_EQ("no client ping", msg);
  74. client.close();
  75. }
  76. // Verify that Server::set_websocket_ping_interval works at runtime
  77. class WebSocketServerPingIntervalTest : public ::testing::Test {
  78. protected:
  79. void SetUp() override {
  80. svr_.set_websocket_ping_interval(2);
  81. svr_.WebSocket("/ws", [](const Request &, ws::WebSocket &ws) {
  82. std::string msg;
  83. while (ws.read(msg)) {
  84. ws.send(msg);
  85. }
  86. });
  87. port_ = svr_.bind_to_any_port("localhost");
  88. thread_ = std::thread([this]() { svr_.listen_after_bind(); });
  89. svr_.wait_until_ready();
  90. }
  91. void TearDown() override {
  92. svr_.stop();
  93. thread_.join();
  94. }
  95. Server svr_;
  96. int port_;
  97. std::thread thread_;
  98. };
  99. TEST_F(WebSocketServerPingIntervalTest, ServerRuntimeInterval) {
  100. ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
  101. ASSERT_TRUE(client.connect());
  102. // Server ping interval is 2s; client uses compile-time default (1s).
  103. // Both keep the connection alive.
  104. std::this_thread::sleep_for(std::chrono::seconds(5));
  105. ASSERT_TRUE(client.is_open());
  106. ASSERT_TRUE(client.send("server interval"));
  107. std::string msg;
  108. ASSERT_TRUE(client.read(msg));
  109. EXPECT_EQ("server interval", msg);
  110. client.close();
  111. }
  112. // Verify that the client detects a non-responsive peer via unacked-ping count.
  113. // Setup: the server's heartbeat is disabled AND its handler never calls
  114. // read(), so no automatic Pong reply is ever produced. The client sends
  115. // pings but receives no pongs, and should close itself once the unacked
  116. // ping count reaches max_missed_pongs.
  117. class WebSocketPongTimeoutTest : public ::testing::Test {
  118. protected:
  119. void SetUp() override {
  120. svr_.set_websocket_ping_interval(0);
  121. svr_.WebSocket("/ws", [this](const Request &, ws::WebSocket &) {
  122. std::unique_lock<std::mutex> lock(handler_mutex_);
  123. handler_cv_.wait(lock, [this]() { return release_; });
  124. });
  125. port_ = svr_.bind_to_any_port("localhost");
  126. thread_ = std::thread([this]() { svr_.listen_after_bind(); });
  127. svr_.wait_until_ready();
  128. }
  129. void TearDown() override {
  130. {
  131. std::lock_guard<std::mutex> lock(handler_mutex_);
  132. release_ = true;
  133. }
  134. handler_cv_.notify_all();
  135. svr_.stop();
  136. thread_.join();
  137. }
  138. Server svr_;
  139. int port_;
  140. std::thread thread_;
  141. std::mutex handler_mutex_;
  142. std::condition_variable handler_cv_;
  143. bool release_ = false;
  144. };
  145. TEST_F(WebSocketPongTimeoutTest, ClientDetectsNonResponsivePeer) {
  146. ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
  147. client.set_websocket_max_missed_pongs(2);
  148. ASSERT_TRUE(client.connect());
  149. ASSERT_TRUE(client.is_open());
  150. // Client pings every 1s (compile-time default in this test file).
  151. // With max_missed_pongs = 2, the heartbeat thread should self-close within
  152. // roughly 3s. Poll is_open() up to 6s.
  153. auto start = std::chrono::steady_clock::now();
  154. while (client.is_open() &&
  155. std::chrono::steady_clock::now() - start < std::chrono::seconds(6)) {
  156. std::this_thread::sleep_for(std::chrono::milliseconds(100));
  157. }
  158. EXPECT_FALSE(client.is_open());
  159. }
  160. // Verify that a responsive peer does NOT trigger the pong-timeout mechanism,
  161. // even with a small max_missed_pongs budget. This is the positive counterpart
  162. // of ClientDetectsNonResponsivePeer: the client must actively drive read() so
  163. // that incoming Pong frames are consumed and the unacked counter is reset.
  164. TEST_F(WebSocketHeartbeatTest, ResponsivePeerNeverTimesOut) {
  165. ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
  166. client.set_websocket_max_missed_pongs(2);
  167. ASSERT_TRUE(client.connect());
  168. // Interactive loop over ~6s, longer than 2 ping intervals, so the
  169. // pong-timeout mechanism would trigger if pongs weren't being consumed.
  170. // Each iteration's read() also drains any pending Pong frame.
  171. for (int i = 0; i < 6; i++) {
  172. std::string text = "keepalive" + std::to_string(i);
  173. ASSERT_TRUE(client.send(text));
  174. std::string msg;
  175. ASSERT_TRUE(client.read(msg));
  176. EXPECT_EQ(text, msg);
  177. std::this_thread::sleep_for(std::chrono::seconds(1));
  178. }
  179. EXPECT_TRUE(client.is_open());
  180. client.close();
  181. }
  182. // Verify that multiple heartbeat cycles work
  183. TEST_F(WebSocketHeartbeatTest, MultipleHeartbeatCycles) {
  184. ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
  185. ASSERT_TRUE(client.connect());
  186. // Wait through several heartbeat cycles
  187. for (int i = 0; i < 3; i++) {
  188. std::this_thread::sleep_for(std::chrono::milliseconds(1500));
  189. ASSERT_TRUE(client.is_open());
  190. std::string text = "msg" + std::to_string(i);
  191. ASSERT_TRUE(client.send(text));
  192. std::string msg;
  193. ASSERT_TRUE(client.read(msg));
  194. EXPECT_EQ(text, msg);
  195. }
  196. client.close();
  197. }