//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module; #ifndef CRAFTER_NETWORK_BROWSER #include #include #include #include #include #include #include #include #include #include #include #include #include #endif export module Crafter.Network:ClientTCP; import std; #ifndef CRAFTER_NETWORK_BROWSER namespace Crafter { export class SocketClosedException : public std::exception { public: const char* what() const noexcept override { return "Socket closed"; } }; // A connect or a receive waited longer than its timeout. export class SocketTimeoutException : public std::exception { public: const char* what() const noexcept override { return "Socket timed out"; } }; // The step of a connection a SocketErrorException failed at. export enum class SocketPhase { // getaddrinfo on the host name. Resolve, // socket() for an address the host resolved to. Open, // connect() to that address. Connect, Send, Receive, }; // The error category of getaddrinfo's results (EAI_NONAME, EAI_AGAIN, // ...): a number space of its own that overlaps the errno values, so a // resolver code is never confused with an errno, and whose message() is // gai_strerror's text. One instance for the process, like // std::generic_category(). export const std::error_category& ResolverCategory() noexcept; // A resolve, a socket(), a connect, a send or a receive that failed. A // std::system_error, so still a std::runtime_error for the handlers that // catch one: code() is the errno under std::generic_category(), or the // getaddrinfo result under ResolverCategory(), and what() is the text // ClientTCP has always thrown, the code's own text after it. `phase` // says which step failed. A caller that reports the cause in its own // words — "[Errno 111] Connection refused", say — takes code().value() // and code().message() instead of parsing what(), and compares code() // with std::errc::connection_refused instead of the number. export class SocketErrorException : public std::system_error { public: SocketErrorException(SocketPhase phase, std::error_code code, const std::string& what) : std::system_error(code, what), phase(phase) {} // Without a message of its own: what() is the code's text alone. SocketErrorException(SocketPhase phase, std::error_code code) : std::system_error(code), phase(phase) {} SocketPhase phase; }; export class ClientTCP { public: int socketid; ClientTCP(int socket); // The host is resolved for IPv4 and IPv6 alike and every address it // resolves to is tried in turn, the way Python's // socket.create_connection does: `localhost` reaches a server that // listens on ::1 only, as a dev server on a recent Node does. The // first address that connects wins; when none does, what the last // one failed with is thrown — SocketErrorException, or // SocketTimeoutException for a timeout. // // A non-zero connectTimeout makes a connect nobody answers throw // SocketTimeoutException after that long, where it would otherwise // wait out the kernel's retries, about two minutes. It bounds each // address on its own. A refused connect fails at once either way. ClientTCP(const char* host, std::uint16_t port, std::chrono::milliseconds connectTimeout = std::chrono::milliseconds::zero()); ClientTCP(std::string host, std::uint16_t port, std::chrono::milliseconds connectTimeout = std::chrono::milliseconds::zero()); ~ClientTCP(); ClientTCP(const ClientTCP&) = delete; ClientTCP(ClientTCP&& other) noexcept; void Connect(std::chrono::milliseconds timeout = std::chrono::milliseconds::zero()); void Stop(); // Turns Nagle's algorithm off (TCP_NODELAY), so a small write goes // out at once instead of waiting for the ACK of the one before it. // For small messages that must arrive promptly, such as a stream of // frames. void SetNoDelay(bool enabled); // A receive that waits longer than this throws // SocketTimeoutException, losing what a whole-buffer receive had read // so far. Zero, the default, waits for good. void SetReceiveTimeout(std::chrono::milliseconds timeout); void Send(const void* buffer, std::uint32_t size) const; // The receives return what arrived, at least one byte; asked for none, // none, at once, without looking at the socket. They throw // SocketClosedException once the peer has closed, and // SocketTimeoutException when a receive timeout runs out. std::vector RecieveSync() const; std::vector RecieveUntilCloseSync() const; std::vector RecieveUntilFullSync(std::uint32_t bufferSize) const; std::vector RecieveSync(std::uint32_t bufferSize) const; // Into `buffer`; returns how many bytes it put there. std::uint32_t RecieveSync(std::uint32_t bufferSize, void* buffer) const; // The same receives on Crafter.Thread's ThreadPool. What arrived goes // to the callback; what a receive throws goes to onError instead (an // empty one drops it), as an exception left to escape a pool task // would end the program. void RecieveAsync(std::function)> recieveCallback, std::function onError) const; void RecieveUntilCloseAsync(std::function)> recieveCallback, std::function onError) const; void RecieveAsync(std::uint32_t bufferSize, std::function)> recieveCallback, std::function onError) const; void RecieveUntilFullAsync(std::uint32_t bufferSize, std::function)> recieveCallback, std::function onError) const; // Into `buffer`, which must outlive the receive; the callback gets the count. void RecieveAsync(std::uint32_t bufferSize, std::function recieveCallback, char* buffer, std::function onError) const; private: struct Address { sockaddr_storage storage; socklen_t length; }; // What the host resolved to, in getaddrinfo's order; empty for a // socket handed over by accept(). std::vector
addresses; }; } #endif