//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // ClientTCP's socket options: // - SetNoDelay turns Nagle's algorithm off, so a small message goes out at // once instead of waiting for the ACK of the one before it, which a client // streaming small frames needs. // - A receive timeout makes a receive that waits too long throw // SocketTimeoutException. // - A connect timeout does the same for a connect nobody answers, where // connect() would otherwise sit through the kernel's SYN retries, about // two minutes. // Before, a ClientTCP had none of these, so a stalled peer blocked the // reading thread for good. // // Everything runs over loopback. The unanswered connect goes to a listener // whose accept queue is full: Linux drops the SYNs of further connects to it. #include #include #include #include #include #include import Crafter.Network; import std; using namespace Crafter; using namespace std::chrono_literals; namespace { int failures = 0; void Check(bool ok, std::string_view what) { std::println("{} {}", ok ? "PASS" : "FAIL", what); if (!ok) ++failures; } // A socket listening on 127.0.0.1, on a port the kernel picks. struct LoopbackListener { int fd = -1; std::uint16_t port = 0; explicit LoopbackListener(int backlog) { fd = socket(AF_INET, SOCK_STREAM, 0); sockaddr_in address {}; address.sin_family = AF_INET; address.sin_addr.s_addr = htonl(INADDR_LOOPBACK); bind(fd, reinterpret_cast(&address), sizeof(address)); listen(fd, backlog); socklen_t length = sizeof(address); getsockname(fd, reinterpret_cast(&address), &length); port = ntohs(address.sin_port); } ~LoopbackListener() { close(fd); } }; long long Milliseconds(std::chrono::steady_clock::duration duration) { return std::chrono::duration_cast(duration).count(); } } // namespace int main() { // ── SetNoDelay ─────────────────────────────────────────────────────────── { LoopbackListener listener(4); ClientTCP client("127.0.0.1", listener.port); auto noDelay = [&] { int value = 0; socklen_t size = sizeof(value); getsockopt(client.socketid, IPPROTO_TCP, TCP_NODELAY, &value, &size); return value != 0; }; Check(!noDelay(), "a new connection uses Nagle's algorithm"); client.SetNoDelay(true); Check(noDelay(), "SetNoDelay(true) turns it off"); client.SetNoDelay(false); Check(!noDelay(), "SetNoDelay(false) turns it back on"); } // ── Receive timeout ────────────────────────────────────────────────────── { LoopbackListener listener(4); ClientTCP client("127.0.0.1", listener.port); const int peer = accept(listener.fd, nullptr, nullptr); client.SetReceiveTimeout(150ms); const auto start = std::chrono::steady_clock::now(); bool timedOut = false; try { client.RecieveUntilFullSync(16); } catch (const SocketTimeoutException&) { timedOut = true; } const auto waited = std::chrono::steady_clock::now() - start; Check(timedOut, "a receive from a peer that sends nothing throws SocketTimeoutException"); Check(waited >= 140ms && waited < 1s, std::format("after about the timeout ({} ms)", Milliseconds(waited))); send(peer, "hello", 5, 0); const std::vector received = client.RecieveUntilFullSync(5); Check(std::string_view(received.data(), received.size()) == "hello", "the connection still receives what arrives after a timeout"); close(peer); } // ── Connect timeout ────────────────────────────────────────────────────── { // Connects queue up until the accept queue is full, which with a // backlog of 0 is after one or two; the next one goes unanswered. LoopbackListener full(0); std::vector queued; bool timedOut = false; std::chrono::steady_clock::duration waited {}; for (int i = 0; i < 8 && !timedOut; ++i) { const auto start = std::chrono::steady_clock::now(); try { queued.emplace_back("127.0.0.1", full.port, 200ms); } catch (const SocketTimeoutException&) { timedOut = true; waited = std::chrono::steady_clock::now() - start; } } Check(timedOut, std::format("a connect nobody answers throws SocketTimeoutException (after {} queued)", queued.size())); Check(waited >= 190ms && waited < 1s, std::format("after about the timeout ({} ms)", Milliseconds(waited))); } // ── A refused connect still fails at once ──────────────────────────────── { std::uint16_t closedPort = 0; { LoopbackListener gone(1); closedPort = gone.port; } const auto start = std::chrono::steady_clock::now(); bool refused = false; try { ClientTCP client("127.0.0.1", closedPort, 2s); } catch (const SocketTimeoutException&) { } catch (const std::runtime_error&) { refused = true; } Check(refused && std::chrono::steady_clock::now() - start < 1s, "a connect to a closed port fails at once, not after the timeout"); } std::println("{}", failures == 0 ? "all checks passed" : "check(s) failed"); return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; }