FastMM documentation
FastMM C++ API 0.6.0
Public headers, tier 1 and tier 2
Loading...
Searching...
No Matches
connection.hpp
Go to the documentation of this file.
1#pragma once
2// Resilient WebSocket connection: DNS -> TCP -> TLS -> WS upgrade -> auth -> subscribe ->
3// Live, with stale/dead detection, exponential backoff reconnects, and make-before-break
4// rollover ahead of the venue's connection lifetime limit (Binance: 24 h).
5//
6// Idle -> Resolving -> Connecting -> TlsHandshake -> WsHandshake -> Authenticating
7// -> Subscribing -> Live <-> Stale ; any failure -> Backoff -> Resolving ...
8// close() -> Closing -> Idle (no reconnect)
9//
10// The handler (static dispatch, see ConnectionHandler) receives state changes and messages.
11// on_connected_send_subscriptions() is where the venue layer writes its subscribe/auth
12// payloads with send_text(); Authenticating/Subscribing pass through synchronously unless the
13// config asks for manual acknowledgement (auth_done()/subscribe_done() from the handler when
14// the venue's ack arrives).
15//
16// Rollover: when max_lifetime_ms elapses, a second session is opened while the first keeps
17// delivering; once the new session is Live the old one is closed. Both sessions deliver
18// messages during the overlap (market-data consumers must tolerate duplicates, which the
19// book-sync FSM does by sequence number). Sends target the newest session from its
20// on_ws_open onwards so subscriptions land on the right socket.
22#include "fastmm/net/byte_stream.hpp"
23#include "fastmm/net/dns.hpp"
25#include "fastmm/net/source_address.hpp"
26#include "fastmm/net/tls_stream.hpp"
27#include "fastmm/net/url.hpp"
29
30#include <algorithm>
31#include <cstddef>
32#include <cstdint>
33#include <functional>
34#include <memory>
35#include <optional>
36#include <span>
37#include <stdexcept>
38#include <string>
39#include <string_view>
40#include <type_traits>
41#include <utility>
42#include <vector>
43
44namespace fastmm::net {
45
46enum class ConnState : std::uint8_t {
47 Idle,
54 Live,
55 Stale, // Live, but nothing received for stale_ms (sub-state of Live)
56 Closing,
57 Backoff,
58};
59
60std::string_view to_string(ConnState s) noexcept;
61
63 std::string url; // ws:// or wss://host[:port]/path?query
64 struct Tls {
65 std::string ca_file; // trust only this bundle (self-signed sim server)
66 bool insecure = false; // skip verification entirely
67 } tls;
68 std::uint32_t stale_ms = 2000;
69 std::uint32_t dead_ms = 10'000;
71 std::uint64_t max_lifetime_ms = 0; // 0 = never roll over
72 std::uint32_t heartbeat_interval_ms = 0; // 0 = no client pings
73 std::uint32_t connect_timeout_ms = 10'000;
74 // Where the connection leaves this host from (net/source_address.hpp); empty: anywhere. Only
75 // the venue's addresses of a family the source has are tried.
76 SourceAddress source;
77 bool manual_auth = false; // handler calls auth_done()
78 bool manual_subscribe = false; // handler calls subscribe_done()
79 std::string extra_headers; // "Name: value\r\n" block for the upgrade request
80 // Called before every upgrade request and used instead of extra_headers when set: headers
81 // signed with a fresh nonce (Gemini authenticates the upgrade itself).
82 std::function<std::string()> make_headers;
83 // Venue handlers parse text with simdjson, whose first stage rejects invalid UTF-8; the
84 // WebSocket-level check would scan every message a second time.
86};
88// Throws std::invalid_argument on a bad URL; fills `url` (views into cfg.url).
90
91template <class H>
92concept ConnectionHandler =
93 requires(H& h, ConnState s, std::string_view t, std::span<const std::byte> b, std::int64_t ts) {
94 h.on_state(s);
95 h.on_text(t, ts);
96 h.on_binary(b, ts);
97 h.on_connected_send_subscriptions();
98 };
99
100struct ConnectionStats {
101 std::uint64_t connects = 0; // sessions that reached Live
102 std::uint64_t reconnects = 0; // Backoff entries
103 std::uint64_t rollovers = 0;
104 std::uint64_t stale_events = 0;
105 std::uint64_t dead_events = 0;
106 std::uint64_t frames_rx = 0;
107 std::uint64_t frames_tx = 0;
108 std::uint64_t bytes_rx = 0;
109 std::uint64_t bytes_tx = 0;
110 std::uint64_t drops = 0;
113template <ByteStream Stream, ConnectionHandler Handler>
114class Connection {
115 static constexpr bool kTls = std::is_same_v<Stream, TlsStream<PlainStream>>;
116 static_assert(std::is_same_v<Stream, PlainStream> || kTls,
117 "Connection supports PlainStream and TlsStream<PlainStream>");
118
119 public:
120 // For TLS streams a context is created from cfg.tls unless one is supplied.
121 Connection(Reactor& reactor, ConnectionConfig cfg, Handler& handler, TlsContext* tls = nullptr)
122 : reactor_(reactor),
123 cfg_(std::move(cfg)),
124 handler_(handler),
125 backoff_(cfg_.backoff),
126 resolver_(reactor) {
127 validate_connection_config(cfg_, url_);
128 if constexpr (kTls) {
129 if (tls == nullptr) {
130 owned_tls_ = std::make_unique<TlsContext>();
131 if (!cfg_.tls.ca_file.empty()) owned_tls_->set_ca_file(cfg_.tls.ca_file);
132 owned_tls_->set_insecure(cfg_.tls.insecure);
133 tls = owned_tls_.get();
134 }
135 }
136 tls_ = tls;
137 host_ = std::string(url_.host);
138 // Sized by the URL: a combined-stream path of many symbols is several kilobytes, and a fixed
139 // buffer too small for it left the target empty and the connection never upgrading.
140 target_.resize(url_.path.size() + url_.query.size() + 1);
141 target_.resize(url_.request_target(target_));
142 }
143 ~Connection() { teardown(); }
144 Connection(const Connection&) = delete;
145 Connection& operator=(const Connection&) = delete;
147 // Idle -> Resolving. Reconnects happen automatically until close().
148 void connect() {
149 if (state_ != ConnState::Idle) return;
150 user_closed_ = false;
151 backoff_.reset();
152 start_resolve();
153 }
154
155 // Explicit shutdown: close frame on the live session, then Idle. No reconnect.
156 void close() {
157 if (state_ == ConnState::Idle) return;
158 user_closed_ = true;
159 set_state(ConnState::Closing);
160 if (active_ && active_->ws.is_open())
161 active_->ws.send_close(WsCloseCode::Normal, "client closing");
162 teardown();
163 set_state(ConnState::Idle);
164 }
165
166 // Sends go to the newest session (see rollover note above).
167 bool send_text(std::string_view text) noexcept {
168 Session* s = send_target();
169 if (s == nullptr || !s->ws.is_open()) return false;
170 const bool ok = s->ws.send_text(text);
171 if (!ok) ++stats_.drops;
172 return ok;
173 }
174 // Coalesces the send_text()/send_binary() calls up to uncork() into one write (WsClient::cork).
175 // uncork() returns false when the corked session failed meanwhile (true when none was open).
176 void cork() noexcept {
177 corked_ = send_target();
178 if (corked_ != nullptr) corked_->ws.cork();
179 }
180 bool uncork() noexcept {
181 Session* s = corked_;
182 corked_ = nullptr;
183 if (s == nullptr) return true;
184 if (s != active_.get() && s != pending_.get()) return false;
185 return s->ws.uncork();
186 }
187 bool send_binary(std::span<const std::byte> data) noexcept {
188 Session* s = send_target();
189 if (s == nullptr || !s->ws.is_open()) return false;
190 const bool ok = s->ws.send_binary(data);
191 if (!ok) ++stats_.drops;
192 return ok;
193 }
194 bool send_ping() noexcept {
195 Session* s = send_target();
196 return s != nullptr && s->ws.is_open() && s->ws.send_ping();
197 }
198
199 // Manual acknowledgement hooks (see ConnectionConfig::manual_*).
200 void auth_done() {
201 if (Session* s = newest(); s != nullptr && s->phase == Phase::Authenticating)
202 enter_subscribing(*s);
203 }
204 void subscribe_done() {
205 if (Session* s = newest(); s != nullptr && s->phase == Phase::Subscribing) enter_live(*s);
207
208 ConnState state() const noexcept { return state_; }
209 bool is_live() const noexcept { return state_ == ConnState::Live || state_ == ConnState::Stale; }
210 std::int64_t last_rx_ns() const noexcept { return last_rx_ns_; }
211 const Url& url() const noexcept { return url_; }
212 const ConnectionConfig& config() const noexcept { return cfg_; }
214 // Accumulated counters plus the live sessions' running totals.
215 ConnectionStats stats() const noexcept {
216 ConnectionStats s = stats_;
217 for (const Session* sess : {active_.get(), pending_.get()}) {
218 if (sess == nullptr) continue;
219 const WsStats& w = sess->ws.stats();
220 s.frames_rx += w.frames_rx;
221 s.frames_tx += w.frames_tx;
222 s.bytes_rx += w.bytes_rx;
223 s.bytes_tx += w.bytes_tx;
224 s.drops += w.drops;
225 }
226 return s;
227 }
228
229 private:
230 enum class Phase : std::uint8_t { Connecting, Authenticating, Subscribing, Live, Dead };
231
232 struct Session;
233
234 // WsClientHandler adapter. It is a separate, complete type because naming
235 // WsClient<Stream, Session> from inside Session would check the handler concept against an
236 // incomplete class.
237 struct SessionEvents {
238 Connection* owner;
239 Session* self;
240 void on_ws_progress(WsProgress p) { owner->on_session_progress(*self, p); }
241 void on_ws_open() { owner->on_session_open(*self); }
242 void on_ws_text(std::string_view t, std::int64_t ts) { owner->on_session_text(*self, t, ts); }
243 void on_ws_binary(std::span<const std::byte> b, std::int64_t ts) {
244 owner->on_session_binary(*self, b, ts);
245 }
246 void on_ws_ping(std::span<const std::byte>) { owner->touch_rx(); }
247 void on_ws_pong(std::span<const std::byte>) { owner->touch_rx(); }
248 void on_ws_close(std::uint16_t, std::string_view) { owner->on_session_end(*self); }
249 void on_ws_error(NetError, std::string_view) { owner->on_session_end(*self); }
250 };
251
252 // One WebSocket session; the Connection may briefly own two during rollover. Heap
253 // allocated and never moved, so the client's handler reference stays valid.
254 struct Session {
255 Session(Connection& c, Stream&& stream)
256 : events{&c, this}, ws(c.reactor_, std::move(stream), events, c.cfg_.ws) {}
257
258 SessionEvents events;
259 WsClient<Stream, SessionEvents> ws;
260 Phase phase = Phase::Connecting;
261 std::int64_t live_at_ns = 0;
262 };
263
264 // ------------------------------------------------------------------------ state helpers
265 void set_state(ConnState s) {
266 if (state_ == s) return;
267 state_ = s;
268 handler_.on_state(s);
269 }
270 Session* newest() noexcept { return pending_ ? pending_.get() : active_.get(); }
271 Session* send_target() noexcept {
272 if (pending_ && pending_->phase != Phase::Connecting) return pending_.get();
273 return active_.get();
274 }
275 void touch_rx() noexcept { last_rx_ns_ = Reactor::now_ns(); }
276
277 // --------------------------------------------------------------------------- connecting
278 void start_resolve() {
279 set_state(ConnState::Resolving);
280 const std::uint64_t gen = ++resolve_gen_;
281 std::weak_ptr<int> alive = alive_;
282 resolver_.resolve(host_, url_.port, [this, gen, alive](ResolveResult r) {
283 if (alive.expired() || gen != resolve_gen_ || user_closed_) return;
284 if (!r.ok()) {
285 schedule_backoff();
286 return;
287 }
288 addrs_ = std::move(r.addrs);
289 keep_reachable(cfg_.source, addrs_);
290 if (addrs_.empty()) {
291 schedule_backoff();
292 return;
293 }
294 addr_index_ = 0;
295 open_session(/*rollover=*/false);
296 });
297 }
298
299 Stream make_stream(TcpSocket&& sock) {
300 if constexpr (kTls) {
301 return Stream(*tls_, PlainStream(std::move(sock)), host_);
302 } else {
303 return Stream(std::move(sock));
304 }
305 }
306
307 void open_session(bool rollover) {
308 const SockAddr& addr = addrs_[addr_index_ % addrs_.size()];
309 ++addr_index_;
310 TcpSocket sock;
311 const std::optional<SockAddr> local = source_bind_address(cfg_.source, addr.family());
312 if (sock.connect(addr, local ? &*local : nullptr) == ConnectStatus::Error) {
313 if (rollover) {
314 schedule_rollover_retry();
315 } else {
316 schedule_backoff();
317 }
318 return;
319 }
320 sock.set_nodelay(true);
321 sock.set_keepalive(true);
322 sock.set_rcvbuf(4 * 1024 * 1024);
323 auto session = std::make_unique<Session>(*this, make_stream(std::move(sock)));
324 Session* raw = session.get();
325 (rollover ? pending_ : active_) = std::move(session);
326 if (!rollover) set_state(ConnState::Connecting);
327 arm_connect_timer();
328 const std::string headers = cfg_.make_headers ? cfg_.make_headers() : cfg_.extra_headers;
329 if (!raw->ws.start(host_, url_.port, kTls, target_, headers)) {
330 on_session_end(*raw);
331 }
332 }
333
334 void arm_connect_timer() {
335 cancel(connect_timer_);
336 connect_timer_ = reactor_.add_timer_after(ms_to_ns(cfg_.connect_timeout_ms), [this] {
337 connect_timer_ = kInvalidTimer;
338 if (Session* s = newest(); s != nullptr && s->phase != Phase::Live) on_session_end(*s);
339 });
340 }
341
342 void on_session_progress(Session& s, WsProgress p) {
343 if (&s != active_.get()) return; // rollover sessions do not change the public state
344 if (p == WsProgress::TcpConnected) {
345 if constexpr (kTls) set_state(ConnState::TlsHandshake);
346 } else {
347 set_state(ConnState::WsHandshake);
348 }
349 }
350
351 void on_session_open(Session& s) {
352 s.phase = Phase::Authenticating;
353 touch_rx();
354 if (&s == active_.get()) set_state(ConnState::Authenticating);
355 if (!cfg_.manual_auth) enter_subscribing(s);
356 }
357
358 void enter_subscribing(Session& s) {
359 s.phase = Phase::Subscribing;
360 if (&s == active_.get()) set_state(ConnState::Subscribing);
361 handler_.on_connected_send_subscriptions();
362 if (s.phase != Phase::Subscribing) return; // handler closed us
363 if (!cfg_.manual_subscribe) enter_live(s);
364 }
365
366 void enter_live(Session& s) {
367 s.phase = Phase::Live;
368 s.live_at_ns = Reactor::now_ns();
369 cancel(connect_timer_);
370 ++stats_.connects;
371 backoff_.reset();
372 if (&s == pending_.get()) promote_pending();
373 set_state(ConnState::Live);
374 arm_health_timer();
375 arm_heartbeat_timer();
376 arm_lifetime_timer();
377 }
378
379 // ------------------------------------------------------------------------------ traffic
380 void on_session_text(Session&, std::string_view t, std::int64_t ts) {
381 on_rx(ts);
382 handler_.on_text(t, ts);
383 }
384 void on_session_binary(Session&, std::span<const std::byte> b, std::int64_t ts) {
385 on_rx(ts);
386 handler_.on_binary(b, ts);
387 }
388 void on_rx(std::int64_t ts) {
389 last_rx_ns_ = ts;
390 if (state_ == ConnState::Stale) set_state(ConnState::Live);
391 }
392
393 // ------------------------------------------------------------------------------ failure
394 // Sessions end from inside their own WsClient callbacks, so they are parked in a
395 // graveyard and deleted from the reactor loop instead of synchronously.
396 void on_session_end(Session& s) {
397 if (s.phase == Phase::Dead) return;
398 s.phase = Phase::Dead;
399 retire(s);
400 if (&s == pending_.get()) {
401 // A failed rollover keeps the old session; try again after a backoff delay.
402 bury(pending_);
403 schedule_rollover_retry();
404 return;
405 }
406 bury(active_);
407 if (pending_) {
408 // The old session died while the new one was still connecting: the new one becomes
409 // the (not yet live) active session and the public state follows it.
410 active_ = std::move(pending_);
411 set_state(active_->phase == Phase::Connecting ? ConnState::Connecting
412 : active_->phase == Phase::Authenticating ? ConnState::Authenticating
414 return;
415 }
416 cancel_session_timers();
417 if (!user_closed_) schedule_backoff();
418 }
419
420 void retire(Session& s) noexcept {
421 const WsStats& w = s.ws.stats();
422 stats_.frames_rx += w.frames_rx;
423 stats_.frames_tx += w.frames_tx;
424 stats_.bytes_rx += w.bytes_rx;
425 stats_.bytes_tx += w.bytes_tx;
426 stats_.drops += w.drops;
427 s.ws.close();
428 }
429
430 void schedule_backoff() {
431 ++stats_.reconnects;
432 set_state(ConnState::Backoff);
433 cancel(backoff_timer_);
434 backoff_timer_ = reactor_.add_timer_after(ms_to_ns(backoff_.next_ms()), [this] {
435 backoff_timer_ = kInvalidTimer;
436 if (!user_closed_) start_resolve();
437 });
438 }
439
440 // ----------------------------------------------------------------------------- timers
441 void arm_health_timer() {
442 cancel(health_timer_);
443 const std::uint32_t period =
444 std::max<std::uint32_t>(10, std::min(cfg_.stale_ms, cfg_.dead_ms) / 4);
445 health_timer_ = reactor_.add_timer_after(ms_to_ns(period), [this] {
446 health_timer_ = kInvalidTimer;
447 if (!is_live()) return;
448 const std::int64_t idle_ms = (Reactor::now_ns() - last_rx_ns_) / 1'000'000;
449 if (idle_ms >= cfg_.dead_ms) {
450 ++stats_.dead_events;
451 on_session_end(*active_); // forced reconnect through Backoff
452 return;
453 }
454 if (idle_ms >= cfg_.stale_ms && state_ == ConnState::Live) {
455 ++stats_.stale_events;
456 set_state(ConnState::Stale);
457 }
458 arm_health_timer();
459 });
460 }
461
462 void arm_heartbeat_timer() {
463 cancel(heartbeat_timer_);
464 if (cfg_.heartbeat_interval_ms == 0) return;
465 heartbeat_timer_ = reactor_.add_timer_after(ms_to_ns(cfg_.heartbeat_interval_ms), [this] {
466 heartbeat_timer_ = kInvalidTimer;
467 if (!is_live()) return;
468 send_ping();
469 arm_heartbeat_timer();
470 });
471 }
472
473 void arm_lifetime_timer() {
474 cancel(lifetime_timer_);
475 if (cfg_.max_lifetime_ms == 0) return;
476 lifetime_timer_ = reactor_.add_timer_after(ms_to_ns(cfg_.max_lifetime_ms), [this] {
477 lifetime_timer_ = kInvalidTimer;
478 start_rollover();
479 });
480 }
481
482 void start_rollover() {
483 if (!is_live() || pending_ || addrs_.empty()) return;
484 ++stats_.rollovers;
485 open_session(/*rollover=*/true);
486 }
487
488 void schedule_rollover_retry() {
489 cancel(lifetime_timer_);
490 lifetime_timer_ = reactor_.add_timer_after(ms_to_ns(backoff_.next_ms()), [this] {
491 lifetime_timer_ = kInvalidTimer;
492 start_rollover();
493 });
494 }
495
496 void promote_pending() {
497 if (active_) {
498 retire(*active_);
499 bury(active_);
500 }
501 active_ = std::move(pending_);
502 }
503
504 void bury(std::unique_ptr<Session>& s) {
505 if (!s) return;
506 graveyard_.push_back(std::move(s));
507 if (reap_pending_) return;
508 reap_pending_ = true;
509 std::weak_ptr<int> alive = alive_;
510 reactor_.post([this, alive] {
511 if (alive.expired()) return;
512 reap_pending_ = false;
513 graveyard_.clear();
514 });
515 }
516
517 void cancel(TimerId& id) noexcept {
518 if (id != kInvalidTimer) {
519 reactor_.cancel_timer(id);
520 id = kInvalidTimer;
521 }
522 }
523 void cancel_session_timers() noexcept {
524 cancel(connect_timer_);
525 cancel(health_timer_);
526 cancel(heartbeat_timer_);
527 cancel(lifetime_timer_);
528 }
529 // close(): sessions go to the graveyard (we may be inside one of their callbacks).
530 // Destructor: everything is deleted now; destroying a Connection from inside its own
531 // handler callbacks is not supported.
532 void teardown() noexcept {
533 cancel_session_timers();
534 cancel(backoff_timer_);
535 ++resolve_gen_; // orphan any in-flight resolve
536 if (pending_) {
537 retire(*pending_);
538 bury(pending_);
539 }
540 if (active_) {
541 retire(*active_);
542 bury(active_);
543 }
544 }
545 static constexpr std::int64_t ms_to_ns(std::uint64_t ms) noexcept {
546 return static_cast<std::int64_t>(ms) * 1'000'000;
547 }
548
549 Reactor& reactor_;
550 ConnectionConfig cfg_;
551 Handler& handler_;
552 Url url_;
553 std::string host_;
554 std::string target_;
555 std::unique_ptr<TlsContext> owned_tls_;
556 TlsContext* tls_ = nullptr;
557 ExponentialBackoff backoff_;
558 AsyncResolver resolver_;
559 std::shared_ptr<int> alive_ = std::make_shared<int>(0);
560 std::uint64_t resolve_gen_ = 0;
561 std::vector<SockAddr> addrs_;
562 std::size_t addr_index_ = 0;
563 std::unique_ptr<Session> active_;
564 std::unique_ptr<Session> pending_;
565 Session* corked_ = nullptr; // between cork() and uncork()
566 std::vector<std::unique_ptr<Session>> graveyard_;
567 bool reap_pending_ = false;
568 ConnState state_ = ConnState::Idle;
569 bool user_closed_ = false;
570 std::int64_t last_rx_ns_ = 0;
571 ConnectionStats stats_;
572 TimerId connect_timer_ = kInvalidTimer;
573 TimerId health_timer_ = kInvalidTimer;
574 TimerId heartbeat_timer_ = kInvalidTimer;
575 TimerId lifetime_timer_ = kInvalidTimer;
576 TimerId backoff_timer_ = kInvalidTimer;
577};
578
579} // namespace fastmm::net
Exponential backoff with jitter for reconnects: delay_n = cap_n * (1 - jitter) + U(0,...
Definition connection.hpp:116
const Url & url() const noexcept
Definition connection.hpp:213
void close()
Explicit shutdown: close frame on the live session, then Idle. No reconnect.
Definition connection.hpp:158
void cork() noexcept
Coalesces the send_text()/send_binary() calls up to uncork() into one write (WsClient::cork).
Definition connection.hpp:178
std::int64_t last_rx_ns() const noexcept
Definition connection.hpp:212
ConnectionStats stats() const noexcept
Accumulated counters plus the live sessions' running totals.
Definition connection.hpp:217
bool uncork() noexcept
Definition connection.hpp:182
const ConnectionConfig & config() const noexcept
Definition connection.hpp:214
ConnState state() const noexcept
Definition connection.hpp:210
bool send_text(std::string_view text) noexcept
Sends go to the newest session (see rollover note above).
Definition connection.hpp:169
bool is_live() const noexcept
Definition connection.hpp:211
Connection & operator=(const Connection &)=delete
void auth_done()
Manual acknowledgement hooks (see ConnectionConfig::manual_*).
Definition connection.hpp:202
void connect()
Idle -> Resolving. Reconnects happen automatically until close().
Definition connection.hpp:150
void subscribe_done()
Definition connection.hpp:206
~Connection()
Definition connection.hpp:145
bool send_binary(std::span< const std::byte > data) noexcept
Definition connection.hpp:189
bool send_ping() noexcept
Definition connection.hpp:196
void reset() noexcept
Definition backoff.hpp:40
Definition reactor.hpp:72
static std::int64_t now_ns() noexcept
CLOCK_MONOTONIC.
Definition connection.hpp:94
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)
ConnState
Definition connection.hpp:48
@ Stale
Live, but nothing received for stale_ms (sub-state of Live)
std::uint64_t TimerId
Definition reactor.hpp:69
std::string_view to_string(ReactorBackend backend) noexcept
"epoll" / "io_uring", the spelling of [engine] net_backend.
void validate_connection_config(const ConnectionConfig &cfg, Url &url)
Throws std::invalid_argument on a bad URL; fills url (views into cfg.url).
constexpr TimerId kInvalidTimer
Definition reactor.hpp:70
ConnState
Definition enums.hpp:404
Single-threaded event loop with a timer min-heap and a thread-safe mailbox (post/wake via eventfd),...
Definition backoff.hpp:15
Definition connection.hpp:66
bool insecure
skip verification entirely
Definition connection.hpp:68
std::string ca_file
trust only this bundle (self-signed sim server)
Definition connection.hpp:67
Definition connection.hpp:64
std::uint32_t stale_ms
Definition connection.hpp:70
bool manual_subscribe
handler calls subscribe_done()
Definition connection.hpp:80
bool manual_auth
handler calls auth_done()
Definition connection.hpp:79
BackoffConfig backoff
Definition connection.hpp:72
std::string url
ws:// or wss://host[:port]/path?query
Definition connection.hpp:65
std::uint32_t heartbeat_interval_ms
0 = no client pings
Definition connection.hpp:74
SourceAddress source
Where the connection leaves this host from (net/source_address.hpp); empty: anywhere.
Definition connection.hpp:78
std::string extra_headers
"Name: value\\r\\n" block for the upgrade request
Definition connection.hpp:81
std::uint64_t max_lifetime_ms
0 = never roll over
Definition connection.hpp:73
std::uint32_t connect_timeout_ms
Definition connection.hpp:75
WsClientConfig ws
Venue handlers parse text with simdjson, whose first stage rejects invalid UTF-8; the WebSocket-level...
Definition connection.hpp:87
std::function< std::string()> make_headers
Called before every upgrade request and used instead of extra_headers when set: headers signed with a...
Definition connection.hpp:84
struct fastmm::net::ConnectionConfig::Tls tls
std::uint32_t dead_ms
Definition connection.hpp:71
Definition connection.hpp:102
std::uint64_t drops
Definition connection.hpp:112
std::uint64_t frames_tx
Definition connection.hpp:109
std::uint64_t bytes_rx
Definition connection.hpp:110
std::uint64_t connects
sessions that reached Live
Definition connection.hpp:103
std::uint64_t reconnects
Backoff entries.
Definition connection.hpp:104
std::uint64_t bytes_tx
Definition connection.hpp:111
std::uint64_t frames_rx
Definition connection.hpp:108
std::uint64_t rollovers
Definition connection.hpp:105
std::uint64_t stale_events
Definition connection.hpp:106
std::uint64_t dead_events
Definition connection.hpp:107
Definition url.hpp:16
std::uint16_t port
explicit or scheme default (80/443)
Definition url.hpp:19
std::size_t request_target(std::span< char > buf) const noexcept
Path + "?" + query as sent in the request line; buf must hold both.
Definition url.hpp:25
std::string_view host
without brackets for IPv6 literals
Definition url.hpp:18
std::string_view query
without '?'
Definition url.hpp:21
std::string_view path
always at least "/"
Definition url.hpp:20
Definition ws_client.hpp:58
bool validate_utf8
reject text messages that are not UTF-8 (close code 1007)
Definition ws_client.hpp:63
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
Minimal URL parsing / query building for venue endpoints.
RFC 6455 WebSocket client over any ByteStream, driven by a Reactor.