14#include "fastmm/net/byte_stream.hpp"
15#include "fastmm/net/http_message.hpp"
17#include "fastmm/net/recv_buffer.hpp"
18#include "fastmm/net/wire_buffer.hpp"
19#include "fastmm/net/ws_frame.hpp"
39concept WsClientHandler =
requires(H& h,
41 std::span<const std::byte> b,
49 h.on_ws_binary(b, ts);
52 h.on_ws_close(code, s);
58 std::size_t{4} * 1024 * 1024;
76inline constexpr std::size_t kWsKeyLen = 24;
77inline constexpr std::size_t kWsAcceptLen = 28;
80void ws_compute_accept_key(std::string_view client_key, std::span<char, kWsAcceptLen> out)
noexcept;
81void ws_generate_client_key(std::span<char, kWsKeyLen> out)
noexcept;
85bool ws_build_upgrade_request(WireBuffer& out,
86 std::string_view host,
89 std::string_view target,
91 std::string_view extra_headers)
noexcept;
94const char* ws_check_upgrade_response(
const HttpResponseHead& head,
95 std::string_view expected_accept)
noexcept;
101 MaskGenerator()
noexcept;
102 void next(std::uint8_t out[4])
noexcept {
103 state_ ^= state_ >> 12;
104 state_ ^= state_ << 25;
105 state_ ^= state_ >> 27;
106 const std::uint64_t v = state_ * 0x2545F4914F6CDD1DULL;
107 const auto w =
static_cast<std::uint32_t
>(v >> 32);
108 out[0] =
static_cast<std::uint8_t
>(w);
109 out[1] =
static_cast<std::uint8_t
>(w >> 8);
110 out[2] =
static_cast<std::uint8_t
>(w >> 16);
111 out[3] =
static_cast<std::uint8_t
>(w >> 24);
115 std::uint64_t state_;
120template <ByteStream Stream, WsClientHandler Handler>
121class WsClient final :
public IoHandler {
127 rx_(cfg.recv_capacity),
128 tx_(cfg.send_capacity),
130 cfg.max_message_bytes != 0 ? cfg.max_message_bytes
131 : cfg.recv_capacity - kWsMaxHeaderSize,
133 cfg.validate_utf8) {}
136 WsClient(
const WsClient&) =
delete;
141 bool start(std::string_view host,
144 std::string_view target,
145 std::string_view extra_headers = {}) {
147 host_ = std::string(host);
150 target_ = std::string(target);
151 extra_headers_ = std::string(extra_headers);
164 bool send_text(std::string_view text)
noexcept {
167 std::span<const std::byte>(
reinterpret_cast<const std::byte*
>(text.data()), text.size()));
169 bool send_binary(std::span<const std::byte> data)
noexcept {
170 return send_frame(WsOpcode::Binary, data);
172 bool send_ping(std::span<const std::byte> data = {})
noexcept {
173 return send_frame(WsOpcode::Ping, data);
175 bool send_pong(std::span<const std::byte> data = {})
noexcept {
176 return send_frame(WsOpcode::Pong, data);
183 void cork() noexcept { corked_ =
true; }
192 bool send_close(WsCloseCode code = WsCloseCode::Normal, std::string_view reason = {})
noexcept {
194 std::array<std::byte, kWsMaxControlPayload> payload{};
195 const std::size_t n =
196 ws_encode_close_payload(payload,
static_cast<std::uint16_t
>(code), reason);
197 if (!encode_frame(WsOpcode::Close, std::span<const std::byte>(payload.data(), n)))
return false;
204 void close() noexcept {
214 Stream&
stream() noexcept {
return stream_; }
215 int fd() const noexcept {
return stream_.fd(); }
224 read_upgrade_response();
243 fail(NetError::Syscall, err != 0 ?
"socket error" :
"epoll error");
247 void detach() noexcept {
249 reactor_.
remove(stream_.fd());
254 void drive_connect() {
255 const IoResult r = stream_.handshake();
257 fail(r.error, r.error == NetError::Tls ?
"tls handshake failed" :
"connect failed");
260 if (!tcp_reported_ && (!r.would_block() || tcp_connected())) {
261 tcp_reported_ =
true;
265 if (r.would_block())
return;
267 detail::ws_generate_client_key(std::span<char, detail::kWsKeyLen>(key_.data(), key_.size()));
268 detail::ws_compute_accept_key(
269 std::string_view(key_.data(), key_.size()),
270 std::span<char, detail::kWsAcceptLen>(expected_accept_.data(), expected_accept_.size()));
271 if (!detail::ws_build_upgrade_request(tx_,
276 std::string_view(key_.data(), key_.size()),
278 fail(NetError::Overflow,
"upgrade request exceeds send buffer");
289 bool tcp_connected() const noexcept {
290 if constexpr (
requires(
const Stream& s) { s.transport_ready(); }) {
291 return stream_.transport_ready();
297 void read_upgrade_response() {
299 const std::span<std::byte> w = rx_.writable();
301 fail(NetError::Overflow,
"upgrade response exceeds receive buffer");
304 const IoResult r = stream_.read(w);
308 HttpResponseHead head;
309 const HttpParseStatus st = parse_response_head(rx_.readable_view(), head);
310 if (st == HttpParseStatus::Invalid || st == HttpParseStatus::TooManyHeaders) {
311 fail(NetError::Protocol,
"malformed upgrade response");
314 if (st == HttpParseStatus::Ok) {
315 if (
const char* why = detail::ws_check_upgrade_response(
316 head, std::string_view(expected_accept_.data(), expected_accept_.size()))) {
317 fail(NetError::Protocol, why);
320 rx_.consume(head.head_len);
322 handler_.on_ws_open();
325 if (!process_frames())
return;
331 fail(NetError::Closed,
"eof during upgrade");
335 fail(r.error,
"read failed during upgrade");
338 if (r.would_block())
return;
345 const std::span<std::byte> w = rx_.writable();
347 fail_close(WsCloseCode::MessageTooBig,
"message larger than receive buffer");
350 const IoResult r = stream_.read(w);
355 if (!process_frames())
return;
357 if (input_drained())
return;
360 fail(NetError::Closed,
"eof");
364 fail(r.error,
"read failed");
367 if (r.would_block())
return;
373 bool input_drained() const noexcept {
374 if constexpr (
requires(
const Stream& s) {
375 { s.input_drained() } -> std::same_as<bool>;
377 return !reactor_.
event_hangup() && stream_.input_drained();
384 bool process_frames() {
385 const detail::WsAssembleError err = assembler_.process(*
this);
387 fail_close(err.code, err.detail);
394 friend class detail::WsMessageAssembler;
397 void on_message(WsOpcode op, std::span<std::byte> payload)
noexcept {
400 if (op == WsOpcode::Text) {
402 std::string_view(
reinterpret_cast<const char*
>(payload.data()), payload.size()), rx_ts_);
404 handler_.on_ws_binary(payload, rx_ts_);
407 void on_control(WsOpcode op, std::span<std::byte> payload)
noexcept {
412 handler_.on_ws_ping(payload);
416 handler_.on_ws_pong(payload);
418 case WsOpcode::Close: {
419 const std::uint16_t code = ws_decode_close_code(payload);
420 const std::string_view reason =
421 payload.size() > 2 ? std::string_view(
reinterpret_cast<const char*
>(payload.data()) + 2,
423 : std::string_view{};
426 std::array<std::byte, kWsMaxControlPayload> echo{};
427 const std::size_t n = ws_encode_close_payload(
428 echo, code ==
static_cast<std::uint16_t
>(WsCloseCode::NoStatus) ? 1000 : code, {});
429 send_frame(WsOpcode::Close, std::span<const std::byte>(echo.data(), n));
433 handler_.on_ws_close(code, reason);
442 bool encode_frame(WsOpcode op, std::span<const std::byte> payload)
noexcept {
443 std::uint8_t mask[4];
445 if (!ws_encode_frame(tx_, op,
true, payload, mask)) {
455 bool send_frame(WsOpcode op, std::span<const std::byte> payload)
noexcept {
457 if (corked_ && op != WsOpcode::Text && op != WsOpcode::Binary) corked_ =
false;
458 if (corked_ && tx_.free_space() < payload.size() + kWsMaxHeaderSize) {
462 if (!encode_frame(op, payload))
return false;
463 if (corked_)
return true;
468 void flush() noexcept {
469 if constexpr (
requires(Stream& s) { s.flush(); }) {
470 const IoResult fl = stream_.flush();
472 fail(fl.error,
"flush failed");
475 if (fl.want_write)
return;
477 while (!tx_.empty()) {
478 const IoResult w = stream_.write(tx_.pending());
479 tx_.consumed(w.bytes);
482 fail(w.error,
"write failed");
485 if (w.would_block())
return;
490 void fail_close(WsCloseCode code,
const char* detail)
noexcept {
492 std::array<std::byte, kWsMaxControlPayload> payload{};
493 const std::size_t n = ws_encode_close_payload(payload,
static_cast<std::uint16_t
>(code), {});
494 send_frame(WsOpcode::Close, std::span<const std::byte>(payload.data(), n));
496 fail(code == WsCloseCode::MessageTooBig ? NetError::Overflow : NetError::Protocol, detail);
499 void fail(NetError err,
const char* detail)
noexcept {
502 handler_.on_ws_error(err, detail);
510 detail::WsMessageAssembler assembler_;
511 detail::MaskGenerator masks_;
514 bool registered_ =
false;
515 bool corked_ =
false;
516 bool tcp_reported_ =
false;
518 std::uint16_t port_ = 0;
519 std::int64_t rx_ts_ = 0;
520 std::array<char, detail::kWsKeyLen> key_{};
521 std::array<char, detail::kWsAcceptLen> expected_accept_{};
524 std::string extra_headers_;
Definition reactor.hpp:72
bool event_hangup() const noexcept
True while a handler runs for an event that also reported a hang-up or an error (RDHUP/HUP/ERR).
Definition reactor.hpp:146
bool add(int fd, IoHandler &handler, IoEvent events) noexcept
fd registration (reactor thread only).
static std::int64_t now_ns() noexcept
CLOCK_MONOTONIC.
bool remove(int fd) noexcept
Definition ws_client.hpp:123
void close() noexcept
Immediate teardown, no callbacks.
Definition ws_client.hpp:205
void on_writable() override
Definition ws_client.hpp:235
WsClient & operator=(const WsClient &)=delete
void on_message(WsOpcode op, std::span< std::byte > payload) noexcept
Definition ws_client.hpp:397
std::size_t pending_send_bytes() const noexcept
Definition ws_client.hpp:214
WsState state() const noexcept
Definition ws_client.hpp:211
bool send_ping(std::span< const std::byte > data={}) noexcept
Definition ws_client.hpp:173
void cork() noexcept
Between cork() and uncork() data frames are only encoded into the send buffer; uncork() writes them t...
Definition ws_client.hpp:184
bool send_text(std::string_view text) noexcept
False means the frame did not go out: the client is not open, the frame did not fit,...
Definition ws_client.hpp:165
void on_error(int err) override
POLLERR (or a poll request the kernel refused).
Definition ws_client.hpp:242
bool send_binary(std::span< const std::byte > data) noexcept
Definition ws_client.hpp:170
void on_readable() override
Definition ws_client.hpp:218
bool send_close(WsCloseCode code=WsCloseCode::Normal, std::string_view reason={}) noexcept
Starts the closing handshake; on_ws_close fires when the peer echoes the close frame.
Definition ws_client.hpp:193
bool is_open() const noexcept
Definition ws_client.hpp:212
void on_control(WsOpcode op, std::span< std::byte > payload) noexcept
Definition ws_client.hpp:407
int fd() const noexcept
Definition ws_client.hpp:216
bool start(std::string_view host, std::uint16_t port, bool tls, std::string_view target, std::string_view extra_headers={})
Registers with the reactor and begins the connect/TLS/upgrade sequence.
Definition ws_client.hpp:143
Stream & stream() noexcept
Definition ws_client.hpp:215
bool uncork() noexcept
Definition ws_client.hpp:185
const WsStats & stats() const noexcept
Definition ws_client.hpp:213
~WsClient() override
Definition ws_client.hpp:137
bool send_pong(std::span< const std::byte > data={}) noexcept
Definition ws_client.hpp:176
Definition reactor.hpp:37
WsProgress
Milestones before the upgrade completes, so owners can report fine-grained states.
Definition ws_client.hpp:35
@ TcpConnected
TCP established (TLS handshake starts next for TLS streams)
@ UpgradeSent
transport ready, HTTP upgrade request written
WsState
Definition ws_client.hpp:74
Single-threaded event loop with a timer min-heap and a thread-safe mailbox (post/wake via eventfd),...
Definition ws_client.hpp:58
bool validate_utf8
reject text messages that are not UTF-8 (close code 1007)
Definition ws_client.hpp:63
std::size_t send_capacity
WireBuffer for outgoing frames.
Definition ws_client.hpp:61
std::size_t max_message_bytes
0 = recv_capacity - kWsMaxHeaderSize
Definition ws_client.hpp:62
std::size_t recv_capacity
RecvBuffer; bounds the largest message.
Definition ws_client.hpp:59
Definition ws_client.hpp:66
std::uint64_t drops
sends refused because the send buffer was full
Definition ws_client.hpp:71
std::uint64_t frames_tx
Definition ws_client.hpp:68
std::uint64_t frames_rx
Definition ws_client.hpp:67
std::uint64_t bytes_tx
Definition ws_client.hpp:70
std::uint64_t bytes_rx
Definition ws_client.hpp:69