mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-08 07:54:42 +07: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:
@@ -36,6 +36,7 @@ The `read()` return value is a `ReadResult` enum:
|
||||
- `ReadResult::Text`: received a text message
|
||||
- `ReadResult::Binary`: received a binary message
|
||||
- `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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
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).
|
||||
|
||||
@@ -42,6 +42,9 @@ switch (result) {
|
||||
case httplib::ws::ReadResult::Fail:
|
||||
// error or closed
|
||||
break;
|
||||
case httplib::ws::ReadResult::Timeout:
|
||||
// read timeout elapsed; the connection is still open
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ status: "draft"
|
||||
| Kind | API | Default |
|
||||
| --- | --- | --- |
|
||||
| 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 |
|
||||
|
||||
## 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`
|
||||
|
||||
@@ -40,9 +40,42 @@ ws.set_read_timeout(30s);
|
||||
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.
|
||||
|
||||
|
||||
@@ -31,11 +31,12 @@ int main() {
|
||||
|
||||
`svr.WebSocket()`でWebSocket用のハンドラを登録します。ハンドラが呼ばれた時点で、すでにWebSocketのハンドシェイクは完了しています。ループの中で`ws.read()`して`ws.send()`するだけで、エコー動作が完成します。
|
||||
|
||||
`read()`の返り値は`ReadResult`列挙値で、次の3種類です。
|
||||
`read()`の返り値は`ReadResult`列挙値で、次の4種類です。
|
||||
|
||||
- `ReadResult::Text`: テキストメッセージを受信
|
||||
- `ReadResult::Binary`: バイナリメッセージを受信
|
||||
- `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`以上を設定してください。
|
||||
|
||||
ただし`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)を参照してください。
|
||||
|
||||
@@ -42,6 +42,9 @@ switch (result) {
|
||||
case httplib::ws::ReadResult::Fail:
|
||||
// エラーまたは切断
|
||||
break;
|
||||
case httplib::ws::ReadResult::Timeout:
|
||||
// 読み取りタイムアウト。接続は開いたまま
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ status: "draft"
|
||||
| 種類 | API | デフォルト |
|
||||
| --- | --- | --- |
|
||||
| 接続タイムアウト | `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秒 |
|
||||
|
||||
## 基本の使い方
|
||||
@@ -26,7 +26,7 @@ if (ws.connect()) {
|
||||
}
|
||||
```
|
||||
|
||||
`connect()`を呼ぶ前に設定してください。
|
||||
接続タイムアウトと書き込みタイムアウトは`connect()`を呼ぶ前に設定してください。読み取りタイムアウトはいつでも変更でき、接続済みの状態で設定した場合は次の`read()`から効きます。
|
||||
|
||||
## `std::chrono`で指定する
|
||||
|
||||
@@ -40,9 +40,42 @@ ws.set_read_timeout(30s);
|
||||
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)を参照してください。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user