Crafter.Network/interfaces/Crafter.Network-ClientTCP.cppm
Jorijn van der Graaf 16da0a1979
feat(tcp): a failed send or receive says which errno it failed with
Send() and the receives threw a bare std::runtime_error with the
strerror text, so a caller that reports the cause the way Python does -
"the connection broke ([Errno 104] Connection reset by peer)", which a
language model reads in Crafter.Review's SQL helper output - could not
tell the code. They throw SocketErrorException now, as a failed connect
already does: kind System, the errno, and the same message, still a
std::runtime_error. A receive timeout stays SocketTimeoutException and
a closed peer SocketClosedException.

ShouldReportIoErrors: a peer that resets the connection (SO_LINGER 0)
fails the next receive with ECONNRESET and a send into it with EPIPE
or ECONNRESET.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 20:28:32 +01:00

125 lines
No EOL
5.5 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";
}
};
// Where the code of a SocketErrorException comes from.
export enum class SocketErrorKind {
// An errno: ECONNREFUSED, ENETUNREACH, EMFILE, ...
System,
// A getaddrinfo result: EAI_NONAME, EAI_AGAIN, ...
Resolver,
};
// A resolve, a socket(), a connect, a send or a receive that failed, with
// the code it failed with. what() is the text ClientTCP has always thrown, and catching
// std::runtime_error still catches it; the code is for callers that
// report the cause in their own words — a Python-style
// "[Errno 111] Connection refused", say — and would otherwise have to
// parse it back out of the message.
export class SocketErrorException : public std::runtime_error {
public:
SocketErrorException(SocketErrorKind kind, std::int32_t code, const std::string& message)
: std::runtime_error(message), kind(kind), code(code) {}
SocketErrorKind kind;
std::int32_t code;
};
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