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.
3.7 KiB
title, order, status
| title | order | status |
|---|---|---|
| W01. WebSocketエコーサーバー/クライアントを実装する | 52 | draft |
WebSocketは、クライアントとサーバーの間で双方向にメッセージをやり取りするためのプロトコルです。cpp-httplibはサーバーとクライアントの両方のAPIを提供しています。まずは一番シンプルなエコーサーバーから見てみましょう。
サーバー: エコーサーバー
#include <httplib.h>
int main() {
httplib::Server svr;
svr.WebSocket("/echo", [](const httplib::Request &req, httplib::ws::WebSocket &ws) {
std::string msg;
while (ws.is_open()) {
auto result = ws.read(msg);
if (result == httplib::ws::ReadResult::Fail) {
break;
}
ws.send(msg); // 受け取った内容をそのまま返す
}
});
svr.listen("0.0.0.0", 8080);
}
svr.WebSocket()でWebSocket用のハンドラを登録します。ハンドラが呼ばれた時点で、すでにWebSocketのハンドシェイクは完了しています。ループの中でws.read()してws.send()するだけで、エコー動作が完成します。
read()の返り値はReadResult列挙値で、次の4種類です。
ReadResult::Text: テキストメッセージを受信ReadResult::Binary: バイナリメッセージを受信ReadResult::Fail: エラー、または接続が閉じたReadResult::Timeout: 何も受信しないまま読み取りタイムアウトが経過した。接続は開いたまま。読み取りタイムアウトを設定したときだけ返る(W06. タイムアウトを設定するを参照)
クライアント: エコーを叩く
#include <httplib.h>
int main() {
httplib::ws::WebSocketClient cli("ws://localhost:8080/echo");
if (!cli.connect()) {
std::cerr << "failed to connect" << std::endl;
return 1;
}
cli.send("Hello, WebSocket!");
std::string msg;
if (cli.read(msg) != httplib::ws::ReadResult::Fail) {
std::cout << "received: " << msg << std::endl;
}
cli.close();
}
URLにはws://(平文)またはwss://(TLS)を指定します。connect()でハンドシェイクを行い、あとはsend()とread()でサーバーと同じAPIでやり取りできます。
テキストとバイナリの送り分け
send()には2つのオーバーロードがあり、テキストとバイナリで使い分けられます。
ws.send("Hello"); // テキストフレーム
ws.send(binary_data, binary_data_size); // バイナリフレーム
std::stringを受け取るオーバーロードはテキスト、const char*とサイズを受け取るオーバーロードはバイナリとして送られます。詳しくはW04. バイナリフレームを送受信するを参照してください。
スレッドとの関係
WebSocket接続はハンドラが終わるまで生き続けるので、1接続につきワーカースレッドを1つ占有します。同時接続数が多い場合は、スレッドプールを動的スケーリングに設定しましょう。
svr.new_task_queue = [] {
return new httplib::ThreadPool(8, 128);
};
詳細はS21. マルチスレッド数を設定するを参照してください。
Note: HTTPSサーバーの上でWebSocketを動かしたいときは、
httplib::Serverの代わりにhttplib::SSLServerを使えば、同じWebSocket()ハンドラがそのまま動きます。クライアント側はwss://スキームを指定するだけです。CA証明書やクライアント証明書の設定はW05. wss接続でTLSを設定するを参照してください。