SocketErrorException is now a std::system_error: code() carries the errno
under std::generic_category(), or getaddrinfo's result under the new
ResolverCategory(), whose message() is gai_strerror's text; `phase` says
which step failed (resolve, open, connect, send, receive). It is still a
std::runtime_error with the message ClientTCP always threw, the code's
text after it, so existing handlers keep working.
Before, the exception carried a `kind` and an int, and a caller that had
to print the cause its own way still had to match the message prefix to
learn where the failure happened. The standard type ends both: the
category knows how to print the number, and the phase is a field. A
caller writes "[Errno {}] {}" from code().value() and code().message(),
or compares code() with std::errc::connection_refused.
Breaking: SocketErrorException's `kind` and `code` members are gone;
read code().category() and code().value().
141 lines
No EOL
6.3 KiB
C++
Executable file
141 lines
No EOL
6.3 KiB
C++
Executable file
//SPDX-License-Identifier: LGPL-3.0-only
|
|
//SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
module;
|
|
#ifndef CRAFTER_NETWORK_BROWSER
|
|
#include <stdio.h>
|
|
#include <sys/types.h>
|
|
#include <sys/socket.h>
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
#include <stdlib.h>
|
|
#include <sys/uio.h>
|
|
#include <sys/time.h>
|
|
#include <sys/wait.h>
|
|
#include <sys/ioctl.h>
|
|
#include <netdb.h>
|
|
#include <strings.h>
|
|
#include <cerrno>
|
|
#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<char> RecieveSync() const;
|
|
std::vector<char> RecieveUntilCloseSync() const;
|
|
std::vector<char> RecieveUntilFullSync(std::uint32_t bufferSize) const;
|
|
std::vector<char> 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<void(std::vector<char>)> recieveCallback, std::function<void(std::exception_ptr)> onError) const;
|
|
void RecieveUntilCloseAsync(std::function<void(std::vector<char>)> recieveCallback, std::function<void(std::exception_ptr)> onError) const;
|
|
void RecieveAsync(std::uint32_t bufferSize, std::function<void(std::vector<char>)> recieveCallback, std::function<void(std::exception_ptr)> onError) const;
|
|
void RecieveUntilFullAsync(std::uint32_t bufferSize, std::function<void(std::vector<char>)> recieveCallback, std::function<void(std::exception_ptr)> onError) const;
|
|
// Into `buffer`, which must outlive the receive; the callback gets the count.
|
|
void RecieveAsync(std::uint32_t bufferSize, std::function<void(std::uint32_t)> recieveCallback, char* buffer, std::function<void(std::exception_ptr)> 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<Address> addresses;
|
|
};
|
|
}
|
|
#endif |