1
0

test_websocket_heartbeat.cc 7.4 KB

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