mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-29 12:50:51 +00:00
Tell a WebSocket read timeout apart from a closed connection
read() collapsed every failure into Fail and marked the connection closed with it, so a read timeout could not be used to get control back and send on the same connection -- it killed the connection instead. The information was already there and thrown away: SocketStream::read records Error::Timeout, and read_websocket_frame flattened it into a bool. ReadResult gains Timeout, reported only when the timeout elapsed on a frame boundary with nothing consumed, which is the only case where the stream can be read again. Every multi-byte field now loops until it has its bytes, which also fixes a frame header straddling the read buffer's boundary failing the frame: Stream::read is allowed to return less than asked for, and only the payload was reading in a loop. ws::WebSocket::set_read_timeout() lets a server handler bound its own reads, and WebSocketClient::set_read_timeout() now reaches an already-open connection instead of only seeding the next connect(). The read timeout macro splits in two. A client waits forever by default -- a read timeout is the caller's tool for taking back control, not a liveness check, which is ping/pong's job -- while a server keeps the 300s that reclaims a worker from a peer gone quiet. Defining the old name still sets both. Also record a reason on the two WebSocketSSLStream::read failure paths that returned -1 without one, so get_error() cannot report a previous call's timeout, and make SocketStream's read timeout atomic now that it can be changed while a read is in flight.
This commit is contained in:
1 parent
9e2e33da56
commit
199d7ee248
12 files changed
+449
-88
No files matched your search
+22
-5
@@ -57,14 +57,30 @@ if (ws.connect()) {
|
|||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
enum ReadResult : int {
|
enum ReadResult : int {
|
||||||
Fail = 0, // Connection closed or error
|
Fail = 0, // Connection closed or error
|
||||||
Text = 1, // UTF-8 text message
|
Text = 1, // UTF-8 text message
|
||||||
Binary = 2, // Binary message
|
Binary = 2, // Binary message
|
||||||
|
Timeout = 3, // Read timeout elapsed; connection still open
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Returned by `read()`. Since `Fail` is `0`, the result works naturally in boolean contexts — `while (ws.read(msg))` continues until the connection closes. When you need to distinguish text from binary, check the return value directly.
|
Returned by `read()`. Since `Fail` is `0`, the result works naturally in boolean contexts — `while (ws.read(msg))` continues until the connection closes. When you need to distinguish text from binary, check the return value directly.
|
||||||
|
|
||||||
|
`Timeout` only appears once a read timeout is in effect (a client waits forever unless you set one; a server uses `CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND`). It means the timeout elapsed on a message boundary: nothing was consumed and the connection is still open, so you can send on it and read again.
|
||||||
|
|
||||||
|
**`msg` is left untouched on `Timeout`.** Because `Timeout` is non-zero, `while (ws.read(msg))` keeps looping — with the *previous* message still in `msg`. Once a read timeout is set, test the result instead:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
ws.set_read_timeout(std::chrono::milliseconds(100));
|
||||||
|
std::string msg;
|
||||||
|
while (ws.is_open()) {
|
||||||
|
auto r = ws.read(msg);
|
||||||
|
if (r == httplib::ws::Timeout) { continue; } // nothing yet; send if you like
|
||||||
|
if (r == httplib::ws::Fail) { break; }
|
||||||
|
handle(msg);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
### CloseStatus
|
### CloseStatus
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
@@ -409,7 +425,8 @@ if (ws.connect()) {
|
|||||||
| Macro | Default | Description |
|
| Macro | Default | Description |
|
||||||
|---------------------------------------------|-------------------|----------------------------------------------------------|
|
|---------------------------------------------|-------------------|----------------------------------------------------------|
|
||||||
| `CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH` | `16777216` (16MB) | Maximum payload size per message |
|
| `CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH` | `16777216` (16MB) | Maximum payload size per message |
|
||||||
| `CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND` | `300` | Read timeout for WebSocket connections (seconds) |
|
| `CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND` | `0` | Client read timeout (seconds); `0` waits forever |
|
||||||
|
| `CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND` | `300` | Server read timeout (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 |
|
| `CPPHTTPLIB_WEBSOCKET_MAX_MISSED_PONGS` | `0` (disabled) | Close the connection after N consecutive unacked pings |
|
||||||
@@ -446,7 +463,7 @@ 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.
|
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.
|
**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. On the server side a dead connection still does not linger: while a handler is inside `read()`, `CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop. A client has no such backstop — it waits forever unless you set a read timeout — so there `max_missed_pongs` is what notices an unresponsive peer at all. On either side it is also the knob for noticing one *faster* than the 5-minute fallback.
|
||||||
|
|
||||||
## Threading Model
|
## Threading Model
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ The `read()` return value is a `ReadResult` enum:
|
|||||||
- `ReadResult::Text`: received a text message
|
- `ReadResult::Text`: received a text message
|
||||||
- `ReadResult::Binary`: received a binary message
|
- `ReadResult::Binary`: received a binary message
|
||||||
- `ReadResult::Fail`: error, or connection closed
|
- `ReadResult::Fail`: error, or connection closed
|
||||||
|
- `ReadResult::Timeout`: the read timeout elapsed with nothing received; the connection is still open. Only appears once a read timeout is set — see [W06. Set Timeouts](../w06-websocket-timeouts)
|
||||||
|
|
||||||
## Client: talk to the echo server
|
## Client: talk to the echo server
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,6 @@ The counter is reset whenever `read()` consumes an incoming Pong frame, so this
|
|||||||
|
|
||||||
`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.
|
`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.
|
On the server side, even with `0`, a dead connection won't linger forever: while a handler is inside `read()`, `CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND` (default **300 seconds = 5 minutes**) acts as a backstop. A client has no backstop of its own — it waits forever unless you set a read timeout — so there `max_missed_pongs` is what notices an unresponsive peer at all. On either side, it is also how you notice one **faster** than that 5-minute fallback.
|
||||||
|
|
||||||
> 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).
|
||||||
@@ -42,6 +42,9 @@ switch (result) {
|
|||||||
case httplib::ws::ReadResult::Fail:
|
case httplib::ws::ReadResult::Fail:
|
||||||
// error or closed
|
// error or closed
|
||||||
break;
|
break;
|
||||||
|
case httplib::ws::ReadResult::Timeout:
|
||||||
|
// read timeout elapsed; the connection is still open
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ status: "draft"
|
|||||||
| Kind | API | Default |
|
| Kind | API | Default |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Connection | `set_connection_timeout` | 300s |
|
| Connection | `set_connection_timeout` | 300s |
|
||||||
| Read | `set_read_timeout` | 300s (`CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND`) |
|
| Read | `set_read_timeout` | none — waits forever (`CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND`) |
|
||||||
| Write | `set_write_timeout` | 5s |
|
| Write | `set_write_timeout` | 5s |
|
||||||
|
|
||||||
## Basic usage
|
## Basic usage
|
||||||
@@ -26,7 +26,7 @@ if (ws.connect()) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Set these before calling `connect()`.
|
Set the connection and write timeouts before calling `connect()`. The read timeout can be changed at any time — setting it on an open connection takes effect on the next `read()`.
|
||||||
|
|
||||||
## Use `std::chrono`
|
## Use `std::chrono`
|
||||||
|
|
||||||
@@ -40,9 +40,42 @@ ws.set_read_timeout(30s);
|
|||||||
ws.set_write_timeout(10s);
|
ws.set_write_timeout(10s);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Watch out for what the read timeout means
|
## What the read timeout means
|
||||||
|
|
||||||
`set_read_timeout()` applies to a single `read()` call. If no message arrives within that time, `read()` returns `ReadResult::Fail`. For connections where long idle periods are normal — waiting on notifications, for example — set a longer timeout, or reconnect from your application code when the read fails.
|
`set_read_timeout()` applies to a single `read()` call. If no message arrives within that time, `read()` returns `ReadResult::Timeout`: **the connection is still open** and nothing was consumed, so you can send on it and read again. That is what separates it from `ReadResult::Fail`, which means the connection is gone.
|
||||||
|
|
||||||
|
This is what lets one thread own a connection in both directions:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
ws.set_read_timeout(100ms);
|
||||||
|
std::string msg;
|
||||||
|
while (ws.is_open()) {
|
||||||
|
auto r = ws.read(msg);
|
||||||
|
if (r == httplib::ws::Timeout) {
|
||||||
|
flush_outgoing(ws); // nothing arrived — send whatever is queued
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r == httplib::ws::Fail) { break; }
|
||||||
|
handle(msg);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Without a read timeout, `read()` blocks until a message arrives, so the thread holding the connection never gets to its writes.
|
||||||
|
|
||||||
|
Two things to know about `Timeout`:
|
||||||
|
|
||||||
|
- It leaves `msg` untouched, and it is non-zero. So `while (ws.read(msg))` is not usable once a read timeout is set — the loop would keep running with the *previous* message still in `msg`.
|
||||||
|
- It is only reported on a message boundary. If the timeout elapses partway through a fragmented message, that message cannot be resumed and `read()` returns `Fail`.
|
||||||
|
|
||||||
|
For connections where long idle periods are normal — waiting on notifications, for example — either leave the read timeout unset, or treat `Timeout` as the no-op it is and keep looping.
|
||||||
|
|
||||||
|
## On the server side
|
||||||
|
|
||||||
|
A handler's `ws::WebSocket` has `set_read_timeout()` too, and the pattern above is how a handler relays between connections instead of parking in `read()`.
|
||||||
|
|
||||||
|
The server default is 300s (`CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND`) rather than "forever": it is a backstop that reclaims a worker from a peer that has gone silent, since a WebSocket handler holds its worker for the life of the connection.
|
||||||
|
|
||||||
> Unresponsive-peer detection via Ping/Pong is a separate mechanism. See [W02. Set a WebSocket Heartbeat](../w02-websocket-ping) for details.
|
> Unresponsive-peer detection via Ping/Pong is a separate mechanism. See [W02. Set a WebSocket Heartbeat](../w02-websocket-ping) for details.
|
||||||
|
|
||||||
|
|||||||
@@ -31,11 +31,12 @@ int main() {
|
|||||||
|
|
||||||
`svr.WebSocket()`でWebSocket用のハンドラを登録します。ハンドラが呼ばれた時点で、すでにWebSocketのハンドシェイクは完了しています。ループの中で`ws.read()`して`ws.send()`するだけで、エコー動作が完成します。
|
`svr.WebSocket()`でWebSocket用のハンドラを登録します。ハンドラが呼ばれた時点で、すでにWebSocketのハンドシェイクは完了しています。ループの中で`ws.read()`して`ws.send()`するだけで、エコー動作が完成します。
|
||||||
|
|
||||||
`read()`の返り値は`ReadResult`列挙値で、次の3種類です。
|
`read()`の返り値は`ReadResult`列挙値で、次の4種類です。
|
||||||
|
|
||||||
- `ReadResult::Text`: テキストメッセージを受信
|
- `ReadResult::Text`: テキストメッセージを受信
|
||||||
- `ReadResult::Binary`: バイナリメッセージを受信
|
- `ReadResult::Binary`: バイナリメッセージを受信
|
||||||
- `ReadResult::Fail`: エラー、または接続が閉じた
|
- `ReadResult::Fail`: エラー、または接続が閉じた
|
||||||
|
- `ReadResult::Timeout`: 何も受信しないまま読み取りタイムアウトが経過した。接続は開いたまま。読み取りタイムアウトを設定したときだけ返る([W06. タイムアウトを設定する](../w06-websocket-timeouts)を参照)
|
||||||
|
|
||||||
## クライアント: エコーを叩く
|
## クライアント: エコーを叩く
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,6 @@ cli.set_websocket_max_missed_pongs(2); // 2回連続でPongが返ってこなけ
|
|||||||
|
|
||||||
`max_missed_pongs`のデフォルトは`0`で、これは「Pongが何回返ってこなくてもこの仕組みでは切断しない」という意味です。Ping自体は送られ続けますが、応答の有無はチェックされません。無応答ピアを検出したい場合は明示的に`1`以上を設定してください。
|
`max_missed_pongs`のデフォルトは`0`で、これは「Pongが何回返ってこなくてもこの仕組みでは切断しない」という意味です。Ping自体は送られ続けますが、応答の有無はチェックされません。無応答ピアを検出したい場合は明示的に`1`以上を設定してください。
|
||||||
|
|
||||||
ただし`0`のままでも最終的に接続が残り続けることはありません。`read()`を呼んでいる間は`CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND`(デフォルト**300秒 = 5分**)が保険として働き、フレームが一定時間来なければ`read()`が失敗します。つまり`max_missed_pongs`は「**もっと速く**無応答を検出したい」ときに使うオプションだと考えてください。
|
サーバ側は`0`のままでも接続が残り続けることはありません。ハンドラが`read()`を呼んでいる間は`CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND`(デフォルト**300秒 = 5分**)が保険として働きます。一方クライアント側にはこの保険がなく、読み取りタイムアウトを設定しない限り無期限に待つので、無応答ピアを検出する手段は`max_missed_pongs`だけです。どちらの側でも「**もっと速く**検出したい」ときに使うオプションでもあります。
|
||||||
|
|
||||||
> 接続が閉じたときの処理は[W03. 接続クローズをハンドリングする](../w03-websocket-close)を参照してください。
|
> 接続が閉じたときの処理は[W03. 接続クローズをハンドリングする](../w03-websocket-close)を参照してください。
|
||||||
@@ -42,6 +42,9 @@ switch (result) {
|
|||||||
case httplib::ws::ReadResult::Fail:
|
case httplib::ws::ReadResult::Fail:
|
||||||
// エラーまたは切断
|
// エラーまたは切断
|
||||||
break;
|
break;
|
||||||
|
case httplib::ws::ReadResult::Timeout:
|
||||||
|
// 読み取りタイムアウト。接続は開いたまま
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ status: "draft"
|
|||||||
| 種類 | API | デフォルト |
|
| 種類 | API | デフォルト |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 接続タイムアウト | `set_connection_timeout` | 300秒 |
|
| 接続タイムアウト | `set_connection_timeout` | 300秒 |
|
||||||
| 読み取りタイムアウト | `set_read_timeout` | 300秒(`CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND`) |
|
| 読み取りタイムアウト | `set_read_timeout` | なし。無期限に待つ(`CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND`) |
|
||||||
| 書き込みタイムアウト | `set_write_timeout` | 5秒 |
|
| 書き込みタイムアウト | `set_write_timeout` | 5秒 |
|
||||||
|
|
||||||
## 基本の使い方
|
## 基本の使い方
|
||||||
@@ -26,7 +26,7 @@ if (ws.connect()) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`connect()`を呼ぶ前に設定してください。
|
接続タイムアウトと書き込みタイムアウトは`connect()`を呼ぶ前に設定してください。読み取りタイムアウトはいつでも変更でき、接続済みの状態で設定した場合は次の`read()`から効きます。
|
||||||
|
|
||||||
## `std::chrono`で指定する
|
## `std::chrono`で指定する
|
||||||
|
|
||||||
@@ -40,9 +40,42 @@ ws.set_read_timeout(30s);
|
|||||||
ws.set_write_timeout(10s);
|
ws.set_write_timeout(10s);
|
||||||
```
|
```
|
||||||
|
|
||||||
## 読み取りタイムアウトの意味に注意
|
## 読み取りタイムアウトの意味
|
||||||
|
|
||||||
`set_read_timeout()`は「1回の`read()`呼び出し」に対するタイムアウトです。メッセージが届かないまま指定時間が経過すると`read()`が`ReadResult::Fail`を返します。通知の待受のように長時間メッセージが来ないことが正常な接続では、意図せず切断されないよう長めに設定するか、切断されたらアプリケーション側で再接続してください。
|
`set_read_timeout()`は「1回の`read()`呼び出し」に対するタイムアウトです。メッセージが届かないまま指定時間が経過すると`read()`は`ReadResult::Timeout`を返します。このとき**接続は開いたまま**で、1バイトも読み進めていないので、そのまま送信して読み直せます。接続が失われたことを意味する`ReadResult::Fail`とはここが違います。
|
||||||
|
|
||||||
|
1本の接続を1つのスレッドで双方向に扱えるのはこのためです。
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
ws.set_read_timeout(100ms);
|
||||||
|
std::string msg;
|
||||||
|
while (ws.is_open()) {
|
||||||
|
auto r = ws.read(msg);
|
||||||
|
if (r == httplib::ws::Timeout) {
|
||||||
|
flush_outgoing(ws); // 何も届いていない。溜まっている分を送る
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r == httplib::ws::Fail) { break; }
|
||||||
|
handle(msg);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
読み取りタイムアウトを設定しないと`read()`はメッセージが届くまで戻らないので、接続を持っているスレッドは送信に手が回りません。
|
||||||
|
|
||||||
|
`Timeout`について2点あります。
|
||||||
|
|
||||||
|
- `msg`は書き換えられません。値も0以外なので、読み取りタイムアウトを設定した状態で`while (ws.read(msg))`と書くと、**前回のメッセージ**が`msg`に残ったままループが回り続けます。
|
||||||
|
- 報告されるのはメッセージの境界だけです。分割されたメッセージの途中でタイムアウトした場合、そのメッセージは再開できないので`read()`は`Fail`を返します。
|
||||||
|
|
||||||
|
通知の待受のように長時間メッセージが来ないことが正常な接続では、読み取りタイムアウトを設定しないままにするか、`Timeout`を「まだ何も来ていない」印として扱ってループを続けてください。
|
||||||
|
|
||||||
|
## サーバ側
|
||||||
|
|
||||||
|
ハンドラが受け取る`ws::WebSocket`にも`set_read_timeout()`があります。ハンドラが`read()`で止まったままにならないので、上と同じ書き方で複数の接続の間をメッセージが中継できます。
|
||||||
|
|
||||||
|
サーバ側のデフォルトは「無期限」ではなく300秒(`CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND`)です。WebSocketのハンドラは接続が続く限りワーカーを1つ占有するので、無言になったピアからワーカーを回収する保険として働きます。
|
||||||
|
|
||||||
> Ping/Pongによる無応答ピア検出は別の仕組みです。詳しくは[W02. ハートビートを設定する](../w02-websocket-ping)を参照してください。
|
> Ping/Pongによる無応答ピア検出は別の仕組みです。詳しくは[W02. ハートビートを設定する](../w02-websocket-ping)を参照してください。
|
||||||
|
|
||||||
|
|||||||
@@ -215,8 +215,33 @@
|
|||||||
#define CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH 16777216
|
#define CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH 16777216
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND
|
// One macro used to set the read timeout for both sides. They want different
|
||||||
#define CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND 300
|
// defaults: a client's read timeout is the caller's own tool (it waits forever
|
||||||
|
// until asked not to), while a server keeps a ceiling that reclaims a worker
|
||||||
|
// from a peer that has gone quiet. The old name still works and sets both.
|
||||||
|
#ifdef CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND
|
||||||
|
#pragma message( \
|
||||||
|
"CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND is deprecated; define " \
|
||||||
|
"CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND and/or " \
|
||||||
|
"CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND instead")
|
||||||
|
#ifndef CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND
|
||||||
|
#define CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND \
|
||||||
|
CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND
|
||||||
|
#endif
|
||||||
|
#ifndef CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND
|
||||||
|
#define CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND \
|
||||||
|
CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// 0 waits forever. A read timeout is how a caller gets control back to send on
|
||||||
|
// the same connection; it is not a liveness check (that is ping/pong).
|
||||||
|
#ifndef CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND
|
||||||
|
#define CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND
|
||||||
|
#define CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND 300
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND
|
#ifndef CPPHTTPLIB_WEBSOCKET_CLOSE_TIMEOUT_SECOND
|
||||||
@@ -4351,7 +4376,11 @@ enum class CloseStatus : uint16_t {
|
|||||||
InternalError = 1011,
|
InternalError = 1011,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ReadResult : int { Fail = 0, Text = 1, Binary = 2 };
|
// Timeout is returned only when a read timeout was set and it elapsed before
|
||||||
|
// any byte of a frame arrived: nothing was consumed and the connection is
|
||||||
|
// still open, so the caller can send on it and read again. `msg` is left
|
||||||
|
// untouched, so a `while (ws.read(msg))` loop must not treat it as a message.
|
||||||
|
enum ReadResult : int { Fail = 0, Text = 1, Binary = 2, Timeout = 3 };
|
||||||
|
|
||||||
// Result of WebSocketClient::connect(). Truthy only when the WebSocket
|
// Result of WebSocketClient::connect(). Truthy only when the WebSocket
|
||||||
// upgrade handshake fully succeeded. On failure error() identifies the
|
// upgrade handshake fully succeeded. On failure error() identifies the
|
||||||
@@ -4411,6 +4440,13 @@ public:
|
|||||||
const Request &request() const;
|
const Request &request() const;
|
||||||
bool is_open() const;
|
bool is_open() const;
|
||||||
|
|
||||||
|
// Bound how long read() waits before returning Timeout. 0 waits forever.
|
||||||
|
// A server handler owns its connection's timeout this way; a client sets it
|
||||||
|
// through WebSocketClient. Safe to call while another thread is in read().
|
||||||
|
void set_read_timeout(time_t sec, time_t usec = 0);
|
||||||
|
template <class Rep, class Period>
|
||||||
|
void set_read_timeout(const std::chrono::duration<Rep, Period> &duration);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class httplib::Server;
|
friend class httplib::Server;
|
||||||
friend class WebSocketClient;
|
friend class WebSocketClient;
|
||||||
@@ -4533,7 +4569,7 @@ private:
|
|||||||
bool is_valid_ = false;
|
bool is_valid_ = false;
|
||||||
socket_t sock_ = INVALID_SOCKET;
|
socket_t sock_ = INVALID_SOCKET;
|
||||||
std::unique_ptr<WebSocket> ws_;
|
std::unique_ptr<WebSocket> ws_;
|
||||||
time_t read_timeout_sec_ = CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND;
|
time_t read_timeout_sec_ = CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND;
|
||||||
time_t read_timeout_usec_ = 0;
|
time_t read_timeout_usec_ = 0;
|
||||||
time_t write_timeout_sec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND;
|
time_t write_timeout_sec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_SECOND;
|
||||||
time_t write_timeout_usec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND;
|
time_t write_timeout_usec_ = CPPHTTPLIB_CLIENT_WRITE_TIMEOUT_USECOND;
|
||||||
@@ -4592,8 +4628,14 @@ namespace impl {
|
|||||||
|
|
||||||
bool is_valid_utf8(const std::string &s);
|
bool is_valid_utf8(const std::string &s);
|
||||||
|
|
||||||
bool read_websocket_frame(Stream &strm, Opcode &opcode, std::string &payload,
|
// Three states, because a failure that consumed bytes and one that consumed
|
||||||
bool &fin, bool expect_masked, size_t max_len);
|
// none are not the same thing: the first has left the stream in the middle of
|
||||||
|
// a frame and the connection cannot be reused, the second can just be retried.
|
||||||
|
enum class FrameRead { Ok, Fail, Timeout };
|
||||||
|
|
||||||
|
FrameRead read_websocket_frame(Stream &strm, Opcode &opcode,
|
||||||
|
std::string &payload, bool &fin,
|
||||||
|
bool expect_masked, size_t max_len);
|
||||||
|
|
||||||
} // namespace impl
|
} // namespace impl
|
||||||
|
|
||||||
@@ -5512,17 +5554,42 @@ inline bool write_websocket_frame(Stream &strm, ws::Opcode opcode,
|
|||||||
namespace ws {
|
namespace ws {
|
||||||
namespace impl {
|
namespace impl {
|
||||||
|
|
||||||
inline bool read_websocket_frame(Stream &strm, Opcode &opcode,
|
// Read exactly `size` bytes. Stream::read may return less than asked for -- it
|
||||||
std::string &payload, bool &fin,
|
// hands back whatever its buffer already holds -- so every multi-byte field has
|
||||||
bool expect_masked, size_t max_len) {
|
// to loop. Reading a 2-byte header with a single read() fails whenever the
|
||||||
// Read first 2 bytes
|
// header straddles the read buffer's boundary.
|
||||||
|
//
|
||||||
|
// Timeout is reported only when nothing at all was consumed. Once a byte has
|
||||||
|
// been taken the stream sits mid-field and cannot be resumed, so a timeout
|
||||||
|
// there is a failure like any other. (When read() fails it always records why,
|
||||||
|
// so the error belongs to this call and not to an earlier one.)
|
||||||
|
inline FrameRead read_exact(Stream &strm, void *buf, size_t size) {
|
||||||
|
auto p = static_cast<char *>(buf);
|
||||||
|
size_t total = 0;
|
||||||
|
while (total < size) {
|
||||||
|
auto n = strm.read(p + total, size - total);
|
||||||
|
if (n <= 0) {
|
||||||
|
auto timed_out = total == 0 && strm.get_error() == Error::Timeout;
|
||||||
|
return timed_out ? FrameRead::Timeout : FrameRead::Fail;
|
||||||
|
}
|
||||||
|
total += static_cast<size_t>(n);
|
||||||
|
}
|
||||||
|
return FrameRead::Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline FrameRead read_websocket_frame(Stream &strm, Opcode &opcode,
|
||||||
|
std::string &payload, bool &fin,
|
||||||
|
bool expect_masked, size_t max_len) {
|
||||||
|
// Read first 2 bytes. This is the only read that may report a timeout: it
|
||||||
|
// sits on a frame boundary, where nothing has been consumed yet.
|
||||||
uint8_t header[2];
|
uint8_t header[2];
|
||||||
if (strm.read(reinterpret_cast<char *>(header), 2) != 2) { return false; }
|
FrameRead first = read_exact(strm, header, 2);
|
||||||
|
if (first != FrameRead::Ok) { return first; }
|
||||||
|
|
||||||
fin = (header[0] & 0x80) != 0;
|
fin = (header[0] & 0x80) != 0;
|
||||||
|
|
||||||
// RSV1, RSV2, RSV3 must be 0 when no extension is negotiated
|
// RSV1, RSV2, RSV3 must be 0 when no extension is negotiated
|
||||||
if (header[0] & 0x70) { return false; }
|
if (header[0] & 0x70) { return FrameRead::Fail; }
|
||||||
|
|
||||||
opcode = static_cast<Opcode>(header[0] & 0x0F);
|
opcode = static_cast<Opcode>(header[0] & 0x0F);
|
||||||
bool masked = (header[1] & 0x80) != 0;
|
bool masked = (header[1] & 0x80) != 0;
|
||||||
@@ -5532,46 +5599,44 @@ inline bool read_websocket_frame(Stream &strm, Opcode &opcode,
|
|||||||
// MUST have a payload length of 125 bytes or less
|
// MUST have a payload length of 125 bytes or less
|
||||||
bool is_control = (static_cast<uint8_t>(opcode) & 0x08) != 0;
|
bool is_control = (static_cast<uint8_t>(opcode) & 0x08) != 0;
|
||||||
if (is_control) {
|
if (is_control) {
|
||||||
if (!fin) { return false; }
|
if (!fin) { return FrameRead::Fail; }
|
||||||
if (payload_len > 125) { return false; }
|
if (payload_len > 125) { return FrameRead::Fail; }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (masked != expect_masked) { return false; }
|
if (masked != expect_masked) { return FrameRead::Fail; }
|
||||||
|
|
||||||
// Extended payload length
|
// Extended payload length
|
||||||
if (payload_len == 126) {
|
if (payload_len == 126) {
|
||||||
uint8_t ext[2];
|
uint8_t ext[2];
|
||||||
if (strm.read(reinterpret_cast<char *>(ext), 2) != 2) { return false; }
|
if (read_exact(strm, ext, 2) != FrameRead::Ok) { return FrameRead::Fail; }
|
||||||
payload_len = (static_cast<uint64_t>(ext[0]) << 8) | ext[1];
|
payload_len = (static_cast<uint64_t>(ext[0]) << 8) | ext[1];
|
||||||
} else if (payload_len == 127) {
|
} else if (payload_len == 127) {
|
||||||
uint8_t ext[8];
|
uint8_t ext[8];
|
||||||
if (strm.read(reinterpret_cast<char *>(ext), 8) != 8) { return false; }
|
if (read_exact(strm, ext, 8) != FrameRead::Ok) { return FrameRead::Fail; }
|
||||||
// RFC 6455 Section 5.2: the most significant bit MUST be 0
|
// RFC 6455 Section 5.2: the most significant bit MUST be 0
|
||||||
if (ext[0] & 0x80) { return false; }
|
if (ext[0] & 0x80) { return FrameRead::Fail; }
|
||||||
payload_len = 0;
|
payload_len = 0;
|
||||||
for (int i = 0; i < 8; i++) {
|
for (int i = 0; i < 8; i++) {
|
||||||
payload_len = (payload_len << 8) | ext[i];
|
payload_len = (payload_len << 8) | ext[i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload_len > max_len) { return false; }
|
if (payload_len > max_len) { return FrameRead::Fail; }
|
||||||
|
|
||||||
// Read mask key if present
|
// Read mask key if present
|
||||||
uint8_t mask_key[4] = {0};
|
uint8_t mask_key[4] = {0};
|
||||||
if (masked) {
|
if (masked) {
|
||||||
if (strm.read(reinterpret_cast<char *>(mask_key), 4) != 4) { return false; }
|
if (read_exact(strm, mask_key, 4) != FrameRead::Ok) {
|
||||||
|
return FrameRead::Fail;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read payload
|
// Read payload
|
||||||
payload.resize(static_cast<size_t>(payload_len));
|
payload.resize(static_cast<size_t>(payload_len));
|
||||||
if (payload_len > 0) {
|
if (payload_len > 0 &&
|
||||||
size_t total_read = 0;
|
read_exact(strm, &payload[0], static_cast<size_t>(payload_len)) !=
|
||||||
while (total_read < payload_len) {
|
FrameRead::Ok) {
|
||||||
auto n = strm.read(&payload[total_read],
|
return FrameRead::Fail;
|
||||||
static_cast<size_t>(payload_len - total_read));
|
|
||||||
if (n <= 0) { return false; }
|
|
||||||
total_read += static_cast<size_t>(n);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unmask if needed
|
// Unmask if needed
|
||||||
@@ -5581,7 +5646,7 @@ inline bool read_websocket_frame(Stream &strm, Opcode &opcode,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return FrameRead::Ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace impl
|
} // namespace impl
|
||||||
@@ -6328,7 +6393,10 @@ inline ssize_t select_impl(socket_t sock, short events, time_t sec,
|
|||||||
pfd.events = events;
|
pfd.events = events;
|
||||||
pfd.revents = 0;
|
pfd.revents = 0;
|
||||||
|
|
||||||
auto timeout = static_cast<int>(sec * 1000 + usec / 1000);
|
// A negative timeout waits forever, poll's own convention. 0 keeps meaning
|
||||||
|
// "return immediately", which callers here rely on to probe a socket.
|
||||||
|
auto timeout =
|
||||||
|
sec < 0 ? -1 : static_cast<int>(sec * 1000 + usec / 1000);
|
||||||
|
|
||||||
return handle_EINTR([&]() { return poll_wrapper(&pfd, 1, timeout); });
|
return handle_EINTR([&]() { return poll_wrapper(&pfd, 1, timeout); });
|
||||||
}
|
}
|
||||||
@@ -6410,8 +6478,11 @@ private:
|
|||||||
bool ensure_readable();
|
bool ensure_readable();
|
||||||
|
|
||||||
socket_t sock_;
|
socket_t sock_;
|
||||||
time_t read_timeout_sec_;
|
// Atomic because ws::WebSocket::set_read_timeout() reaches this from another
|
||||||
time_t read_timeout_usec_;
|
// thread while a read is in flight -- that is the point of it, for a caller
|
||||||
|
// holding one connection and wanting control back to send on it.
|
||||||
|
std::atomic<time_t> read_timeout_sec_;
|
||||||
|
std::atomic<time_t> read_timeout_usec_;
|
||||||
time_t write_timeout_sec_;
|
time_t write_timeout_sec_;
|
||||||
time_t write_timeout_usec_;
|
time_t write_timeout_usec_;
|
||||||
time_t max_timeout_msec_;
|
time_t max_timeout_msec_;
|
||||||
@@ -12604,12 +12675,19 @@ inline ssize_t WebSocketSSLStream::read(char *ptr, size_t size) {
|
|||||||
needs_readable || (err.code == tls::ErrorCode::SyscallError &&
|
needs_readable || (err.code == tls::ErrorCode::SyscallError &&
|
||||||
WSAGetLastError() == WSAETIMEDOUT);
|
WSAGetLastError() == WSAETIMEDOUT);
|
||||||
#endif
|
#endif
|
||||||
if (!needs_readable && err.code != tls::ErrorCode::WantWrite) { return -1; }
|
if (!needs_readable && err.code != tls::ErrorCode::WantWrite) {
|
||||||
|
error_ = Error::Read;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
if (!(needs_readable ? wait_readable() : wait_writable())) {
|
if (!(needs_readable ? wait_readable() : wait_writable())) {
|
||||||
error_ = Error::Timeout;
|
error_ = Error::Timeout;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Out of retries. Recording a reason matters: a caller that reads get_error()
|
||||||
|
// to tell a timeout from a close would otherwise see whatever the previous
|
||||||
|
// failure left behind (error_ is never cleared on success).
|
||||||
|
error_ = Error::Read;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14352,7 +14430,7 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|||||||
auto ws_strm =
|
auto ws_strm =
|
||||||
std::unique_ptr<Stream>(new detail::WebSocketSSLStream(
|
std::unique_ptr<Stream>(new detail::WebSocketSSLStream(
|
||||||
strm.socket(), const_cast<tls::session_t>(req.ssl),
|
strm.socket(), const_cast<tls::session_t>(req.ssl),
|
||||||
CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND, 0,
|
CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND, 0,
|
||||||
write_timeout_sec_, write_timeout_usec_));
|
write_timeout_sec_, write_timeout_usec_));
|
||||||
ws::WebSocket ws(std::move(ws_strm), req, true,
|
ws::WebSocket ws(std::move(ws_strm), req, true,
|
||||||
websocket_ping_interval_sec_,
|
websocket_ping_interval_sec_,
|
||||||
@@ -14362,7 +14440,8 @@ Server::process_request(Stream &strm, const std::string &remote_addr,
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
// 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_SERVER_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_);
|
websocket_max_missed_pongs_);
|
||||||
entry.handler(req, ws);
|
entry.handler(req, ws);
|
||||||
@@ -22180,8 +22259,13 @@ inline ReadResult WebSocket::read(std::string &msg) {
|
|||||||
std::string payload;
|
std::string payload;
|
||||||
bool fin;
|
bool fin;
|
||||||
|
|
||||||
if (!impl::read_websocket_frame(strm_, opcode, payload, fin, is_server_,
|
impl::FrameRead r =
|
||||||
CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH)) {
|
impl::read_websocket_frame(strm_, opcode, payload, fin, is_server_,
|
||||||
|
CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH);
|
||||||
|
// A timeout landed on a frame boundary: the connection is untouched and
|
||||||
|
// still usable, so hand control back without closing it.
|
||||||
|
if (r == impl::FrameRead::Timeout) { return Timeout; }
|
||||||
|
if (r != impl::FrameRead::Ok) {
|
||||||
closed_ = true;
|
closed_ = true;
|
||||||
return Fail;
|
return Fail;
|
||||||
}
|
}
|
||||||
@@ -22218,9 +22302,14 @@ inline ReadResult WebSocket::read(std::string &msg) {
|
|||||||
Opcode cont_opcode;
|
Opcode cont_opcode;
|
||||||
std::string cont_payload;
|
std::string cont_payload;
|
||||||
bool cont_fin;
|
bool cont_fin;
|
||||||
if (!impl::read_websocket_frame(
|
// A timeout is not reportable here: half of a fragmented message is
|
||||||
|
// already in `msg` and read() has no way to resume it, so it is a
|
||||||
|
// failure like any other. Timeouts are only ever seen on a message
|
||||||
|
// boundary.
|
||||||
|
if (impl::read_websocket_frame(
|
||||||
strm_, cont_opcode, cont_payload, cont_fin, is_server_,
|
strm_, cont_opcode, cont_payload, cont_fin, is_server_,
|
||||||
CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH)) {
|
CPPHTTPLIB_WEBSOCKET_MAX_PAYLOAD_LENGTH) !=
|
||||||
|
impl::FrameRead::Ok) {
|
||||||
closed_ = true;
|
closed_ = true;
|
||||||
return Fail;
|
return Fail;
|
||||||
}
|
}
|
||||||
@@ -22314,7 +22403,8 @@ inline void WebSocket::close(CloseStatus status, const std::string &reason) {
|
|||||||
Opcode op;
|
Opcode op;
|
||||||
std::string resp;
|
std::string resp;
|
||||||
bool fin;
|
bool fin;
|
||||||
while (impl::read_websocket_frame(strm_, op, resp, fin, is_server_, 125)) {
|
while (impl::read_websocket_frame(strm_, op, resp, fin, is_server_, 125) ==
|
||||||
|
impl::FrameRead::Ok) {
|
||||||
if (op == Opcode::Close) { break; }
|
if (op == Opcode::Close) { break; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22359,6 +22449,21 @@ inline const Request &WebSocket::request() const { return req_; }
|
|||||||
|
|
||||||
inline bool WebSocket::is_open() const { return !closed_; }
|
inline bool WebSocket::is_open() const { return !closed_; }
|
||||||
|
|
||||||
|
inline void WebSocket::set_read_timeout(time_t sec, time_t usec) {
|
||||||
|
// 0 waits forever here, as it does for SO_RCVTIMEO. The stream waits with
|
||||||
|
// poll(), where 0 would instead mean "return immediately", so hand it the
|
||||||
|
// negative poll uses for an unbounded wait.
|
||||||
|
if (sec == 0 && usec == 0) { sec = -1; }
|
||||||
|
strm_.set_read_timeout(sec, usec);
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Rep, class Period>
|
||||||
|
inline void WebSocket::set_read_timeout(
|
||||||
|
const std::chrono::duration<Rep, Period> &duration) {
|
||||||
|
detail::duration_to_sec_and_usec(
|
||||||
|
duration, [&](time_t sec, time_t usec) { set_read_timeout(sec, usec); });
|
||||||
|
}
|
||||||
|
|
||||||
// WebSocketClient implementation
|
// WebSocketClient implementation
|
||||||
inline WebSocketClient::WebSocketClient(
|
inline WebSocketClient::WebSocketClient(
|
||||||
const std::string &scheme_host_port_path, const Headers &headers)
|
const std::string &scheme_host_port_path, const Headers &headers)
|
||||||
@@ -22461,6 +22566,16 @@ inline void WebSocketClient::shutdown_and_close() {
|
|||||||
inline bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm,
|
inline bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm,
|
||||||
Error &error, int &ssl_error,
|
Error &error, int &ssl_error,
|
||||||
uint64_t &ssl_backend_error) {
|
uint64_t &ssl_backend_error) {
|
||||||
|
// A read timeout of 0 means "wait forever", the way SO_RCVTIMEO reads it.
|
||||||
|
// The streams wait with poll(), where 0 instead means "return immediately",
|
||||||
|
// so they are given the negative poll uses for an unbounded wait.
|
||||||
|
auto unbounded = read_timeout_sec_ == 0 && read_timeout_usec_ == 0;
|
||||||
|
time_t strm_read_sec = unbounded ? -1 : read_timeout_sec_;
|
||||||
|
time_t strm_read_usec = unbounded ? 0 : read_timeout_usec_;
|
||||||
|
// The handshake belongs to establishing the connection, so an unset read
|
||||||
|
// timeout leaves it bounded by the connection timeout instead of forever.
|
||||||
|
time_t hs_sec = unbounded ? connection_timeout_sec_ : read_timeout_sec_;
|
||||||
|
time_t hs_usec = unbounded ? connection_timeout_usec_ : read_timeout_usec_;
|
||||||
#ifdef CPPHTTPLIB_SSL_ENABLED
|
#ifdef CPPHTTPLIB_SSL_ENABLED
|
||||||
if (is_ssl_) {
|
if (is_ssl_) {
|
||||||
// A plain flag rather than SSLClient::load_certs()'s call_once: connect()
|
// A plain flag rather than SSLClient::load_certs()'s call_once: connect()
|
||||||
@@ -22480,8 +22595,8 @@ inline bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm,
|
|||||||
detail::ClientTlsSessionError tls_error;
|
detail::ClientTlsSessionError tls_error;
|
||||||
if (!detail::setup_client_tls_session(host_, tls_ctx_, tls_session_, sock_,
|
if (!detail::setup_client_tls_session(host_, tls_ctx_, tls_session_, sock_,
|
||||||
server_certificate_verification_,
|
server_certificate_verification_,
|
||||||
read_timeout_sec_, read_timeout_usec_,
|
hs_sec, hs_usec, &tls_error,
|
||||||
&tls_error, options)) {
|
options)) {
|
||||||
error = tls_error.error;
|
error = tls_error.error;
|
||||||
ssl_error = tls_error.ssl_error;
|
ssl_error = tls_error.ssl_error;
|
||||||
ssl_backend_error = tls_error.backend_error;
|
ssl_backend_error = tls_error.backend_error;
|
||||||
@@ -22489,17 +22604,19 @@ inline bool WebSocketClient::create_stream(std::unique_ptr<Stream> &strm,
|
|||||||
}
|
}
|
||||||
|
|
||||||
strm = std::unique_ptr<Stream>(new detail::WebSocketSSLStream(
|
strm = std::unique_ptr<Stream>(new detail::WebSocketSSLStream(
|
||||||
sock_, tls_session_, read_timeout_sec_, read_timeout_usec_,
|
sock_, tls_session_, strm_read_sec, strm_read_usec, write_timeout_sec_,
|
||||||
write_timeout_sec_, write_timeout_usec_));
|
write_timeout_usec_));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
(void)error;
|
(void)error;
|
||||||
(void)ssl_error;
|
(void)ssl_error;
|
||||||
(void)ssl_backend_error;
|
(void)ssl_backend_error;
|
||||||
|
(void)hs_sec;
|
||||||
|
(void)hs_usec;
|
||||||
#endif
|
#endif
|
||||||
strm = std::unique_ptr<Stream>(
|
strm = std::unique_ptr<Stream>(
|
||||||
new detail::SocketStream(sock_, read_timeout_sec_, read_timeout_usec_,
|
new detail::SocketStream(sock_, strm_read_sec, strm_read_usec,
|
||||||
write_timeout_sec_, write_timeout_usec_));
|
write_timeout_sec_, write_timeout_usec_));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -22601,6 +22718,9 @@ inline const std::string &WebSocketClient::subprotocol() const {
|
|||||||
inline void WebSocketClient::set_read_timeout(time_t sec, time_t usec) {
|
inline void WebSocketClient::set_read_timeout(time_t sec, time_t usec) {
|
||||||
read_timeout_sec_ = sec;
|
read_timeout_sec_ = sec;
|
||||||
read_timeout_usec_ = usec;
|
read_timeout_usec_ = usec;
|
||||||
|
// The members above only seed the next connect(); read() consults the
|
||||||
|
// stream, so an already-open connection has to be told directly.
|
||||||
|
if (ws_) { ws_->set_read_timeout(sec, usec); }
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void WebSocketClient::set_write_timeout(time_t sec, time_t usec) {
|
inline void WebSocketClient::set_write_timeout(time_t sec, time_t usec) {
|
||||||
|
|||||||
+175
-26
@@ -22341,8 +22341,9 @@ TEST(WebSocketTest, RSVBitsMustBeZero) {
|
|||||||
ws::Opcode opcode;
|
ws::Opcode opcode;
|
||||||
std::string payload;
|
std::string payload;
|
||||||
bool fin;
|
bool fin;
|
||||||
EXPECT_FALSE(ws::impl::read_websocket_frame(strm, opcode, payload, fin,
|
EXPECT_EQ(ws::impl::read_websocket_frame(strm, opcode, payload, fin, false,
|
||||||
false, 1024));
|
1024),
|
||||||
|
ws::impl::FrameRead::Fail);
|
||||||
}
|
}
|
||||||
|
|
||||||
// RSV2 set (0x20)
|
// RSV2 set (0x20)
|
||||||
@@ -22352,8 +22353,9 @@ TEST(WebSocketTest, RSVBitsMustBeZero) {
|
|||||||
ws::Opcode opcode;
|
ws::Opcode opcode;
|
||||||
std::string payload;
|
std::string payload;
|
||||||
bool fin;
|
bool fin;
|
||||||
EXPECT_FALSE(ws::impl::read_websocket_frame(strm, opcode, payload, fin,
|
EXPECT_EQ(ws::impl::read_websocket_frame(strm, opcode, payload, fin, false,
|
||||||
false, 1024));
|
1024),
|
||||||
|
ws::impl::FrameRead::Fail);
|
||||||
}
|
}
|
||||||
|
|
||||||
// RSV3 set (0x10)
|
// RSV3 set (0x10)
|
||||||
@@ -22363,8 +22365,9 @@ TEST(WebSocketTest, RSVBitsMustBeZero) {
|
|||||||
ws::Opcode opcode;
|
ws::Opcode opcode;
|
||||||
std::string payload;
|
std::string payload;
|
||||||
bool fin;
|
bool fin;
|
||||||
EXPECT_FALSE(ws::impl::read_websocket_frame(strm, opcode, payload, fin,
|
EXPECT_EQ(ws::impl::read_websocket_frame(strm, opcode, payload, fin, false,
|
||||||
false, 1024));
|
1024),
|
||||||
|
ws::impl::FrameRead::Fail);
|
||||||
}
|
}
|
||||||
|
|
||||||
// No RSV bits set - should succeed
|
// No RSV bits set - should succeed
|
||||||
@@ -22374,8 +22377,9 @@ TEST(WebSocketTest, RSVBitsMustBeZero) {
|
|||||||
ws::Opcode opcode;
|
ws::Opcode opcode;
|
||||||
std::string payload;
|
std::string payload;
|
||||||
bool fin;
|
bool fin;
|
||||||
EXPECT_TRUE(ws::impl::read_websocket_frame(strm, opcode, payload, fin,
|
EXPECT_EQ(ws::impl::read_websocket_frame(strm, opcode, payload, fin, false,
|
||||||
false, 1024));
|
1024),
|
||||||
|
ws::impl::FrameRead::Ok);
|
||||||
EXPECT_EQ(ws::Opcode::Text, opcode);
|
EXPECT_EQ(ws::Opcode::Text, opcode);
|
||||||
EXPECT_EQ("Hello", payload);
|
EXPECT_EQ("Hello", payload);
|
||||||
EXPECT_TRUE(fin);
|
EXPECT_TRUE(fin);
|
||||||
@@ -22396,8 +22400,9 @@ TEST(WebSocketTest, ControlFrameValidation) {
|
|||||||
ws::Opcode opcode;
|
ws::Opcode opcode;
|
||||||
std::string payload;
|
std::string payload;
|
||||||
bool fin;
|
bool fin;
|
||||||
EXPECT_FALSE(ws::impl::read_websocket_frame(strm, opcode, payload, fin,
|
EXPECT_EQ(ws::impl::read_websocket_frame(strm, opcode, payload, fin, false,
|
||||||
false, 1024));
|
1024),
|
||||||
|
ws::impl::FrameRead::Fail);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close with FIN=0 - must be rejected
|
// Close with FIN=0 - must be rejected
|
||||||
@@ -22410,8 +22415,9 @@ TEST(WebSocketTest, ControlFrameValidation) {
|
|||||||
ws::Opcode opcode;
|
ws::Opcode opcode;
|
||||||
std::string payload;
|
std::string payload;
|
||||||
bool fin;
|
bool fin;
|
||||||
EXPECT_FALSE(ws::impl::read_websocket_frame(strm, opcode, payload, fin,
|
EXPECT_EQ(ws::impl::read_websocket_frame(strm, opcode, payload, fin, false,
|
||||||
false, 1024));
|
1024),
|
||||||
|
ws::impl::FrameRead::Fail);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ping with payload_len=126 (extended length) - must be rejected
|
// Ping with payload_len=126 (extended length) - must be rejected
|
||||||
@@ -22427,8 +22433,9 @@ TEST(WebSocketTest, ControlFrameValidation) {
|
|||||||
ws::Opcode opcode;
|
ws::Opcode opcode;
|
||||||
std::string payload;
|
std::string payload;
|
||||||
bool fin;
|
bool fin;
|
||||||
EXPECT_FALSE(ws::impl::read_websocket_frame(strm, opcode, payload, fin,
|
EXPECT_EQ(ws::impl::read_websocket_frame(strm, opcode, payload, fin, false,
|
||||||
false, 1024));
|
1024),
|
||||||
|
ws::impl::FrameRead::Fail);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ping with FIN=1 and payload_len=125 - should succeed
|
// Ping with FIN=1 and payload_len=125 - should succeed
|
||||||
@@ -22442,8 +22449,9 @@ TEST(WebSocketTest, ControlFrameValidation) {
|
|||||||
ws::Opcode opcode;
|
ws::Opcode opcode;
|
||||||
std::string payload;
|
std::string payload;
|
||||||
bool fin;
|
bool fin;
|
||||||
EXPECT_TRUE(ws::impl::read_websocket_frame(strm, opcode, payload, fin,
|
EXPECT_EQ(ws::impl::read_websocket_frame(strm, opcode, payload, fin, false,
|
||||||
false, 1024));
|
1024),
|
||||||
|
ws::impl::FrameRead::Ok);
|
||||||
EXPECT_EQ(ws::Opcode::Ping, opcode);
|
EXPECT_EQ(ws::Opcode::Ping, opcode);
|
||||||
EXPECT_EQ(125u, payload.size());
|
EXPECT_EQ(125u, payload.size());
|
||||||
EXPECT_TRUE(fin);
|
EXPECT_TRUE(fin);
|
||||||
@@ -22466,8 +22474,9 @@ TEST(WebSocketTest, PayloadLength64BitMSBMustBeZero) {
|
|||||||
ws::Opcode opcode;
|
ws::Opcode opcode;
|
||||||
std::string payload;
|
std::string payload;
|
||||||
bool fin;
|
bool fin;
|
||||||
EXPECT_FALSE(ws::impl::read_websocket_frame(strm, opcode, payload, fin,
|
EXPECT_EQ(ws::impl::read_websocket_frame(strm, opcode, payload, fin, false,
|
||||||
false, 1024));
|
1024),
|
||||||
|
ws::impl::FrameRead::Fail);
|
||||||
}
|
}
|
||||||
|
|
||||||
// MSB clear - should pass length parsing (will be rejected by max_len,
|
// MSB clear - should pass length parsing (will be rejected by max_len,
|
||||||
@@ -22484,8 +22493,9 @@ TEST(WebSocketTest, PayloadLength64BitMSBMustBeZero) {
|
|||||||
ws::Opcode opcode;
|
ws::Opcode opcode;
|
||||||
std::string payload;
|
std::string payload;
|
||||||
bool fin;
|
bool fin;
|
||||||
EXPECT_TRUE(ws::impl::read_websocket_frame(strm, opcode, payload, fin,
|
EXPECT_EQ(ws::impl::read_websocket_frame(strm, opcode, payload, fin, false,
|
||||||
false, 1024));
|
1024),
|
||||||
|
ws::impl::FrameRead::Ok);
|
||||||
EXPECT_EQ(ws::Opcode::Text, opcode);
|
EXPECT_EQ(ws::Opcode::Text, opcode);
|
||||||
EXPECT_EQ("abc", payload);
|
EXPECT_EQ("abc", payload);
|
||||||
}
|
}
|
||||||
@@ -22970,10 +22980,46 @@ TEST_F(WebSocketIntegrationTest, ReadTimeout) {
|
|||||||
client.set_read_timeout(1, 0); // 1 second
|
client.set_read_timeout(1, 0); // 1 second
|
||||||
ASSERT_TRUE(client.connect());
|
ASSERT_TRUE(client.connect());
|
||||||
|
|
||||||
// Don't send anything — server echo handler waits for a message,
|
// Don't send anything — server echo handler waits for a message, so read()
|
||||||
// so read() should time out and return false.
|
// should time out. The connection survives it: nothing was consumed.
|
||||||
std::string msg;
|
std::string msg;
|
||||||
EXPECT_FALSE(client.read(msg));
|
EXPECT_EQ(client.read(msg), ws::Timeout);
|
||||||
|
EXPECT_TRUE(client.is_open());
|
||||||
|
|
||||||
|
// And it is still usable afterwards.
|
||||||
|
EXPECT_TRUE(client.send("after timeout"));
|
||||||
|
EXPECT_EQ(client.read(msg), ws::Text);
|
||||||
|
EXPECT_EQ(msg, "after timeout");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(WebSocketIntegrationTest, ReadTimeoutLeavesMessageUntouched) {
|
||||||
|
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) +
|
||||||
|
"/ws-echo");
|
||||||
|
client.set_read_timeout(1, 0);
|
||||||
|
ASSERT_TRUE(client.connect());
|
||||||
|
|
||||||
|
ASSERT_TRUE(client.send("first"));
|
||||||
|
std::string msg;
|
||||||
|
ASSERT_EQ(client.read(msg), ws::Text);
|
||||||
|
ASSERT_EQ(msg, "first");
|
||||||
|
|
||||||
|
// A timeout does not write to msg. This is why `while (ws.read(msg))` must
|
||||||
|
// not be used with a read timeout: it would reprocess the previous message.
|
||||||
|
EXPECT_EQ(client.read(msg), ws::Timeout);
|
||||||
|
EXPECT_EQ(msg, "first");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(WebSocketIntegrationTest, ReadTimeoutSetAfterConnect) {
|
||||||
|
ws::WebSocketClient client("ws://localhost:" + std::to_string(port_) +
|
||||||
|
"/ws-echo");
|
||||||
|
ASSERT_TRUE(client.connect());
|
||||||
|
|
||||||
|
// Setting it once the connection is open reaches the live stream, not just
|
||||||
|
// the seed for the next connect().
|
||||||
|
client.set_read_timeout(1, 0);
|
||||||
|
std::string msg;
|
||||||
|
EXPECT_EQ(client.read(msg), ws::Timeout);
|
||||||
|
EXPECT_TRUE(client.is_open());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(WebSocketIntegrationTest, MaxPayloadExceeded) {
|
TEST_F(WebSocketIntegrationTest, MaxPayloadExceeded) {
|
||||||
@@ -23162,12 +23208,13 @@ TEST_F(WebSocketIntegrationTest, ChronoTimeoutSetters) {
|
|||||||
|
|
||||||
auto start = std::chrono::steady_clock::now();
|
auto start = std::chrono::steady_clock::now();
|
||||||
std::string msg;
|
std::string msg;
|
||||||
EXPECT_EQ(client.read(msg), ws::ReadResult::Fail);
|
EXPECT_EQ(client.read(msg), ws::ReadResult::Timeout);
|
||||||
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
std::chrono::steady_clock::now() - start)
|
std::chrono::steady_clock::now() - start)
|
||||||
.count();
|
.count();
|
||||||
// Above 1s so that dropping the microseconds half of the split fails here,
|
// Above 1s so that dropping the microseconds half of the split fails here.
|
||||||
// and well under the 300s default so that ignoring the setter fails too.
|
// Ignoring the setter altogether would not reach this line at all: a client
|
||||||
|
// waits forever by default.
|
||||||
EXPECT_GE(elapsed, 1400);
|
EXPECT_GE(elapsed, 1400);
|
||||||
EXPECT_LT(elapsed, 30000);
|
EXPECT_LT(elapsed, 30000);
|
||||||
}
|
}
|
||||||
@@ -23214,6 +23261,108 @@ TEST(WebSocketPreRoutingTest, RejectWithoutAuth) {
|
|||||||
t.join();
|
t.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(WebSocketServerTimeoutTest, HandlerSendsWhileNothingArrives) {
|
||||||
|
Server svr;
|
||||||
|
|
||||||
|
svr.WebSocket("/ws", [](const Request &, ws::WebSocket &ws) {
|
||||||
|
// A read timeout is how a handler gets control back. Without it, a handler
|
||||||
|
// parked in read() can never write on the connection it is reading.
|
||||||
|
ws.set_read_timeout(std::chrono::milliseconds(100));
|
||||||
|
std::string msg;
|
||||||
|
while (ws.is_open()) {
|
||||||
|
auto r = ws.read(msg);
|
||||||
|
if (r == httplib::ws::Timeout) {
|
||||||
|
ws.send("tick");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (r == httplib::ws::Fail) { break; }
|
||||||
|
ws.send(msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
auto port = svr.bind_to_any_port("localhost");
|
||||||
|
std::thread t([&]() { svr.listen_after_bind(); });
|
||||||
|
svr.wait_until_ready();
|
||||||
|
|
||||||
|
ws::WebSocketClient client("ws://localhost:" + std::to_string(port) + "/ws");
|
||||||
|
client.set_read_timeout(10, 0); // fail rather than hang if no tick arrives
|
||||||
|
ASSERT_TRUE(client.connect());
|
||||||
|
|
||||||
|
// The client sends nothing, so anything it receives came out of the
|
||||||
|
// handler's timeout path.
|
||||||
|
std::string msg;
|
||||||
|
ASSERT_EQ(client.read(msg), ws::Text);
|
||||||
|
EXPECT_EQ("tick", msg);
|
||||||
|
|
||||||
|
client.close();
|
||||||
|
svr.stop();
|
||||||
|
t.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream::read may return fewer bytes than asked for. This is that contract at
|
||||||
|
// its worst -- one byte per call -- which is what a frame header straddling a
|
||||||
|
// read buffer's boundary looks like to the frame parser.
|
||||||
|
class WsByteAtATimeStream : public Stream {
|
||||||
|
public:
|
||||||
|
explicit WsByteAtATimeStream(std::string data) : data_(std::move(data)) {}
|
||||||
|
bool is_readable() const override { return true; }
|
||||||
|
bool wait_readable() const override { return true; }
|
||||||
|
bool wait_writable() const override { return true; }
|
||||||
|
ssize_t read(char *ptr, size_t size) override {
|
||||||
|
if (size == 0 || pos_ >= data_.size()) { return 0; }
|
||||||
|
*ptr = data_[pos_++];
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
ssize_t write(const char *, size_t size) override {
|
||||||
|
return static_cast<ssize_t>(size);
|
||||||
|
}
|
||||||
|
void get_remote_ip_and_port(std::string &ip, int &port) const override {
|
||||||
|
ip = "127.0.0.1";
|
||||||
|
port = 0;
|
||||||
|
}
|
||||||
|
void get_local_ip_and_port(std::string &ip, int &port) const override {
|
||||||
|
ip = "127.0.0.1";
|
||||||
|
port = 0;
|
||||||
|
}
|
||||||
|
socket_t socket() const override { return INVALID_SOCKET; }
|
||||||
|
time_t duration() const override { return 0; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string data_;
|
||||||
|
size_t pos_ = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST(WebSocketFrameTest, MultiByteFieldsSplitAcrossReads) {
|
||||||
|
// A 200-byte payload uses the 16-bit extended length, so the header, the
|
||||||
|
// length and the mask key are all multi-byte fields here. Each used to be
|
||||||
|
// read with a single read() call, which fails as soon as one is split.
|
||||||
|
std::string body(200, 'x');
|
||||||
|
const uint8_t mask[4] = {0x0a, 0x0b, 0x0c, 0x0d};
|
||||||
|
|
||||||
|
std::string frame;
|
||||||
|
frame += static_cast<char>(0x81); // FIN + Text
|
||||||
|
frame += static_cast<char>(0x80 | 126); // masked, 16-bit length follows
|
||||||
|
frame += static_cast<char>(body.size() >> 8);
|
||||||
|
frame += static_cast<char>(body.size() & 0xff);
|
||||||
|
for (size_t i = 0; i < 4; i++) {
|
||||||
|
frame += static_cast<char>(mask[i]);
|
||||||
|
}
|
||||||
|
for (size_t i = 0; i < body.size(); i++) {
|
||||||
|
frame += static_cast<char>(body[i] ^ mask[i % 4]);
|
||||||
|
}
|
||||||
|
|
||||||
|
WsByteAtATimeStream strm(frame);
|
||||||
|
ws::Opcode opcode;
|
||||||
|
std::string payload;
|
||||||
|
bool fin = false;
|
||||||
|
ASSERT_EQ(ws::impl::read_websocket_frame(strm, opcode, payload, fin,
|
||||||
|
/*expect_masked=*/true, 1024),
|
||||||
|
ws::impl::FrameRead::Ok);
|
||||||
|
EXPECT_TRUE(fin);
|
||||||
|
EXPECT_EQ(opcode, ws::Opcode::Text);
|
||||||
|
EXPECT_EQ(payload, body);
|
||||||
|
}
|
||||||
|
|
||||||
TEST(WebSocketTest, QueryStringInHandshake) {
|
TEST(WebSocketTest, QueryStringInHandshake) {
|
||||||
Server svr;
|
Server svr;
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
// without waiting 30 seconds.
|
// without waiting 30 seconds.
|
||||||
|
|
||||||
#define CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND 1
|
#define CPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECOND 1
|
||||||
#define CPPHTTPLIB_WEBSOCKET_READ_TIMEOUT_SECOND 3
|
#define CPPHTTPLIB_WEBSOCKET_CLIENT_READ_TIMEOUT_SECOND 3
|
||||||
|
#define CPPHTTPLIB_WEBSOCKET_SERVER_READ_TIMEOUT_SECOND 3
|
||||||
#include <httplib.h>
|
#include <httplib.h>
|
||||||
|
|
||||||
#include "gtest/gtest.h"
|
#include "gtest/gtest.h"
|
||||||
|
|||||||
Reference in new issue
Block a user