algotradelib 0.0.0.9000
C++ header-only library for algorithmic trading
Loading...
Searching...
No Matches
ws_session_base.hpp
1#pragma once
2
8#define ATL_TRANSPORT_LAYER_PRESENT 1
9
10#include <boost/asio/ip/tcp.hpp>
11#include <boost/asio/post.hpp>
12#include <boost/asio/steady_timer.hpp>
13#include <boost/asio/strand.hpp>
14#include <boost/beast/core.hpp>
15#include <boost/beast/websocket.hpp>
16
17#include <algorithm>
18#include <chrono>
19#include <cstdint>
20#include <deque>
21#include <map>
22#include <memory>
23#include <random>
24#include <string>
25#include <string_view>
26#include <utility>
27
28namespace algotradelib {
29
30namespace beast = boost::beast;
31namespace http = beast::http;
32namespace websocket = beast::websocket;
33namespace net = boost::asio;
35using tcp = net::ip::tcp;
36
37namespace network {
38
42 std::string target_{"/"};
44 std::map<std::string, std::string> headers_;
51 std::chrono::seconds operation_timeout_{30};
55 std::chrono::seconds reconnect_initial_{1};
57 std::chrono::seconds reconnect_max_{30};
58};
59
71template <typename Derived, typename Stream>
72class WebSocketSessionBase : public std::enable_shared_from_this<Derived> {
73public:
78 void connect() {
79 net::post(strand_, [self = shared_derived()] {
80 self->stop_requested_ = false;
81 self->do_connect();
82 });
83 }
84
93 void send(std::string message) {
94 net::post(strand_, [self = shared_derived(), message = std::move(message)]() mutable {
95 self->write_queue_.push_back(std::move(message));
96 if (self->connected_ && !self->closing_ && !self->writing_) {
97 self->do_write();
98 }
99 });
100 }
101
106 void close() {
107 net::post(strand_, [self = shared_derived()] {
108 self->stop_requested_ = true;
109 self->reconnect_timer_.cancel();
110 if (self->closing_) {
111 return;
112 }
113 self->closing_ = true;
114 if (self->connecting_ && !self->connected_) {
115 self->resolver_.cancel();
116 if (self->ws_) {
117 beast::error_code ignored;
118 beast::get_lowest_layer(*self->ws_).socket().cancel(ignored);
119 }
120 return;
121 }
122 if (self->connected_ && !self->writing_) {
123 self->do_close();
124 } else if (!self->connected_) {
125 self->closing_ = false;
126 self->on_closed();
127 }
128 });
129 }
130
131protected:
140 net::io_context& ioc,
141 std::string host,
142 std::string port,
144 )
145 : strand_(net::make_strand(ioc)), resolver_(strand_), reconnect_timer_(strand_),
146 host_(std::move(host)), port_(std::move(port)), options_(std::move(options)),
147 reconnect_rng_(std::random_device{}()) {}
148
150 virtual ~WebSocketSessionBase() = default;
151
156 virtual void on_message(std::string_view message) = 0;
157
159 virtual void on_open() {}
160
167 virtual void on_error(std::string_view stage, beast::error_code error) {
168 boost::ignore_unused(stage, error);
169 }
170
172 virtual void on_closed() {}
173
175 net::strand<net::io_context::executor_type> strand_;
177 tcp::resolver resolver_;
179 net::steady_timer reconnect_timer_;
181 std::unique_ptr<Stream> ws_;
183 beast::flat_buffer buffer_;
185 std::deque<std::string> write_queue_;
187 std::string host_;
189 std::string port_;
191 std::string host_header_;
195 bool connected_{false};
197 bool connecting_{false};
199 bool closing_{false};
201 bool writing_{false};
203 bool stop_requested_{false};
205 unsigned int reconnect_attempt_{0};
207 std::minstd_rand reconnect_rng_;
208
213 std::shared_ptr<Derived> shared_derived() {
214 return std::static_pointer_cast<Derived>(this->shared_from_this());
215 }
216
219 connecting_ = true;
220 resolver_.async_resolve(
221 host_, port_, beast::bind_front_handler(&WebSocketSessionBase::on_resolve, shared_derived())
222 );
223 }
224
230 beast::get_lowest_layer(*ws_).expires_never();
231 ws_->set_option(websocket::stream_base::timeout::suggested(beast::role_type::client));
232 ws_->set_option(websocket::stream_base::decorator([this](websocket::request_type& request) {
233 request.set(http::field::user_agent, "algotradelib");
234 for (const auto& header : options_.headers_) {
235 request.set(header.first, header.second);
236 }
237 }));
238 ws_->async_handshake(
241 beast::bind_front_handler(&WebSocketSessionBase::on_handshake, shared_derived())
242 );
243 }
244
256 if (!closing_) {
257 return false;
258 }
259 connecting_ = false;
260 closing_ = false;
261 if (ws_) {
262 beast::error_code ignored;
263 beast::get_lowest_layer(*ws_).socket().close(ignored);
264 }
265 on_closed();
266 return true;
267 }
268
275 void fail(std::string_view stage, beast::error_code error) {
276 connected_ = false;
277 connecting_ = false;
278 writing_ = false;
279 const bool closing = closing_;
280 closing_ = false;
281 if (ws_) {
282 beast::error_code ignored;
283 beast::get_lowest_layer(*ws_).socket().close(ignored);
284 }
285 on_error(stage, error);
286 if (closing) {
287 on_closed();
288 }
289 schedule_reconnect();
290 }
291
292private:
293 friend struct WebSocketSessionTestAccess;
294
295 void on_resolve(beast::error_code error, tcp::resolver::results_type endpoints) {
296 if (error) {
297 fail("resolve", error);
298 return;
299 }
300 if (abandon_if_closing()) {
301 return;
302 }
303 beast::get_lowest_layer(*ws_).expires_after(options_.operation_timeout_);
304 beast::get_lowest_layer(*ws_).async_connect(
305 endpoints, beast::bind_front_handler(&Derived::on_connect, shared_derived())
306 );
307 }
308
309 void on_handshake(beast::error_code error) {
310 if (error) {
311 fail("handshake", error);
312 return;
313 }
314 if (abandon_if_closing()) {
315 return;
316 }
317 connecting_ = false;
318 connected_ = true;
320 on_open();
321 do_read();
322 if (!write_queue_.empty()) {
323 do_write();
324 }
325 }
326
327 void do_write() {
328 writing_ = true;
329 ws_->async_write(
330 net::buffer(write_queue_.front()),
331 beast::bind_front_handler(&WebSocketSessionBase::on_write, shared_derived())
332 );
333 }
334
335 void on_write(beast::error_code error, std::size_t bytes_transferred) {
336 boost::ignore_unused(bytes_transferred);
337 writing_ = false;
338 if (error) {
339 fail("write", error);
340 return;
341 }
342 write_queue_.pop_front();
343 if (closing_) {
344 do_close();
345 } else if (!write_queue_.empty()) {
346 do_write();
347 }
348 }
349
350 void do_read() {
351 ws_->async_read(
352 buffer_, beast::bind_front_handler(&WebSocketSessionBase::on_read, shared_derived())
353 );
354 }
355
356 void on_read(beast::error_code error, std::size_t bytes_transferred) {
357 boost::ignore_unused(bytes_transferred);
358 if (error == websocket::error::closed) {
359 connected_ = false;
360 connecting_ = false;
361 closing_ = false;
362 on_closed();
363 schedule_reconnect();
364 return;
365 }
366 if (error) {
367 fail("read", error);
368 return;
369 }
370 const auto message = beast::buffers_to_string(buffer_.data());
371 buffer_.consume(buffer_.size());
372 on_message(message);
373 do_read();
374 }
375
376 void do_close() {
377 ws_->async_close(
378 websocket::close_code::normal,
379 beast::bind_front_handler(&WebSocketSessionBase::on_close, shared_derived())
380 );
381 }
382
383 void on_close(beast::error_code error) {
384 connected_ = false;
385 connecting_ = false;
386 closing_ = false;
387 if (error) {
388 on_error("close", error);
389 }
390 on_closed();
391 if (!stop_requested_) {
392 schedule_reconnect();
393 }
394 }
395
396 void schedule_reconnect() {
398 return;
399 }
400 const auto shift = std::min(reconnect_attempt_, 16U);
401 const auto multiplier = static_cast<std::int64_t>(1ULL << shift);
402 const auto delay = std::min(options_.reconnect_initial_ * multiplier, options_.reconnect_max_);
404 const auto base_delay = std::chrono::duration_cast<std::chrono::milliseconds>(delay);
405 const auto jitter_window = base_delay / 5;
406 const auto jitter = jitter_window.count() <= 0 ?
407 std::chrono::milliseconds(0) :
408 std::chrono::milliseconds(
409 std::uniform_int_distribution<std::int64_t>(0, jitter_window.count())(reconnect_rng_)
410 );
411 reconnect_timer_.expires_after(base_delay + jitter);
412 reconnect_timer_.async_wait([self = shared_derived()](beast::error_code error) {
413 if (!error) {
414 self->do_connect();
415 }
416 });
417 }
418};
419
420} // namespace network
421} // namespace algotradelib
virtual ~WebSocketSessionBase()=default
Destroys the session.
unsigned int reconnect_attempt_
Reconnect attempts since the last successful connect.
bool abandon_if_closing()
Ends a connect that close() interrupted, if it did; called first by each connect stage that completed...
virtual void on_closed()
Called when the session has closed (by close() or by the server).
virtual void on_open()
Called when the session is connected (after each successful upgrade, reconnects included).
void start_ws_handshake()
Sends the upgrade request; called once connected (WsSession) or after the TLS handshake (WssSession).
WebSocketSessionOptions options_
The options.
void fail(std::string_view stage, beast::error_code error)
Tears the session down after a failure, calls on_error (and on_closed if closing),...
net::steady_timer reconnect_timer_
The reconnect timer.
bool stop_requested_
Whether close() was called (no reconnection).
std::string host_header_
The Host header value (host:port), set on connect.
bool writing_
Whether a write is in progress.
bool connected_
Whether the session is connected.
WebSocketSessionBase(net::io_context &ioc, std::string host, std::string port, WebSocketSessionOptions options)
Constructs the session; nothing connects until connect().
void start_resolve()
Starts the DNS lookup; called by the transport class's do_connect() once ws_ is ready.
bool connecting_
Whether a connect is in progress.
beast::flat_buffer buffer_
The read buffer.
void send(std::string message)
Queues a message; messages are sent in order once connected.
bool closing_
Whether a close is in progress.
void connect()
Connects asynchronously (does nothing if connected, connecting or closing), and re-enables reconnecti...
std::unique_ptr< Stream > ws_
The stream, created by the transport class on each connect.
std::shared_ptr< Derived > shared_derived()
Returns a shared pointer to this session as the transport type.
virtual void on_message(std::string_view message)=0
Called with each complete message received.
void close()
Closes the session asynchronously after any write in progress, and disables reconnection; on_closed i...
net::strand< net::io_context::executor_type > strand_
The strand every handler runs on.
virtual void on_error(std::string_view stage, beast::error_code error)
Called after a failed stage has torn the session down; it then reconnects if automatic_reconnect_ is ...
std::deque< std::string > write_queue_
Messages waiting to be sent; the front one may be being written.
std::minstd_rand reconnect_rng_
The random source for reconnect jitter.
HTTP and WebSocket transport, OAuth tokens, and request pacing.
The library: broker-neutral types (core), and the modules below.
net::ip::tcp tcp
Short for boost::asio::ip::tcp.
Options for a WebSocket session (WsSession, WssSession).
std::map< std::string, std::string > headers_
Extra headers sent with the upgrade request.
std::chrono::seconds operation_timeout_
The deadline for the TCP connect and (for WssSession) the TLS handshake. The DNS lookup has no deadli...
std::chrono::seconds reconnect_max_
The longest reconnect delay (before jitter).
bool automatic_reconnect_
Whether to reconnect after a failure or a close not requested with close().
std::string target_
The path of the upgrade request.
std::chrono::seconds reconnect_initial_
The first reconnect delay; it doubles on each attempt, plus up to 20% random jitter.