test_websocket_heartbeat.cc 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  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. #include <future>
  10. using namespace httplib;
  11. class WebSocketHeartbeatTest : public ::testing::Test {
  12. protected:
  13. void SetUp() override {
  14. svr_.WebSocket("/ws", [](const Request &, ws::WebSocket &ws) {
  15. std::string msg;
  16. while (ws.read(msg)) {
  17. ws.send(msg);
  18. }
  19. });
  20. port_ = svr_.bind_to_any_port("localhost");
  21. thread_ = std::thread([this]() { svr_.listen_after_bind(); });
  22. svr_.wait_until_ready();
  23. }
  24. void TearDown() override {
  25. svr_.stop();
  26. thread_.join();
  27. }
  28. Server svr_;
  29. int port_;
  30. std::thread thread_;
  31. };
  32. // Verify that an idle connection stays alive beyond the read timeout
  33. // thanks to automatic heartbeat pings.
  34. TEST_F(WebSocketHeartbeatTest, IdleConnectionStaysAlive) {
  35. ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
  36. ASSERT_TRUE(client.connect());
  37. // Sleep longer than read timeout (3s). Without heartbeat, the connection
  38. // would time out. With heartbeat pings every 1s, it stays alive.
  39. std::this_thread::sleep_for(std::chrono::seconds(5));
  40. // Connection should still be open
  41. ASSERT_TRUE(client.is_open());
  42. // Verify we can still exchange messages
  43. ASSERT_TRUE(client.send("hello after idle"));
  44. std::string msg;
  45. ASSERT_TRUE(client.read(msg));
  46. EXPECT_EQ("hello after idle", msg);
  47. client.close();
  48. }
  49. // Verify that set_websocket_ping_interval overrides the compile-time default
  50. TEST_F(WebSocketHeartbeatTest, RuntimePingIntervalOverride) {
  51. // The server is already using the compile-time default (1s).
  52. // Create a client with a custom runtime interval.
  53. ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
  54. client.set_websocket_ping_interval(2);
  55. ASSERT_TRUE(client.connect());
  56. // Sleep longer than read timeout (3s). Client heartbeat at 2s keeps alive.
  57. std::this_thread::sleep_for(std::chrono::seconds(5));
  58. ASSERT_TRUE(client.is_open());
  59. ASSERT_TRUE(client.send("runtime interval"));
  60. std::string msg;
  61. ASSERT_TRUE(client.read(msg));
  62. EXPECT_EQ("runtime interval", msg);
  63. client.close();
  64. }
  65. // Verify that ping_interval=0 disables heartbeat without breaking basic I/O.
  66. TEST_F(WebSocketHeartbeatTest, ZeroDisablesHeartbeat) {
  67. ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
  68. client.set_websocket_ping_interval(0);
  69. ASSERT_TRUE(client.connect());
  70. // Basic send/receive still works with heartbeat disabled
  71. ASSERT_TRUE(client.send("no client ping"));
  72. std::string msg;
  73. ASSERT_TRUE(client.read(msg));
  74. EXPECT_EQ("no client ping", msg);
  75. client.close();
  76. }
  77. // Verify that Server::set_websocket_ping_interval works at runtime
  78. class WebSocketServerPingIntervalTest : public ::testing::Test {
  79. protected:
  80. void SetUp() override {
  81. svr_.set_websocket_ping_interval(2);
  82. svr_.WebSocket("/ws", [](const Request &, ws::WebSocket &ws) {
  83. std::string msg;
  84. while (ws.read(msg)) {
  85. ws.send(msg);
  86. }
  87. });
  88. port_ = svr_.bind_to_any_port("localhost");
  89. thread_ = std::thread([this]() { svr_.listen_after_bind(); });
  90. svr_.wait_until_ready();
  91. }
  92. void TearDown() override {
  93. svr_.stop();
  94. thread_.join();
  95. }
  96. Server svr_;
  97. int port_;
  98. std::thread thread_;
  99. };
  100. TEST_F(WebSocketServerPingIntervalTest, ServerRuntimeInterval) {
  101. ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
  102. ASSERT_TRUE(client.connect());
  103. // Server ping interval is 2s; client uses compile-time default (1s).
  104. // Both keep the connection alive.
  105. std::this_thread::sleep_for(std::chrono::seconds(5));
  106. ASSERT_TRUE(client.is_open());
  107. ASSERT_TRUE(client.send("server interval"));
  108. std::string msg;
  109. ASSERT_TRUE(client.read(msg));
  110. EXPECT_EQ("server interval", msg);
  111. client.close();
  112. }
  113. // Verify that the client detects a non-responsive peer via unacked-ping count.
  114. // Setup: the server's heartbeat is disabled AND its handler never calls
  115. // read(), so no automatic Pong reply is ever produced. The client sends
  116. // pings but receives no pongs, and should close itself once the unacked
  117. // ping count reaches max_missed_pongs.
  118. class WebSocketPongTimeoutTest : public ::testing::Test {
  119. protected:
  120. void SetUp() override {
  121. svr_.set_websocket_ping_interval(0);
  122. svr_.WebSocket("/ws", [this](const Request &, ws::WebSocket &) {
  123. std::unique_lock<std::mutex> lock(handler_mutex_);
  124. handler_cv_.wait(lock, [this]() { return release_; });
  125. });
  126. port_ = svr_.bind_to_any_port("localhost");
  127. thread_ = std::thread([this]() { svr_.listen_after_bind(); });
  128. svr_.wait_until_ready();
  129. }
  130. void TearDown() override {
  131. {
  132. std::lock_guard<std::mutex> lock(handler_mutex_);
  133. release_ = true;
  134. }
  135. handler_cv_.notify_all();
  136. svr_.stop();
  137. thread_.join();
  138. }
  139. Server svr_;
  140. int port_;
  141. std::thread thread_;
  142. std::mutex handler_mutex_;
  143. std::condition_variable handler_cv_;
  144. bool release_ = false;
  145. };
  146. TEST_F(WebSocketPongTimeoutTest, ClientDetectsNonResponsivePeer) {
  147. ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
  148. client.set_websocket_max_missed_pongs(2);
  149. // A read timeout asked for at runtime is reported as Timeout, so it cannot
  150. // be mistaken for the Fail a pong timeout produces.
  151. client.set_read_timeout(10);
  152. ASSERT_TRUE(client.connect());
  153. ASSERT_TRUE(client.is_open());
  154. // Client pings every 1s (compile-time default in this test file).
  155. // With max_missed_pongs = 2, the heartbeat thread should self-close within
  156. // roughly 3s, and that has to end a read() already waiting on the peer.
  157. auto start = std::chrono::steady_clock::now();
  158. std::string msg;
  159. EXPECT_EQ(client.read(msg), ws::Fail);
  160. EXPECT_TRUE(std::chrono::steady_clock::now() - start <
  161. std::chrono::seconds(6));
  162. EXPECT_FALSE(client.is_open());
  163. }
  164. // The compile-time client read timeout (3s here) was never asked for through
  165. // set_read_timeout(), so when it elapses read() reports Fail and closes the
  166. // connection rather than handing back a Timeout on a still-open one.
  167. TEST_F(WebSocketPongTimeoutTest, CompileTimeClientReadTimeoutIsFail) {
  168. ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
  169. client.set_websocket_ping_interval(0);
  170. ASSERT_TRUE(client.connect());
  171. // Server pings are off and its handler never sends, so nothing arrives.
  172. std::string msg;
  173. EXPECT_EQ(client.read(msg), ws::Fail);
  174. EXPECT_FALSE(client.is_open());
  175. }
  176. // The compile-time server read timeout (3s here) is a backstop that reclaims
  177. // the worker from a peer gone quiet, not a timeout the handler asked for. When
  178. // it elapses read() must return Fail, so a handler written as
  179. // `while (ws.read(msg))` ends instead of re-running its body with the previous
  180. // message still in `msg`.
  181. class WebSocketServerReadTimeoutTest : public ::testing::Test {
  182. protected:
  183. void SetUp() override {
  184. svr_.set_websocket_ping_interval(0);
  185. svr_.WebSocket("/ws", [this](const Request &, ws::WebSocket &ws) {
  186. std::string msg;
  187. while (ws.read(msg)) {
  188. iterations_++;
  189. ws.send(msg);
  190. }
  191. handler_done_.set_value();
  192. });
  193. port_ = svr_.bind_to_any_port("localhost");
  194. thread_ = std::thread([this]() { svr_.listen_after_bind(); });
  195. svr_.wait_until_ready();
  196. }
  197. void TearDown() override {
  198. svr_.stop();
  199. thread_.join();
  200. }
  201. Server svr_;
  202. int port_;
  203. std::thread thread_;
  204. std::atomic<int> iterations_{0};
  205. std::promise<void> handler_done_;
  206. };
  207. TEST_F(WebSocketServerReadTimeoutTest, BackstopEndsHandlerLoop) {
  208. ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
  209. client.set_websocket_ping_interval(0); // nothing reaches the server's read()
  210. client.set_read_timeout(10, 0); // fail rather than hang
  211. ASSERT_TRUE(client.connect());
  212. ASSERT_TRUE(client.send("hello"));
  213. std::string msg;
  214. ASSERT_EQ(client.read(msg), ws::Text);
  215. EXPECT_EQ("hello", msg);
  216. // The client now stays silent. The server's backstop elapses and the
  217. // handler returns, having run its loop body exactly once.
  218. auto done = handler_done_.get_future();
  219. ASSERT_EQ(done.wait_for(std::chrono::seconds(6)), std::future_status::ready);
  220. EXPECT_EQ(1, iterations_.load());
  221. EXPECT_EQ(client.read(msg), ws::Fail);
  222. EXPECT_FALSE(client.is_open());
  223. }
  224. // Verify that a responsive peer does NOT trigger the pong-timeout mechanism,
  225. // even with a small max_missed_pongs budget. This is the positive counterpart
  226. // of ClientDetectsNonResponsivePeer: the client must actively drive read() so
  227. // that incoming Pong frames are consumed and the unacked counter is reset.
  228. TEST_F(WebSocketHeartbeatTest, ResponsivePeerNeverTimesOut) {
  229. ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
  230. client.set_websocket_max_missed_pongs(2);
  231. ASSERT_TRUE(client.connect());
  232. // Interactive loop over ~6s, longer than 2 ping intervals, so the
  233. // pong-timeout mechanism would trigger if pongs weren't being consumed.
  234. // Each iteration's read() also drains any pending Pong frame.
  235. for (int i = 0; i < 6; i++) {
  236. std::string text = "keepalive" + std::to_string(i);
  237. ASSERT_TRUE(client.send(text));
  238. std::string msg;
  239. ASSERT_TRUE(client.read(msg));
  240. EXPECT_EQ(text, msg);
  241. std::this_thread::sleep_for(std::chrono::seconds(1));
  242. }
  243. EXPECT_TRUE(client.is_open());
  244. client.close();
  245. }
  246. // Verify that multiple heartbeat cycles work
  247. TEST_F(WebSocketHeartbeatTest, MultipleHeartbeatCycles) {
  248. ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
  249. ASSERT_TRUE(client.connect());
  250. // Wait through several heartbeat cycles
  251. for (int i = 0; i < 3; i++) {
  252. std::this_thread::sleep_for(std::chrono::milliseconds(1500));
  253. ASSERT_TRUE(client.is_open());
  254. std::string text = "msg" + std::to_string(i);
  255. ASSERT_TRUE(client.send(text));
  256. std::string msg;
  257. ASSERT_TRUE(client.read(msg));
  258. EXPECT_EQ(text, msg);
  259. }
  260. client.close();
  261. }