Sfoglia il codice sorgente

Wake a blocked WebSocket read when the pong timeout fires

When max_missed_pongs was exceeded, the heartbeat thread called close(),
which returns without touching the socket while another thread is in
read(). That read() then stayed blocked until its read timeout: forever
by default on a client, 300 seconds on a server. A plain read loop never
noticed the unresponsive peer the heartbeat had just detected.

Shut down the read side of the socket after close() so the pending
read() returns Fail. Only the read side: a TLS backend answers the EOF
with an alert, and writing it to a socket closed for writing raises
SIGPIPE in a process that has not ignored it.

ClientDetectsNonResponsivePeer now waits in read() instead of polling
is_open(), which covers both.
yhirose 1 giorno fa
parent
commit
8aa547e4a1
2 ha cambiato i file con 16 aggiunte e 6 eliminazioni
  1. 8 0
      httplib.h
  2. 8 6
      test/test_websocket_heartbeat.cc

+ 8 - 0
httplib.h

@@ -22843,6 +22843,14 @@ inline void WebSocket::start_heartbeat() {
       if (max_missed_pongs_ > 0 && unacked_pings_ >= max_missed_pongs_) {
         lock.unlock();
         close(CloseStatus::GoingAway, "pong timeout");
+        // Wake a read() blocked on the unresponsive peer. Only the read side
+        // is shut down: a TLS backend answers the EOF with an alert, and
+        // writing it to a socket closed for writing raises SIGPIPE.
+#ifdef _WIN32
+        shutdown(strm_.socket(), SD_RECEIVE);
+#else
+        shutdown(strm_.socket(), SHUT_RD);
+#endif
         return;
       }
       lock.unlock();

+ 8 - 6
test/test_websocket_heartbeat.cc

@@ -179,18 +179,20 @@ protected:
 TEST_F(WebSocketPongTimeoutTest, ClientDetectsNonResponsivePeer) {
   ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
   client.set_websocket_max_missed_pongs(2);
+  // A read timeout asked for at runtime is reported as Timeout, so it cannot
+  // be mistaken for the Fail a pong timeout produces.
+  client.set_read_timeout(10);
   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.
+  // roughly 3s, and that has to end a read() already waiting on the peer.
   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));
-  }
-
+  std::string msg;
+  EXPECT_EQ(client.read(msg), ws::Fail);
+  EXPECT_TRUE(std::chrono::steady_clock::now() - start <
+              std::chrono::seconds(6));
   EXPECT_FALSE(client.is_open());
 }