LCOV - code coverage report
Current view: top level - asio/sockets - socket.hpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 88.9 % 9 8
Test Date: 2026-11-10 10:38:07 Functions: 87.5 % 8 7

            Line data    Source code
       1              : 
       2              : #ifndef THINGER_ASIO_SOCKET_HPP
       3              : #define THINGER_ASIO_SOCKET_HPP
       4              : 
       5              : #include <mutex>
       6              : #include <map>
       7              : #include <memory>
       8              : #include <optional>
       9              : #include <string_view>
      10              : 
      11              : #include <boost/asio.hpp>
      12              : #include <boost/asio/buffer.hpp>
      13              : #include <boost/system/error_code.hpp>
      14              : 
      15              : #include "../../util/types.hpp"
      16              : #include "../ssl/tls_session.hpp"
      17              : 
      18              : namespace thinger::asio {
      19              : 
      20              : // Byte stream socket: TCP, TLS, Unix, a WebSocket (see websocket.hpp), or a socket defined by
      21              : // the application (e.g. a virtual stream multiplexed inside another connection), all used the
      22              : // same way by the protocols and tunnels running on them (see socket_pipe.hpp).
      23              : //
      24              : // Like the asio objects they wrap, sockets are not thread-safe: use them (close() and cancel()
      25              : // included) on a thread running their io_context (see get_io_context()), dispatching there
      26              : // from any other thread. At most one read and one write may be pending at the same time
      27              : // (a websocket queues concurrent writes).
      28              : //
      29              : // A socket defined by the application must implement read_some(), write(buffer, size),
      30              : // close(), cancel() and is_open(); the other methods have defaults built on them (read(),
      31              : // read_until() and the other writes) or answering as a socket that does not support them
      32              : // (connect(), wait(), and the getters); `using socket::read; using socket::write;` keeps the
      33              : // default overloads visible on the derived type. Its operations must keep these guarantees:
      34              : // - read_some() waits until at least one byte is available, and returns it (never 0 bytes
      35              : //   without an error, unless max_size is 0); boost::asio::error::eof once the peer closed
      36              : //   the stream and everything it sent was read.
      37              : // - write() returns once all the bytes are written (accepted by the socket, as a TCP socket
      38              : //   copies them to the kernel), or with an error.
      39              : // - close() closes the socket right away, without waiting: is_open() is false afterwards, the
      40              : //   pending operations complete with an error (operation_aborted) and new ones fail. It may be
      41              : //   called several times, and from the destructor.
      42              : // - cancel() makes the pending operations complete with operation_aborted; the socket stays
      43              : //   open.
      44              : // - Operations complete on the io_context of the socket. Data arriving from another thread
      45              : //   (e.g. from the connection a virtual stream runs inside) is handed over by posting it to
      46              : //   that io_context.
      47              : // - Pending operations should support per-operation cancellation (as asio operations and
      48              : //   timers do), used by the awaitable operators (||). If they do not, close() must still
      49              : //   complete them.
      50              : class socket : private boost::asio::noncopyable {
      51              : 
      52              : public:
      53              :     // constructors and destructors
      54              :     socket(const std::string &context, boost::asio::io_context &io_context);
      55              :     virtual ~socket();
      56              : 
      57              :     // socket control
      58              :     // Connect to host:port (with the TLS handshake if required); operation_not_supported
      59              :     // by default. `timeout` bounds the whole call, taken as a deadline when it starts: the
      60              :     // name resolution, the connect and, over TLS, the client handshake share it, each phase
      61              :     // getting the time left. Once it passes, the socket is closed and the call returns
      62              :     // boost::asio::error::timed_out (operation_aborted if the caller cancelled it). Other
      63              :     // errors are returned as they are (a handshake error with the socket closed). The bound of
      64              :     // the resolution is best-effort: asio cannot interrupt getaddrinfo, so a resolver that
      65              :     // hangs ends the call only once it returns (with timed_out if the deadline passed); a host
      66              :     // given as an address is not resolved
      67              :     virtual awaitable<boost::system::error_code> connect(
      68              :         const std::string &host,
      69              :         const std::string &port,
      70              :         std::chrono::seconds timeout);
      71              :     virtual void close() = 0;
      72              :     virtual void cancel() = 0;
      73              :     virtual bool requires_handshake() const;
      74              :     virtual awaitable<boost::system::error_code> handshake(const std::string &host = "");
      75              : 
      76              :     // read operations
      77              :     // Up to max_size bytes, at least one (see above)
      78              :     virtual awaitable<io_result> read_some(uint8_t buffer[], size_t max_size) = 0;
      79              :     // As read_some(), for a caller that ends this read alone: a cancellation through `slot` ends
      80              :     // it (operation_aborted), not whatever else waits on the socket, as cancel() would, and that
      81              :     // of the caller does not reach it, which is for the caller to forward. The default runs
      82              :     // read_some() on a coroutine of its own (which costs one per call); TCP, TLS and Unix bind
      83              :     // the slot to the read itself
      84              :     virtual awaitable<io_result> read_some_cancellable(uint8_t buffer[], size_t max_size,
      85              :                                                        boost::asio::cancellation_slot slot);
      86              :     // Exactly `size` bytes (fewer only with an error)
      87              :     virtual awaitable<io_result> read(uint8_t buffer[], size_t size);
      88              :     // Exactly `size` bytes appended to `buffer` (fewer only with an error)
      89              :     virtual awaitable<io_result> read(boost::asio::streambuf &buffer, size_t size);
      90              :     // Read into `buffer` until it contains `delim`, as boost::asio::async_read_until: the
      91              :     // bytes up to the end of the delimiter (the buffer may hold more); not_found if the
      92              :     // buffer gets full before
      93              :     virtual awaitable<io_result> read_until(boost::asio::streambuf &buffer, std::string_view delim);
      94              : 
      95              :     // write operations: all the bytes, or an error
      96              :     virtual awaitable<io_result> write(const uint8_t buffer[], size_t size) = 0;
      97              :     virtual awaitable<io_result> write(std::string_view str);
      98              :     virtual awaitable<io_result> write(const std::vector<boost::asio::const_buffer> &buffers);
      99              : 
     100              :     // Write what the socket accepts right now, without waiting: the bytes written, with
     101              :     // would_block if none, or operation_not_supported if the socket cannot write without
     102              :     // waiting (then nothing is written). Not to be mixed with a pending write.
     103              :     virtual size_t write_now(const std::vector<boost::asio::const_buffer> &buffers, boost::system::error_code &ec);
     104              : 
     105              :     // As write(buffers), in slices of WRITE_SLICE_SIZE (a socket buffer), `progress` called after
     106              :     // each one: a write deadline it moves tells a slow peer that keeps reading from one that
     107              :     // stopped, as nginx's send_timeout does. `buffers` is left with what was not written
     108              :     static constexpr size_t WRITE_SLICE_SIZE = 64 * 1024;
     109              :     awaitable<io_result> write_slices(std::vector<boost::asio::const_buffer> &buffers,
     110              :                                       const sync_callback<void()> &progress);
     111              :     // Drop the first `size` bytes of `buffers` (and the empty buffers they leave first)
     112              :     static void consume(std::vector<boost::asio::const_buffer> &buffers, size_t size);
     113              : 
     114              :     // wait (operation_not_supported by default: then an HTTP server on the socket does not
     115              :     // watch for a client leaving while a response is pending)
     116              :     virtual awaitable<boost::system::error_code> wait(boost::asio::socket_base::wait_type type);
     117              : 
     118              :     // Once the socket is readable (see wait()): why, without consuming anything. No error if
     119              :     // the peer sent data (it stays available to the next read), eof if it closed its sending
     120              :     // side (a FIN; over TLS a close_notify, or a FIN without one), the error of the connection
     121              :     // otherwise (a reset). operation_not_supported by default, as wait(). It may wait for the
     122              :     // rest of a TLS record
     123              :     virtual awaitable<boost::system::error_code> peer_closed();
     124              : 
     125              :     // some getters to check the state
     126              :     virtual bool is_open() const = 0;
     127              :     // false by default
     128              :     virtual bool is_secure() const;
     129              :     // Bytes that can be read without waiting (0 by default)
     130              :     virtual size_t available() const;
     131              :     // Bytes written that the peer has not taken yet, as the system counts them (over TCP not
     132              :     // acknowledged, over a Unix socket not read); 0 by default (unknown). It only falls as the
     133              :     // peer takes them, so a stalled write tells a slow reader from one that stopped reading
     134              :     virtual size_t send_backlog() const;
     135              :     // Without waiting: whether a read would complete now, with data, the end of the stream or
     136              :     // an error (e.g. on an idle connection, which should have nothing to read, the peer
     137              :     // closing it). False by default (unknown)
     138              :     virtual bool readable_now() const;
     139              :     // Addresses of the connection (empty by default)
     140              :     virtual std::string get_remote_ip() const;
     141              :     virtual std::string get_local_port() const;
     142              :     virtual std::string get_remote_port() const;
     143              :     // TLS details of the connection once its handshake is done: none for a plain socket (the
     144              :     // default), or before the handshake or once closed. A socket running on another one (a
     145              :     // websocket) answers for it, so the caller never casts
     146              :     virtual std::optional<tls_session> tls() const;
     147              :     // Whether a request for the host name `host` (its Host, port removed) is misdirected on
     148              :     // this connection: its TLS handshake chose another client certificate mode or certificate
     149              :     // than `host` gets (see tls_handshake_record::misdirected()). False without TLS (the default)
     150            0 :     virtual bool misdirected(std::string_view /*host*/) const { return false; }
     151              : 
     152              :     // other methods
     153              :     boost::asio::io_context &get_io_context() const;
     154              : 
     155              : protected:
     156              :     using native_handle_type = boost::asio::ip::tcp::socket::native_handle_type;
     157              : 
     158              :     // write_now() of a plain stream socket: a single non-blocking gathered send (the
     159              :     // buffers past the first 64 are left, as if the socket accepted no more)
     160              :     static size_t send_now(native_handle_type descriptor, const std::vector<boost::asio::const_buffer>& buffers,
     161              :                            boost::system::error_code& ec);
     162              : 
     163              :     // readable_now() of a plain stream socket: polled with no timeout (true if the poll fails)
     164              :     static bool poll_readable(native_handle_type descriptor);
     165              : 
     166              :     // send_backlog() of a plain stream socket: SIOCOUTQ on Linux, SO_NWRITE on macOS (0 if
     167              :     // neither tells, or on an error)
     168              :     static size_t output_queue(native_handle_type descriptor);
     169              : 
     170              :     // peer_closed() of a plain stream socket: a non-blocking peek of one byte, which leaves it
     171              :     // in the socket. No error with data (or nothing to read yet), eof at the end of the stream,
     172              :     // else the error of the socket
     173              :     static boost::system::error_code peek_closed(native_handle_type descriptor);
     174              : 
     175              :     boost::asio::io_context &io_context_;
     176              :     static std::atomic<unsigned long> connections;
     177              : };
     178              : 
     179              : namespace detail {
     180              : 
     181              :     // async_read_some() that a cancellation through `slot` ends alone, completing as
     182              :     // use_nothrow_awaitable does. A handler of its own, not bind_cancellation_slot(slot,
     183              :     // use_nothrow_awaitable): an echo of 128 KiB takes 4% more cycles with the binder (the same
     184              :     // instructions), even with a slot not connected; with this one, connected as it is, none.
     185              :     // For TCP and Unix: over TLS the binder is not slower (see ssl_socket::read_some_cancellable())
     186              :     template <typename Handler>
     187              :     struct on_slot_handler {
     188              :         Handler handler;
     189              :         boost::asio::cancellation_slot slot;
     190              : 
     191              :         using cancellation_slot_type = boost::asio::cancellation_slot;
     192        36707 :         cancellation_slot_type get_cancellation_slot() const noexcept { return slot; }
     193              : 
     194              :         using executor_type = boost::asio::associated_executor_t<Handler>;
     195        36707 :         executor_type get_executor() const noexcept { return boost::asio::get_associated_executor(handler); }
     196              : 
     197        36383 :         void operator()(boost::system::error_code ec, size_t bytes) { std::move(handler)(io_result{ec, bytes}); }
     198              :     };
     199              : 
     200              :     template <typename Stream>
     201        36707 :     awaitable<io_result> read_some_on_slot(Stream& stream, uint8_t* buffer, size_t max_size,
     202              :                                            boost::asio::cancellation_slot slot) {
     203              :         return boost::asio::async_initiate<const boost::asio::use_awaitable_t<>, void(io_result)>(
     204        73414 :             [&stream, buffer, max_size, slot](auto handler) {
     205        36707 :                 stream.async_read_some(boost::asio::buffer(buffer, max_size),
     206        36707 :                                        on_slot_handler<decltype(handler)>{std::move(handler), slot});
     207              :             },
     208        73414 :             boost::asio::use_awaitable);
     209              :     }
     210              : 
     211              : }
     212              : 
     213              : }
     214              : 
     215              : #endif
        

Generated by: LCOV version 2.0-1