LCOV - code coverage report
Current view: top level - http/server - websocket_connection.hpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 100.0 % 10 10
Test Date: 2026-11-10 10:38:07 Functions: 100.0 % 3 3

            Line data    Source code
       1              : #ifndef THINGER_WEBSOCKET_CONNECTION_HPP
       2              : #define THINGER_WEBSOCKET_CONNECTION_HPP
       3              : 
       4              : #include <chrono>
       5              : #include <cstdint>
       6              : #include <memory>
       7              : #include <string>
       8              : #include <boost/asio/streambuf.hpp>
       9              : #include "../../asio/async_event.hpp"
      10              : #include "../../asio/io_context_of.hpp"
      11              : #include "../../asio/sockets/websocket.hpp"
      12              : #include "../../asio/sockets/socket.hpp"
      13              : #include "../data/out_data.hpp"
      14              : #include "../../util/types.hpp"
      15              : #include "upgrade.hpp"
      16              : #include "flow_control.hpp"
      17              : #include "close_notifier.hpp"
      18              : 
      19              : namespace thinger::http{
      20              : 
      21              : // The queue of the connection (detail/push_queue.hpp), only declared here: not public
      22              : namespace detail {
      23              :     template<typename Item>
      24              :     class push_queue;
      25              :     enum class push_failure;
      26              : }
      27              : 
      28              : /// How a server WebSocket closed (see websocket_connection::on_close() and
      29              : /// asio::websocket_close). The status code (RFC 6455, 7.4) is the one of the peer's close frame
      30              : /// (1005 if it had none, 1002 if it was not valid); else the one this end closed with, as 1000
      31              : /// (stop(), or the code given to stop(code, reason)), 1001 (server shutdown, or idle), 1007 or
      32              : /// 1009 (a message not valid, or too large), 1011 (on_message() threw) or 1013 (it did not keep
      33              : /// up, see write_limits); 1006 if no close frame was exchanged: the connection was lost, closed
      34              : /// by the server at the end of a shutdown's grace period, or the close frame of stop() not
      35              : /// written whole by its deadline. When this end closed first and the peer answered while the
      36              : /// websocket was still reading (stop(), the shutdown), it is the code of the peer's answer, its
      37              : /// echo, as Node's ws reports it; if the peer did not answer, the one sent. The reason is always
      38              : /// the one of the peer's close frame, not the one given to stop()
      39              : using websocket_close = asio::websocket_close;
      40              : 
      41              : /**
      42              :  * A server WebSocket exchanging whole messages: received with on_message(), sent with
      43              :  * send_text()/send_binary() (queued, without waiting) or send_text_async()/send_binary_async()
      44              :  * (waiting until written, so a producer goes at the pace of the peer).
      45              :  *
      46              :  * Flow control: the queue holds 100 messages and drops the newest sent to a full queue, unless
      47              :  * set_write_limits() says otherwise (see write_limits in flow_control.hpp), and
      48              :  * dropped_messages() counts what the policy drops. pause_reading() lets TCP backpressure reach
      49              :  * the peer.
      50              :  *
      51              :  * Closing: stop() closes with 1000, stop(code, reason) with a code and a reason of the
      52              :  * application. Both do the closing handshake and end within asio::websocket::CLOSE_TIMEOUT,
      53              :  * whatever the peer does, and on_close() tells how it ended.
      54              :  *
      55              :  * Threads. Callbacks run on the websocket thread. From any thread: send_*(), wait_writable(),
      56              :  * set_write_limits(), on_high_watermark(), on_low_watermark(), pause_reading(),
      57              :  * resume_reading(), stop(), execute(), queued_bytes(), queued_messages() and dropped_messages().
      58              :  * On the websocket thread only (e.g. from its callbacks, or execute()): get_write_limits(), queue_full(),
      59              :  * is_reading_paused() and release_socket(). on_message() and on_shutdown() are set before
      60              :  * start(), in the handler that accepts it; on_close() too, or later on the websocket thread.
      61              :  */
      62              : class websocket_connection : public std::enable_shared_from_this<websocket_connection>{
      63              : 
      64              : public:
      65              : 
      66              :     /// Size of a read, and the default largest message received (a larger one closes with
      67              :     /// 1009, see websocket_accept_options::max_message_size)
      68              :     static constexpr size_t DEFAULT_BUFFER_SIZE = 1024;        // 1KB
      69              :     static constexpr size_t MAX_BUFFER_SIZE = 16*1024*1024;   // 16MB
      70              : 
      71              :     /// Default max_messages of the queue
      72              :     static constexpr int MAX_OUTPUT_MESSAGES = 100;
      73              : 
      74              :     /// On `socket`, with the largest message and the keepalive of `options` (its subprotocols
      75              :     /// and binary are those of an upgrade, not used here), and taken over from a server through
      76              :     /// `scope`, if any: told when the server shuts down (see on_shutdown()), counted as a
      77              :     /// websocket until closed
      78              :     websocket_connection(std::shared_ptr<asio::websocket> socket, const websocket_accept_options& options = {},
      79              :                          server_scope scope = {});
      80              : 
      81              :     virtual ~websocket_connection();
      82              : 
      83              :     /// Each message received, whole, and whether it is binary
      84              :     void on_message(sync_callback<void(std::string, bool binary)> callback);
      85              : 
      86              :     /// What to do when the server shuts down gracefully (see server_base::shutdown()),
      87              :     /// e.g. send a last message and stop(). By default it closes with 1001 (Going Away);
      88              :     /// whatever is open at the end of the grace period is closed anyway
      89              :     void on_shutdown(sync_callback<void()> callback);
      90              : 
      91              :     /// Once the WebSocket closed, whatever closed it (the peer, an error, stop(), the server
      92              :     /// shutdown, the overflow policy...), and how: called once, after its last on_message();
      93              :     /// nothing can be sent anymore. Always posted to the websocket thread, never called from
      94              :     /// within the call closing it (e.g. a send from a loop over the connections, which may then
      95              :     /// remove this one); what it throws is logged. Set once the WebSocket closed, it is posted
      96              :     /// at once. Not called once released (release_socket()), nor if the io_context is stopped
      97              :     /// and destroyed with it open. See websocket_close for the code it gets; after stop(), it is
      98              :     /// called within asio::websocket::CLOSE_TIMEOUT of the call
      99              :     void on_close(sync_callback<void(const websocket_close&)> callback);
     100              : 
     101              :     /// The executor of the websocket (its thread, or strand): the one of its socket
     102          672 :     const boost::asio::any_io_executor& get_executor() const { return ws_->get_executor(); }
     103              : 
     104              :     /// Run `callback` on the websocket thread
     105      2437190 :     inline void execute(sync_callback<void()> callback){
     106              :         // Already there (e.g. from on_message()): at once, as dispatch() does, without its cost
     107              :         // through a type-erased executor on every message
     108      2437190 :         if(asio::running_in_this_thread(ws_->get_executor())){
     109        36233 :             callback();
     110        36233 :             return;
     111              :         }
     112      2400957 :         boost::asio::dispatch(ws_->get_executor(), [self = shared_from_this(), callback = std::move(callback)](){
     113      2400957 :             callback();
     114      2400957 :         });
     115              :     }
     116              : 
     117              :     /// Start reading, and the keepalive: the connection lives while it is open
     118              :     void start();
     119              : 
     120              :     /// Close with 1000 (Normal Closure) and no reason, as stop(code, reason). It never throws
     121              :     void stop();
     122              : 
     123              :     /**
     124              :      * Close with `code` and `reason`, as ws.close(code, reason) in Node: the closing handshake
     125              :      * (the frame goes after the message being written, ahead of those still queued, which are
     126              :      * not sent; then the peer answers), the queue closed and the producers waiting told false,
     127              :      * the keepalive stopped, and on_close() called once.
     128              :      *
     129              :      * It ends within asio::websocket::CLOSE_TIMEOUT (5 s) of the call, whatever the peer does
     130              :      * (it does not answer, it does not read, it reads a few bytes at a time): at the deadline the
     131              :      * socket is closed without the handshake, even with a message being written, which also
     132              :      * bounds a slow peer that keeps reading: to get a large message out first, await its send.
     133              :      * write_limits::write_timeout is not waited for, and does not turn it into a 1013. on_close()
     134              :      * gets the code of the peer's answer; `code` if the peer did not answer; 1006 if the close
     135              :      * frame was not written whole.
     136              :      *
     137              :      * Throws std::invalid_argument, in the calling thread and without closing, if a close frame
     138              :      * may not carry them: `code` is not 1000-1014 (but 1004, 1005 and 1006) nor 3000-4999, or
     139              :      * `reason` is above 123 bytes or not valid UTF-8, whatever the state of the connection. Once
     140              :      * closing or closed it does nothing else: the first code decides, the close of a server
     141              :      * shutdown included
     142              :      */
     143              :     void stop(uint16_t code, std::string reason = {});
     144              : 
     145              :     /// Whether the next message sent without waiting overflows the queue (not the watermarks)
     146              :     bool queue_full() const;
     147              : 
     148              :     /// Send without waiting: queued, or handled by the overflow policy if the queue is full
     149              :     void send_text(std::string text);
     150              :     void send_binary(std::string data);
     151              : 
     152              :     /// Send and wait until written to the socket: true, or false if the connection closed or
     153              :     /// failed first. Never dropped by the overflow policy; a cancelled wait leaves it queued
     154              :     awaitable<bool> send_text_async(std::string text);
     155              :     awaitable<bool> send_binary_async(std::string data);
     156              : 
     157              :     void set_write_limits(const write_limits& limits);
     158              :     write_limits get_write_limits() const;
     159              : 
     160              :     /// Payload bytes, and messages, not written yet (the one being written included)
     161              :     size_t queued_bytes() const;
     162              :     size_t queued_messages() const;
     163              : 
     164              :     /// Messages sent without waiting that the overflow policy dropped since the connection was
     165              :     /// created: the new one (drop_newest, or drop_oldest with no room to make), or the older ones
     166              :     /// (drop_oldest). Not those awaited, those sent once closing or closed, those queued when it
     167              :     /// closed, nor an overflow with overflow_policy::close. It never decreases, and a send from
     168              :     /// another thread counts once the websocket thread took it: once all were taken (e.g. after
     169              :     /// execute()), written + queued + dropped is what was sent while open
     170              :     uint64_t dropped_messages() const;
     171              : 
     172              :     /// The watermarks crossed (see write_limits); not told once the websocket is closing
     173              :     void on_high_watermark(sync_callback<void()> callback);
     174              :     void on_low_watermark(sync_callback<void()> callback);
     175              : 
     176              :     /// Wait while above the high watermark, until drained to the low one: true, or false if
     177              :     /// the connection closed
     178              :     awaitable<bool> wait_writable();
     179              : 
     180              :     /**
     181              :      * Stop reading the socket, so the peer's writes block once the TCP buffers fill; writes go
     182              :      * on. Messages already read are delivered: no other one if called on the websocket thread
     183              :      * (e.g. from on_message()), one more may be from another thread. While paused, the pings
     184              :      * and close frame of the peer are not read and the keepalive is suspended, so a dead peer
     185              :      * is not noticed: bound the pause, or set a write_timeout for a peer that stops reading
     186              :      * too. stop() still closes, discarding the messages before the peer's close frame
     187              :      */
     188              :     void pause_reading();
     189              : 
     190              :     /// Read again, in order and from where reading stopped; the keepalive is restarted. Nothing
     191              :     /// once closing: the closing handshake reads the socket
     192              :     void resume_reading();
     193              : 
     194              :     bool is_reading_paused() const;
     195              : 
     196              :     /// Release the socket for another handler: no other call may be made to this instance, and
     197              :     /// on_close() is not called. To close with a code of your own, stop(code, reason)
     198              :     std::shared_ptr<asio::socket> release_socket();
     199              : 
     200              : private:
     201              : 
     202              :     void start_read_loop();
     203              :     awaitable<void> read_loop();
     204              : 
     205              :     /// The read loop, closing the websocket if on_message() throws
     206              :     awaitable<void> read_messages();
     207              : 
     208              :     /// How the websocket closed (see on_close())
     209              :     websocket_close close_status() const;
     210              : 
     211              :     /// Deliver the message read whole: false if it is not valid
     212              :     bool deliver_message();
     213              : 
     214              :     /// Wait while reading is paused: false if the websocket closed, or was released
     215              :     awaitable<bool> wait_resumed();
     216              :     void wake_reader();
     217              : 
     218              :     /// The keepalive is suspended while paused
     219              :     void suspend_keepalive();
     220              :     void restore_keepalive();
     221              : 
     222              :     /// Start the closing handshake with `code` and `reason`, unless closing already, and end the
     223              :     /// read loop once closed if paused
     224              :     void start_close(uint16_t code, std::string reason = {});
     225              : 
     226              :     /// A message to write
     227              :     struct out_message {
     228              :         std::string data;
     229              :         bool binary = false;
     230              :     };
     231              : 
     232              :     /// Write a message, closing the connection if the queue fails (see push_queue)
     233              :     awaitable<boost::system::error_code> write_message(out_message& message);
     234              :     void queue_failed(detail::push_failure failure, boost::system::error_code ec);
     235              : 
     236              :     /// Queue a message sent without waiting, or apply the overflow policy
     237              :     void push_message(std::string data, bool binary);
     238              : 
     239              :     /// Close a websocket that does not keep up: 1013 (Try Again Later)
     240              :     void close_congested(const char* reason);
     241              : 
     242              :     awaitable<bool> send_async(std::string data, bool binary);
     243              : 
     244              :     /// The server shuts down (on the websocket thread)
     245              :     void server_shutdown();
     246              : 
     247              : private:
     248              : 
     249              :     std::shared_ptr<asio::websocket> ws_;
     250              : 
     251              :     /// Messages not written yet, against the write limits, and their writer
     252              :     std::unique_ptr<detail::push_queue<out_message>> queue_;
     253              : 
     254              :     /// Incoming data: the message being received, grown as its bytes arrive (never to the size
     255              :     /// a frame header announces), up to max_message_size; given up after a message above 64 KiB
     256              :     boost::asio::streambuf buffer_{MAX_BUFFER_SIZE};
     257              : 
     258              :     sync_callback<void(std::string, bool)> on_frame_callback_;
     259              :     sync_callback<void()> on_shutdown_callback_;
     260              :     close_notifier<websocket_close> on_close_;
     261              : 
     262              :     /// The server this connection was taken over from, if any
     263              :     server_scope scope_;
     264              : 
     265              :     /// Reading paused (see pause_reading()), and the event the read loop waits on meanwhile
     266              :     bool paused_ = false;
     267              :     asio::async_event resumed_;
     268              : 
     269              :     /// Started (start()), and released (release_socket())
     270              :     bool started_ = false;
     271              :     bool released_ = false;
     272              : 
     273              :     /// The connection was lost: a read failed while the websocket was open, with no close frame
     274              :     /// out
     275              :     bool lost_ = false;
     276              : 
     277              :     /// The closing handshake started (see start_close())
     278              :     bool closing_ = false;
     279              : 
     280              :     /// Keepalive started by start(), and its settings while suspended
     281              :     std::chrono::milliseconds ping_interval_;
     282              :     std::chrono::milliseconds idle_timeout_;
     283         3102 :     std::chrono::milliseconds keepalive_ping_{0};
     284         3102 :     std::chrono::milliseconds keepalive_idle_{0};
     285              : };
     286              : 
     287              : }
     288              : 
     289              : #endif
        

Generated by: LCOV version 2.0-1