150 lines
6 KiB
C++
150 lines
6 KiB
C++
|
|
//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 <arpa/inet.h>
|
||
|
|
#include <netinet/in.h>
|
||
|
|
#include <netinet/tcp.h>
|
||
|
|
#include <sys/socket.h>
|
||
|
|
#include <unistd.h>
|
||
|
|
#include <cstdlib>
|
||
|
|
|
||
|
|
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<sockaddr*>(&address), sizeof(address));
|
||
|
|
listen(fd, backlog);
|
||
|
|
socklen_t length = sizeof(address);
|
||
|
|
getsockname(fd, reinterpret_cast<sockaddr*>(&address), &length);
|
||
|
|
port = ntohs(address.sin_port);
|
||
|
|
}
|
||
|
|
~LoopbackListener() { close(fd); }
|
||
|
|
};
|
||
|
|
|
||
|
|
long long Milliseconds(std::chrono::steady_clock::duration duration) {
|
||
|
|
return std::chrono::duration_cast<std::chrono::milliseconds>(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<char> 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<ClientTCP> 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;
|
||
|
|
}
|