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.
4.6 KiB
title, order, status
| title | order | status |
|---|---|---|
| W02. ハートビートを設定する | 53 | draft |
WebSocket接続は長時間つなぎっぱなしになるので、プロキシやロードバランサが「アイドルだから」と勝手に切ってしまうことがあります。これを防ぐために、定期的にPingフレームを送って接続を生かしておく仕組みがあります。cpp-httplibでは、指定した間隔で自動的にPingを送ってくれます。
サーバー側の設定
svr.set_websocket_ping_interval(30); // 30秒ごとにPing
svr.WebSocket("/chat", [](const auto &req, auto &ws) {
// ...
});
set_websocket_ping_interval()に秒数を渡すだけです。このサーバーが受け入れるすべてのWebSocket接続に対して、指定した間隔でPingが送られます。
std::chronoの期間を受け取るオーバーロードもあります。
using namespace std::chrono_literals;
svr.set_websocket_ping_interval(30s);
クライアント側の設定
クライアント側でも同じAPIがあります。
httplib::ws::WebSocketClient cli("ws://localhost:8080/chat");
cli.set_websocket_ping_interval(30);
cli.connect();
connect()を呼ぶ前に設定しておきましょう。
デフォルト値
デフォルトのPing間隔は、ビルド時のマクロCPPHTTPLIB_WEBSOCKET_PING_INTERVAL_SECONDで決まります。通常はそのままで問題ありませんが、特別なプロキシ環境に合わせて短くしたい場合は調整してください。
PongはどうやってpIngに応答するか
WebSocketプロトコルでは、PingフレームにはPongフレームで応答することが決まっています。cpp-httplibは受信したPingに自動でPongを返すので、アプリケーションコード側で気にする必要はありません。
Pingの間隔をどう決めるか
| 環境 | 推奨 |
|---|---|
| 通常のインターネット接続 | 30〜60秒 |
| 厳しいプロキシ(AWS ALBなど) | 15〜30秒 |
| モバイル回線 | 短すぎるとバッテリーを食う、60秒以上 |
短すぎると無駄なトラフィックになり、長すぎると接続が切れます。だいたい接続が切れる時間の半分くらいが目安です。
Warning: Ping間隔を極端に短くすると、WebSocket接続ごとにバックグラウンドでスレッドが走るので、CPU負荷が上がります。接続数が多いサーバーでは控えめな値に設定しましょう。
無応答のピアを検出する
Pingを送るだけでは、相手が「黙って落ちた」場合に気付けません。TCPの接続自体は生きているように見えるのに、相手のプロセスはもう応答しない、というケースです。これを検出するには、送ったPingに対してPongがN回連続で返ってこなかったら接続を切る、というオプションを有効にします。
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_SERVER_READ_TIMEOUT_SECOND(デフォルト300秒 = 5分)が保険として働きます。一方クライアント側にはこの保険がなく、読み取りタイムアウトを設定しない限り無期限に待つので、無応答ピアを検出する手段はmax_missed_pongsだけです。どちらの側でも「もっと速く検出したい」ときに使うオプションでもあります。
接続が閉じたときの処理はW03. 接続クローズをハンドリングするを参照してください。