mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-22 11:46:13 +00:00
Enhance WebSocket support with unresponsive-peer detection and documentation updates
- Added `set_websocket_max_missed_pongs` method to configure unresponsive-peer detection. - Updated README and documentation to clarify WebSocket limitations and features. - Introduced tests for detecting non-responsive peers and ensuring responsive peers do not trigger timeouts.
This commit is contained in:
1 parent
b4eec3ee77
commit
6bdd657713
6 files changed
+199
-12
No files matched your search
+21
-2
@@ -3,15 +3,19 @@
|
|||||||
A simple, blocking WebSocket implementation for C++11.
|
A simple, blocking WebSocket implementation for C++11.
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> This is a blocking I/O WebSocket implementation using a thread-per-connection model. If you need high-concurrency WebSocket support with non-blocking/async I/O (e.g., thousands of simultaneous connections), this is not the one that you want.
|
> This is a blocking I/O WebSocket implementation using a thread-per-connection model (plus one heartbeat thread per connection). It is intended for small- to mid-scale workloads; handling large numbers of simultaneous WebSocket connections is outside the design target of this library. If you need high-concurrency WebSocket support with non-blocking/async I/O (e.g., thousands of simultaneous connections), this is not the one that you want.
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> WebSocket extensions (`permessage-deflate` and others defined by RFC 6455) are **not supported**. If a client proposes an extension via `Sec-WebSocket-Extensions`, the server silently declines it — the negotiated connection always runs without extensions.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **RFC 6455 compliant**: Full WebSocket protocol support
|
- **RFC 6455 compliant**: Full WebSocket protocol support (extensions are not implemented)
|
||||||
- **Server and Client**: Both sides included
|
- **Server and Client**: Both sides included
|
||||||
- **SSL/TLS support**: `wss://` scheme for secure connections
|
- **SSL/TLS support**: `wss://` scheme for secure connections
|
||||||
- **Text and Binary**: Both message types supported
|
- **Text and Binary**: Both message types supported
|
||||||
- **Automatic heartbeat**: Periodic Ping/Pong keeps connections alive
|
- **Automatic heartbeat**: Periodic Ping/Pong keeps connections alive
|
||||||
|
- **Unresponsive-peer detection**: Opt-in liveness check via `set_websocket_max_missed_pongs()`
|
||||||
- **Subprotocol negotiation**: `Sec-WebSocket-Protocol` support for GraphQL, MQTT, etc.
|
- **Subprotocol negotiation**: `Sec-WebSocket-Protocol` support for GraphQL, MQTT, etc.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
@@ -352,6 +356,7 @@ if (ws.connect()) {
|
|||||||
| `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` | `300` | Read timeout for WebSocket connections (seconds) |
|
| `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` | `300` | Read timeout for WebSocket connections (seconds) |
|
||||||
| `CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND` | `5` | Timeout for waiting peer's Close response (seconds) |
|
| `CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND` | `5` | Timeout for waiting peer's Close response (seconds) |
|
||||||
| `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND` | `30` | Automatic Ping interval for heartbeat (seconds) |
|
| `CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND` | `30` | Automatic Ping interval for heartbeat (seconds) |
|
||||||
|
| `CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS` | `0` (disabled) | Close the connection after N consecutive unacked pings |
|
||||||
|
|
||||||
### Runtime Ping Interval
|
### Runtime Ping Interval
|
||||||
|
|
||||||
@@ -373,6 +378,20 @@ ws.set_websocket_ping_interval(10); // 10 seconds
|
|||||||
ws.set_websocket_ping_interval(0);
|
ws.set_websocket_ping_interval(0);
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Unresponsive-Peer Detection (Pong Timeout)
|
||||||
|
|
||||||
|
By default the heartbeat only sends pings — it does not enforce that pongs come back. To detect a silently dropped connection faster, enable the max-missed-pongs check. Once `max_missed_pongs` consecutive pings go unanswered, the heartbeat thread closes the connection with `CloseStatus::GoingAway` and the reason `"pong timeout"`.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
ws.set_websocket_max_missed_pongs(2); // close after 2 consecutive unacked pings
|
||||||
|
```
|
||||||
|
|
||||||
|
The server side has the same `set_websocket_max_missed_pongs()`.
|
||||||
|
|
||||||
|
With the default ping interval of 30 seconds, `max_missed_pongs = 2` detects a dead peer within ~60 seconds. The counter is reset every time a Pong frame is received, so the mechanism only works when your code is actively calling `read()` — exactly the pattern a normal WebSocket client already uses.
|
||||||
|
|
||||||
|
**The default is `0`**, which means "never close the connection because of missing pongs." Pings are still sent on the heartbeat interval, but their responses are not checked. Even so, a dead connection does not linger forever: while your code is inside `read()`, `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop and `read()` fails if no frame arrives in time. `max_missed_pongs` is the knob for detecting an unresponsive peer faster than that 5-minute fallback.
|
||||||
|
|
||||||
## Threading Model
|
## Threading Model
|
||||||
|
|
||||||
WebSocket connections share the same thread pool as HTTP requests. Each WebSocket connection occupies one thread for its entire lifetime.
|
WebSocket connections share the same thread pool as HTTP requests. Each WebSocket connection occupies one thread for its entire lifetime.
|
||||||
|
|||||||
@@ -1462,7 +1462,11 @@ if (ws.connect()) {
|
|||||||
|
|
||||||
SSL is also supported via `wss://` scheme (e.g. `WebSocketClient("wss://example.com/ws")`). Subprotocol negotiation (`Sec-WebSocket-Protocol`) is supported via `SubProtocolSelector` callback.
|
SSL is also supported via `wss://` scheme (e.g. `WebSocketClient("wss://example.com/ws")`). Subprotocol negotiation (`Sec-WebSocket-Protocol`) is supported via `SubProtocolSelector` callback.
|
||||||
|
|
||||||
> **Note:** WebSocket connections occupy a thread for their entire lifetime. If you plan to handle many simultaneous WebSocket connections, consider using a dynamic thread pool: `svr.new_task_queue = [] { return new ThreadPool(8, 64); };`
|
> **Note:** WebSocket connections occupy a thread for their entire lifetime (plus an additional thread per connection for heartbeat pings). This thread-per-connection model is intended for small- to mid-scale workloads; large numbers of simultaneous WebSocket connections are outside the design target of this library. If you expect many concurrent WebSocket clients, configure a dynamic thread pool (`svr.new_task_queue = [] { return new ThreadPool(8, 64); };`) and measure carefully.
|
||||||
|
|
||||||
|
> **WebSocket extensions are not supported.** `permessage-deflate` and other RFC 6455 extensions are not implemented. If a client proposes them via `Sec-WebSocket-Extensions`, the server silently declines them in its handshake response.
|
||||||
|
|
||||||
|
> **Unresponsive-peer detection.** Heartbeat pings also serve as a liveness probe when `set_websocket_max_missed_pongs(n)` is set: if the client sends `n` consecutive pings without receiving a pong, it will close the connection. Disabled by default (`0`).
|
||||||
|
|
||||||
See [README-websocket.md](README-websocket.md) for more details.
|
See [README-websocket.md](README-websocket.md) for more details.
|
||||||
|
|
||||||
|
|||||||
@@ -57,4 +57,24 @@ Too short wastes bandwidth; too long and connections get dropped. As a rule of t
|
|||||||
|
|
||||||
> **Warning:** A very short ping interval spawns background work per connection and increases CPU usage. For servers with many connections, keep the interval modest.
|
> **Warning:** A very short ping interval spawns background work per connection and increases CPU usage. For servers with many connections, keep the interval modest.
|
||||||
|
|
||||||
|
## Detecting an unresponsive peer
|
||||||
|
|
||||||
|
Sending pings alone doesn't tell you anything if the peer just silently dies — the TCP socket might still look open while the process on the other end is long gone. To catch that, enable the max-missed-pongs check: if N consecutive pings go unanswered, the connection is closed.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
cli.set_websocket_max_missed_pongs(2); // close after 2 consecutive unacked pings
|
||||||
|
```
|
||||||
|
|
||||||
|
The server side has the same `set_websocket_max_missed_pongs()`.
|
||||||
|
|
||||||
|
With a 30-second ping interval and `max_missed_pongs = 2`, a dead peer is detected within roughly 60 seconds and the connection is closed with `CloseStatus::GoingAway` and the reason `"pong timeout"`.
|
||||||
|
|
||||||
|
The counter is reset whenever `read()` consumes an incoming Pong frame, so this only works if your code is actively calling `read()` in a loop — which is what a normal WebSocket client does anyway.
|
||||||
|
|
||||||
|
### Why the default is 0
|
||||||
|
|
||||||
|
`max_missed_pongs` defaults to `0`, which means "never close the connection because of missing pongs." Pings are still sent on the heartbeat interval, but their responses aren't checked. If you want unresponsive-peer detection, set it explicitly to `1` or higher.
|
||||||
|
|
||||||
|
Even with `0`, a dead connection won't linger forever: while your code is inside `read()`, `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop and `read()` fails if no frame arrives in time. Think of `max_missed_pongs` as the knob for detecting an unresponsive peer **faster** than that.
|
||||||
|
|
||||||
> For handling a closed connection, see [W03. Handle connection close](w03-websocket-close).
|
> For handling a closed connection, see [W03. Handle connection close](w03-websocket-close).
|
||||||
@@ -57,4 +57,24 @@ WebSocketプロトコルでは、PingフレームにはPongフレームで応答
|
|||||||
|
|
||||||
> **Warning:** Ping間隔を極端に短くすると、WebSocket接続ごとにバックグラウンドでスレッドが走るので、CPU負荷が上がります。接続数が多いサーバーでは控えめな値に設定しましょう。
|
> **Warning:** Ping間隔を極端に短くすると、WebSocket接続ごとにバックグラウンドでスレッドが走るので、CPU負荷が上がります。接続数が多いサーバーでは控えめな値に設定しましょう。
|
||||||
|
|
||||||
|
## 無応答のピアを検出する
|
||||||
|
|
||||||
|
Pingを送るだけでは、相手が「黙って落ちた」場合に気付けません。TCPの接続自体は生きているように見えるのに、相手のプロセスはもう応答しない、というケースです。これを検出するには、送ったPingに対してPongがN回連続で返ってこなかったら接続を切る、というオプションを有効にします。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
cli.set_websocket_max_missed_pongs(2); // 2回連続でPongが返ってこなければ切断
|
||||||
|
```
|
||||||
|
|
||||||
|
サーバー側にも同じ`set_websocket_max_missed_pongs()`があります。
|
||||||
|
|
||||||
|
たとえばPing間隔が30秒で`max_missed_pongs = 2`なら、無応答のピアは約60秒で検出され、`CloseStatus::GoingAway`(理由は`"pong timeout"`)で接続が閉じられます。
|
||||||
|
|
||||||
|
この仕組みは`read()`を呼んでPongフレームを消費したタイミングでカウンタがリセットされます。つまり通常のWebSocketクライアントのように`read()`をループで回していれば、特に意識することなく動きます。
|
||||||
|
|
||||||
|
### デフォルトは無効
|
||||||
|
|
||||||
|
`max_missed_pongs`のデフォルトは`0`で、これは「Pongが何回返ってこなくてもこの仕組みでは切断しない」という意味です。Ping自体は送られ続けますが、応答の有無はチェックされません。無応答ピアを検出したい場合は明示的に`1`以上を設定してください。
|
||||||
|
|
||||||
|
ただし`0`のままでも最終的に接続が残り続けることはありません。`read()`を呼んでいる間は`CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND`(デフォルト**300秒 = 5分**)が保険として働き、フレームが一定時間来なければ`read()`が失敗します。つまり`max_missed_pongs`は「**もっと速く**無応答を検出したい」ときに使うオプションだと考えてください。
|
||||||
|
|
||||||
> 接続が閉じたときの処理は[W03. 接続クローズをハンドリングする](w03-websocket-close)を参照してください。
|
> 接続が閉じたときの処理は[W03. 接続クローズをハンドリングする](w03-websocket-close)を参照してください。
|
||||||
@@ -205,6 +205,10 @@
|
|||||||
#define CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND 30
|
#define CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND 30
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS
|
||||||
|
#define CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS 0
|
||||||
|
#endif
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Headers
|
* Headers
|
||||||
*/
|
*/
|
||||||
@@ -1720,6 +1724,8 @@ public:
|
|||||||
Server &set_websocket_ping_interval(
|
Server &set_websocket_ping_interval(
|
||||||
const std::chrono::duration<Rep, Period> &duration);
|
const std::chrono::duration<Rep, Period> &duration);
|
||||||
|
|
||||||
|
Server &set_websocket_max_missed_pongs(int count);
|
||||||
|
|
||||||
bool bind_to_port(const std::string &host, int port, int socket_flags = 0);
|
bool bind_to_port(const std::string &host, int port, int socket_flags = 0);
|
||||||
int bind_to_any_port(const std::string &host, int socket_flags = 0);
|
int bind_to_any_port(const std::string &host, int socket_flags = 0);
|
||||||
bool listen_after_bind();
|
bool listen_after_bind();
|
||||||
@@ -1756,6 +1762,7 @@ protected:
|
|||||||
size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;
|
size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;
|
||||||
time_t websocket_ping_interval_sec_ =
|
time_t websocket_ping_interval_sec_ =
|
||||||
CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND;
|
CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND;
|
||||||
|
int websocket_max_missed_pongs_ = CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
using Handlers =
|
using Handlers =
|
||||||
@@ -3728,17 +3735,21 @@ private:
|
|||||||
|
|
||||||
WebSocket(
|
WebSocket(
|
||||||
Stream &strm, const Request &req, bool is_server,
|
Stream &strm, const Request &req, bool is_server,
|
||||||
time_t ping_interval_sec = CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND)
|
time_t ping_interval_sec = CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND,
|
||||||
|
int max_missed_pongs = CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS)
|
||||||
: strm_(strm), req_(req), is_server_(is_server),
|
: strm_(strm), req_(req), is_server_(is_server),
|
||||||
ping_interval_sec_(ping_interval_sec) {
|
ping_interval_sec_(ping_interval_sec),
|
||||||
|
max_missed_pongs_(max_missed_pongs) {
|
||||||
start_heartbeat();
|
start_heartbeat();
|
||||||
}
|
}
|
||||||
|
|
||||||
WebSocket(
|
WebSocket(
|
||||||
std::unique_ptr<Stream> &&owned_strm, const Request &req, bool is_server,
|
std::unique_ptr<Stream> &&owned_strm, const Request &req, bool is_server,
|
||||||
time_t ping_interval_sec = CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND)
|
time_t ping_interval_sec = CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND,
|
||||||
|
int max_missed_pongs = CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS)
|
||||||
: strm_(*owned_strm), owned_strm_(std::move(owned_strm)), req_(req),
|
: strm_(*owned_strm), owned_strm_(std::move(owned_strm)), req_(req),
|
||||||
is_server_(is_server), ping_interval_sec_(ping_interval_sec) {
|
is_server_(is_server), ping_interval_sec_(ping_interval_sec),
|
||||||
|
max_missed_pongs_(max_missed_pongs) {
|
||||||
start_heartbeat();
|
start_heartbeat();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3750,6 +3761,8 @@ private:
|
|||||||
Request req_;
|
Request req_;
|
||||||
bool is_server_;
|
bool is_server_;
|
||||||
time_t ping_interval_sec_;
|
time_t ping_interval_sec_;
|
||||||
|
int max_missed_pongs_;
|
||||||
|
int unacked_pings_ = 0;
|
||||||
std::atomic<bool> closed_{false};
|
std::atomic<bool> closed_{false};
|
||||||
std::mutex write_mutex_;
|
std::mutex write_mutex_;
|
||||||
std::thread ping_thread_;
|
std::thread ping_thread_;
|
||||||
@@ -3779,6 +3792,7 @@ public:
|
|||||||
void set_read_timeout(time_t sec, time_t usec = 0);
|
void set_read_timeout(time_t sec, time_t usec = 0);
|
||||||
void set_write_timeout(time_t sec, time_t usec = 0);
|
void set_write_timeout(time_t sec, time_t usec = 0);
|
||||||
void set_websocket_ping_interval(time_t sec);
|
void set_websocket_ping_interval(time_t sec);
|
||||||
|
void set_websocket_max_missed_pongs(int count);
|
||||||
void set_tcp_nodelay(bool on);
|
void set_tcp_nodelay(bool on);
|
||||||
void set_address_family(int family);
|
void set_address_family(int family);
|
||||||
void set_ipv6_v6only(bool on);
|
void set_ipv6_v6only(bool on);
|
||||||
@@ -3810,6 +3824,7 @@ private:
|
|||||||
time_t write_timeout_usec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND;
|
time_t write_timeout_usec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND;
|
||||||
time_t websocket_ping_interval_sec_ =
|
time_t websocket_ping_interval_sec_ =
|
||||||
CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND;
|
CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND;
|
||||||
|
int websocket_max_missed_pongs_ = CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS;
|
||||||
int address_family_ = AF_UNSPEC;
|
int address_family_ = AF_UNSPEC;
|
||||||
bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY;
|
bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY;
|
||||||
bool ipv6_v6only_ = CPPHTTPLIB_IPV6_V6ONLY;
|
bool ipv6_v6only_ = CPPHTTPLIB_IPV6_V6ONLY;
|
||||||
@@ -10912,6 +10927,11 @@ inline Server &Server::set_payload_max_length(size_t length) {
|
|||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline Server &Server::set_websocket_max_missed_pongs(int count) {
|
||||||
|
websocket_max_missed_pongs_ = count;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
inline Server &Server::set_websocket_ping_interval(time_t sec) {
|
inline Server &Server::set_websocket_ping_interval(time_t sec) {
|
||||||
websocket_ping_interval_sec_ = sec;
|
websocket_ping_interval_sec_ = sec;
|
||||||
return *this;
|
return *this;
|
||||||
@@ -12050,7 +12070,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|||||||
{
|
{
|
||||||
// Use WebSocket-specific read timeout instead of HTTP timeout
|
// Use WebSocket-specific read timeout instead of HTTP timeout
|
||||||
strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND, 0);
|
strm.set_read_timeout(CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND, 0);
|
||||||
ws::WebSocket ws(strm, req, true, websocket_ping_interval_sec_);
|
ws::WebSocket ws(strm, req, true, websocket_ping_interval_sec_,
|
||||||
|
websocket_max_missed_pongs_);
|
||||||
entry.handler(req, ws);
|
entry.handler(req, ws);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -19700,7 +19721,11 @@ inline ReadResult WebSocket::read(std::string &msg) {
|
|||||||
payload.size(), true, !is_server_);
|
payload.size(), true, !is_server_);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
case Opcode::Pong: continue;
|
case Opcode::Pong: {
|
||||||
|
std::lock_guard<std::mutex> lock(ping_mutex_);
|
||||||
|
unacked_pings_ = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
case Opcode::Close: {
|
case Opcode::Close: {
|
||||||
if (!closed_.exchange(true)) {
|
if (!closed_.exchange(true)) {
|
||||||
// Echo close frame back
|
// Echo close frame back
|
||||||
@@ -19734,7 +19759,11 @@ inline ReadResult WebSocket::read(std::string &msg) {
|
|||||||
true, !is_server_);
|
true, !is_server_);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (cont_opcode == Opcode::Pong) { continue; }
|
if (cont_opcode == Opcode::Pong) {
|
||||||
|
std::lock_guard<std::mutex> lock(ping_mutex_);
|
||||||
|
unacked_pings_ = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (cont_opcode == Opcode::Close) {
|
if (cont_opcode == Opcode::Close) {
|
||||||
if (!closed_.exchange(true)) {
|
if (!closed_.exchange(true)) {
|
||||||
std::lock_guard<std::mutex> lock(write_mutex_);
|
std::lock_guard<std::mutex> lock(write_mutex_);
|
||||||
@@ -19822,12 +19851,22 @@ inline void WebSocket::start_heartbeat() {
|
|||||||
while (!closed_) {
|
while (!closed_) {
|
||||||
ping_cv_.wait_for(lock, std::chrono::seconds(ping_interval_sec_));
|
ping_cv_.wait_for(lock, std::chrono::seconds(ping_interval_sec_));
|
||||||
if (closed_) { break; }
|
if (closed_) { break; }
|
||||||
|
// If the peer has failed to respond to the previous pings, give up.
|
||||||
|
// RFC 6455 does not define a pong-timeout mechanism; this is an
|
||||||
|
// opt-in liveness check controlled by max_missed_pongs_.
|
||||||
|
if (max_missed_pongs_ > 0 && unacked_pings_ >= max_missed_pongs_) {
|
||||||
|
lock.unlock();
|
||||||
|
close(CloseStatus::GoingAway, "pong timeout");
|
||||||
|
return;
|
||||||
|
}
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
if (!send_frame(Opcode::Ping, nullptr, 0)) {
|
if (!send_frame(Opcode::Ping, nullptr, 0)) {
|
||||||
|
lock.lock();
|
||||||
closed_ = true;
|
closed_ = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
lock.lock();
|
lock.lock();
|
||||||
|
unacked_pings_++;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -19955,8 +19994,9 @@ inline bool WebSocketClient::connect() {
|
|||||||
Request req;
|
Request req;
|
||||||
req.method = "GET";
|
req.method = "GET";
|
||||||
req.path = path_;
|
req.path = path_;
|
||||||
ws_ = std::unique_ptr<WebSocket>(
|
ws_ = std::unique_ptr<WebSocket>(new WebSocket(std::move(strm), req, false,
|
||||||
new WebSocket(std::move(strm), req, false, websocket_ping_interval_sec_));
|
websocket_ping_interval_sec_,
|
||||||
|
websocket_max_missed_pongs_));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20000,6 +20040,10 @@ inline void WebSocketClient::set_websocket_ping_interval(time_t sec) {
|
|||||||
websocket_ping_interval_sec_ = sec;
|
websocket_ping_interval_sec_ = sec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline void WebSocketClient::set_websocket_max_missed_pongs(int count) {
|
||||||
|
websocket_max_missed_pongs_ = count;
|
||||||
|
}
|
||||||
|
|
||||||
inline void WebSocketClient::set_tcp_nodelay(bool on) { tcp_nodelay_ = on; }
|
inline void WebSocketClient::set_tcp_nodelay(bool on) { tcp_nodelay_ = on; }
|
||||||
|
|
||||||
inline void WebSocketClient::set_address_family(int family) {
|
inline void WebSocketClient::set_address_family(int family) {
|
||||||
|
|||||||
@@ -136,6 +136,86 @@ TEST_F(WebSocketServerPingIntervalTest, ServerRuntimeInterval) {
|
|||||||
client.close();
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
// Verify that multiple heartbeat cycles work
|
||||||
TEST_F(WebSocketHeartbeatTest, MultipleHeartbeatCycles) {
|
TEST_F(WebSocketHeartbeatTest, MultipleHeartbeatCycles) {
|
||||||
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
|
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) + "/ws");
|
||||||
|
|||||||
Reference in new issue
Block a user