cheatah
Source

stdlib/socket/socket.cpp

1// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).
2// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.
3#include "socket.hpp"
5#include <cerrno>
6#include <cstdlib>
7#include <cstring>
8#include <string>
9#include <system_error>
10#include <vector>
12#if defined(_WIN32)
13// Windows: the BSD socket API lives in Winsock2 (closesocket, WSAStartup, SOCKET type).
14#include <winsock2.h>
15#include <ws2tcpip.h>
16#pragma comment(lib, "ws2_32.lib")
17#else
18#include <netdb.h>
19#include <netinet/in.h>
20#include <netinet/tcp.h> // TCP_NODELAY, TCP_QUICKACK — the throughput-tuning options
21#include <sys/socket.h>
22#include <unistd.h>
23#endif
25// Suppress SIGPIPE on send() to a closed peer. Linux passes MSG_NOSIGNAL per-call; macOS/
26// BSD have no such flag and instead use the SO_NOSIGPIPE socket option (set in socket()).
27// Define MSG_NOSIGNAL to 0 where it's absent (macOS, Windows) so the send() calls stay portable.
28#ifndef MSG_NOSIGNAL
29#define MSG_NOSIGNAL 0
30#endif
32namespace cheatah::socket {
33namespace {
35#if defined(_WIN32)
36// Winsock must be initialized once per process before any socket call. A function-local
37// static does it lazily and tears it down at exit.
38void ensure_winsock() {
39 struct WinsockInit {
40 WinsockInit() { WSADATA d; WSAStartup(MAKEWORD(2, 2), &d); }
41 ~WinsockInit() { WSACleanup(); }
42 };
43 static WinsockInit init;
45SOCKET as_fd(long long fd) { return static_cast<SOCKET>(fd); }
46#else
47void ensure_winsock() {}
48int as_fd(long long fd) { return static_cast<int>(fd); }
49#endif
51// Resolve host:port to an IPv4 TCP address. Returns true and fills `out`/`len` on
52// success. Used by bind/connect so "localhost", dotted IPs, and DNS names all work.
53bool resolve(const std::string& host, long long port, sockaddr_storage& out, socklen_t& len) {
54 ensure_winsock();
55 addrinfo hints{};
56 hints.ai_family = AF_INET;
57 hints.ai_socktype = SOCK_STREAM;
58 hints.ai_protocol = IPPROTO_TCP;
59 const std::string service = std::to_string(port);
60 addrinfo* res = nullptr;
61 if (::getaddrinfo(host.c_str(), service.c_str(), &hints, &res) != 0 || res == nullptr) {
62 errno = EADDRNOTAVAIL;
63 return false;
64 }
65 std::memcpy(&out, res->ai_addr, res->ai_addrlen);
66 len = res->ai_addrlen;
67 ::freeaddrinfo(res);
68 return true;
71// The receive-buffer / send-buffer size we request on every connected stream socket. Bumping the
72// window off the kernel default is what lets a bulk download stay in flight instead of crawling one
73// TLS record per round-trip. 4 MiB is clamped down by the kernel to net.core.rmem_max, and is far
74// larger than any realistic bandwidth-delay product for the downloads this serves. See NOTES —
75// "TLS download throughput".
76constexpr int kStreamBufBytes = 4 * 1024 * 1024;
78// Apply the high-throughput options to a CONNECTED stream socket (best-effort — tuning never
79// changes correctness, so a failed setsockopt is silently ignored). Called from connect() and
80// accept() so every connected socket is tuned no matter the entry point:
81// TCP_NODELAY — disable Nagle; small control writes (the HTTP request, TLS records) go at once.
82// TCP_QUICKACK — send ACKs immediately instead of delaying them ~40 ms; without this a pure
83// download stalls in delayed-ACK slow start (the server's cwnd never ramps). It is
84// one-shot on Linux, so recv() re-arms it after every read.
85// SO_RCVBUF/SNDBUF — open the window so the peer can keep the pipe full.
86void tune_stream_socket(long long fd) {
87#if !defined(_WIN32)
88 int one = 1;
89#ifdef TCP_NODELAY
90 ::setsockopt(as_fd(fd), IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
91#endif
92#ifdef TCP_QUICKACK
93 ::setsockopt(as_fd(fd), IPPROTO_TCP, TCP_QUICKACK, &one, sizeof(one));
94#endif
95 int buf = kStreamBufBytes;
96 ::setsockopt(as_fd(fd), SOL_SOCKET, SO_RCVBUF, &buf, sizeof(buf));
97 ::setsockopt(as_fd(fd), SOL_SOCKET, SO_SNDBUF, &buf, sizeof(buf));
98#else
99 BOOL one = TRUE;
100 ::setsockopt(as_fd(fd), IPPROTO_TCP, TCP_NODELAY,
101 reinterpret_cast<const char*>(&one), sizeof(one));
102 int buf = kStreamBufBytes;
103 ::setsockopt(as_fd(fd), SOL_SOCKET, SO_RCVBUF,
104 reinterpret_cast<const char*>(&buf), sizeof(buf));
105 ::setsockopt(as_fd(fd), SOL_SOCKET, SO_SNDBUF,
106 reinterpret_cast<const char*>(&buf), sizeof(buf));
107#endif
110} // namespace
112long long socket() {
113 ensure_winsock();
114 const auto fd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
115#if defined(_WIN32)
116 if (fd == INVALID_SOCKET) return -1; // Winsock signals failure with INVALID_SOCKET
117#else
118#ifdef SO_NOSIGPIPE
119 // macOS/BSD: ask the kernel not to raise SIGPIPE on this socket (Linux uses
120 // MSG_NOSIGNAL per send() instead — SO_NOSIGPIPE isn't defined there).
121 if (fd >= 0) {
122 int on = 1;
123 ::setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &on, sizeof(on));
124 }
125#endif
126#endif
127 return static_cast<long long>(fd);
130long long set_reuseaddr(long long fd) {
131 int yes = 1;
132 // optval is const void* on POSIX, const char* on Winsock — char* converts to both.
133 return ::setsockopt(as_fd(fd), SOL_SOCKET, SO_REUSEADDR,
134 reinterpret_cast<const char*>(&yes), sizeof(yes));
137long long bind(long long fd, const std::string& host, long long port) {
138 sockaddr_storage addr{};
139 socklen_t len = 0;
140 if (!resolve(host, port, addr, len)) return -1;
141 return ::bind(as_fd(fd), reinterpret_cast<sockaddr*>(&addr), len);
144long long listen(long long fd, long long backlog) {
145 return ::listen(as_fd(fd), static_cast<int>(backlog));
148long long connect(long long fd, const std::string& host, long long port) {
149 sockaddr_storage addr{};
150 socklen_t len = 0;
151 if (!resolve(host, port, addr, len)) return -1;
152 const int rc = ::connect(as_fd(fd), reinterpret_cast<sockaddr*>(&addr), len);
153 if (rc == 0) {
154 tune_stream_socket(fd); // every connected client socket gets the throughput options
155 }
156 return rc;
159long long accept(long long fd) {
160 const auto c = ::accept(as_fd(fd), nullptr, nullptr);
161#if defined(_WIN32)
162 if (c == INVALID_SOCKET) return -1;
163#endif
164 if (static_cast<long long>(c) >= 0) {
165 tune_stream_socket(static_cast<long long>(c)); // and every accepted server socket
166 }
167 return static_cast<long long>(c);
170long long local_port(long long fd) {
171 sockaddr_storage addr{};
172 socklen_t len = sizeof(addr);
173 if (::getsockname(as_fd(fd), reinterpret_cast<sockaddr*>(&addr), &len) != 0) return -1;
174 // sin_port sits at the same offset for IPv4/IPv6, and we only ever make
175 // AF_INET sockets, so reading it back is safe.
176 return ntohs(reinterpret_cast<sockaddr_in*>(&addr)->sin_port);
179long long send(long long fd, const std::string& data) {
180 // send() returns ssize_t on POSIX, int on Winsock — long long holds both.
181 const long long n = ::send(as_fd(fd), data.data(),
182 static_cast<int>(data.size()), MSG_NOSIGNAL);
183 return n;
186long long sendall(long long fd, const std::string& data) {
187 std::size_t sent = 0;
188 while (sent < data.size()) {
189 const long long n = ::send(as_fd(fd), data.data() + sent,
190 static_cast<int>(data.size() - sent), MSG_NOSIGNAL);
191 if (n <= 0) return -1;
192 sent += static_cast<std::size_t>(n);
193 }
194 return 0;
197std::string recv(long long fd, long long bufsize) {
198 if (bufsize <= 0) return {};
199 // Read into a REUSED per-thread scratch buffer so we don't allocate + zero-fill a fresh
200 // `bufsize` string on every call (a 64 KiB memset per recv on the download hot path). Only the
201 // n bytes actually received are copied into the returned string.
202 static thread_local std::vector<char> scratch;
203 if (scratch.size() < static_cast<std::size_t>(bufsize)) {
204 scratch.resize(static_cast<std::size_t>(bufsize));
205 }
206 const long long n = ::recv(as_fd(fd), scratch.data(), static_cast<int>(bufsize), 0);
207 if (n <= 0) return {};
208 std::string buf(scratch.data(), static_cast<std::size_t>(n));
209#if !defined(_WIN32) && defined(TCP_QUICKACK)
210 // Re-arm quick-ACK: Linux clears it after each read, so without this the delayed-ACK stall
211 // creeps back mid-transfer and the server's window stops growing.
212 int one = 1;
213 ::setsockopt(as_fd(fd), IPPROTO_TCP, TCP_QUICKACK, &one, sizeof(one));
214#endif
215 return buf;
218long long close(long long fd) {
219#if defined(_WIN32)
220 return ::closesocket(as_fd(fd));
221#else
222 return ::close(as_fd(fd));
223#endif
226long long shutdown(long long fd) {
227 // Half-close both directions WITHOUT releasing the fd: a blocking recv() on
228 // ANOTHER thread returns immediately (EOF). Used to wake a reader for a clean
229 // shutdown; the fd is still owned by the caller and must be close()d afterwards.
230#if defined(_WIN32)
231 return ::shutdown(as_fd(fd), SD_BOTH);
232#else
233 return ::shutdown(as_fd(fd), SHUT_RDWR);
234#endif
237long long tcp_listen(const std::string& host, long long port, long long backlog) {
238 // If socket() fails (-1), the bind below fails too and we fall through to the
239 // error path — no separate early return needed.
240 long long fd = socket();
241 set_reuseaddr(fd);
242 if (bind(fd, host, port) != 0 || listen(fd, backlog) != 0) {
243 close(fd);
244 return -1;
245 }
246 return fd;
249long long tcp_connect(const std::string& host, long long port) {
250 long long fd = socket();
251 if (connect(fd, host, port) != 0) {
252 close(fd);
253 return -1;
254 }
255 return fd;
258long long udp_socket() {
259 ensure_winsock();
260 const auto fd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
261#if defined(_WIN32)
262 if (fd == INVALID_SOCKET) return -1;
263#else
264 if (fd < 0) return -1;
265#endif
266 return static_cast<long long>(fd);
269long long sendto(long long fd, const std::string& host, long long port, const std::string& data) {
270 sockaddr_storage addr{};
271 socklen_t len = 0;
272 if (!resolve(host, port, addr, len)) return -1;
273#if defined(_WIN32)
274 const int flags = 0;
275#else
276#ifdef MSG_NOSIGNAL
277 const int flags = MSG_NOSIGNAL;
278#else
279 const int flags = 0;
280#endif
281#endif
282 const auto n = ::sendto(as_fd(fd), data.data(), static_cast<int>(data.size()), flags,
283 reinterpret_cast<const sockaddr*>(&addr), len);
284 return n < 0 ? -1 : static_cast<long long>(n);
287std::string recvfrom(long long fd, long long bufsize, std::string& out_host, long long& out_port) {
288 out_host.clear();
289 out_port = 0;
290 if (bufsize <= 0) return {};
291 static thread_local std::vector<char> scratch;
292 if (scratch.size() < static_cast<std::size_t>(bufsize)) {
293 scratch.resize(static_cast<std::size_t>(bufsize));
294 }
295 sockaddr_storage from{};
296 socklen_t from_len = sizeof from;
297 const auto n = ::recvfrom(as_fd(fd), scratch.data(), static_cast<int>(bufsize), 0,
298 reinterpret_cast<sockaddr*>(&from), &from_len);
299 if (n <= 0) return {};
300 char host[NI_MAXHOST] = {0};
301 char serv[NI_MAXSERV] = {0};
302 if (::getnameinfo(reinterpret_cast<const sockaddr*>(&from), from_len, host, sizeof host, serv,
303 sizeof serv, NI_NUMERICHOST | NI_NUMERICSERV) == 0) {
304 out_host = host;
305 char* end = nullptr;
306 const long long port = std::strtoll(serv, &end, 10);
307 out_port = (end != nullptr && *end == '\0') ? port : 0; // NI_NUMERICSERV is digits; be sure
308 }
309 return {scratch.data(), static_cast<std::size_t>(n)};
312long long set_timeout(long long fd, long long timeout_ms) {
313 ensure_winsock();
314#if defined(_WIN32)
315 const DWORD ms = timeout_ms > 0 ? static_cast<DWORD>(timeout_ms) : 0;
316 if (::setsockopt(as_fd(fd), SOL_SOCKET, SO_RCVTIMEO,
317 reinterpret_cast<const char*>(&ms), sizeof ms) != 0) return -1;
318 if (::setsockopt(as_fd(fd), SOL_SOCKET, SO_SNDTIMEO,
319 reinterpret_cast<const char*>(&ms), sizeof ms) != 0) return -1;
320#else
321 timeval tv{};
322 if (timeout_ms > 0) {
323 tv.tv_sec = timeout_ms / 1000;
324 tv.tv_usec = static_cast<suseconds_t>((timeout_ms % 1000) * 1000);
325 }
326 if (::setsockopt(as_fd(fd), SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv) != 0) return -1;
327 if (::setsockopt(as_fd(fd), SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv) != 0) return -1;
328#endif
329 return 0;
332std::string last_error() {
333#if defined(_WIN32)
334 return "Winsock error " + std::to_string(::WSAGetLastError());
335#else
336 return std::generic_category().message(errno); // thread-safe strerror
337#endif
340// ---- owning RAII connections ----
341// Each method forwards to the fd-based free function above; the value the guards add is
342// deterministic close() on scope exit, so a `with` block cannot leak the fd.
344Conn& Conn::operator=(Conn&& other) noexcept {
345 if (this != &other) {
346 if (fd_ >= 0) cheatah::socket::close(fd_);
347 fd_ = other.fd_;
348 other.fd_ = -1;
349 }
350 return *this;
352Conn::~Conn() {
353 if (fd_ >= 0) cheatah::socket::close(fd_);
355long long Conn::send(const std::string& data) const { return cheatah::socket::send(fd_, data); }
356long long Conn::sendall(const std::string& data) const { return cheatah::socket::sendall(fd_, data); }
357std::string Conn::recv(long long bufsize) const { return cheatah::socket::recv(fd_, bufsize); }
358long long Conn::set_timeout(long long timeout_ms) const {
359 return cheatah::socket::set_timeout(fd_, timeout_ms);
361long long Conn::local_port() const { return cheatah::socket::local_port(fd_); }
362long long Conn::shutdown() const { return cheatah::socket::shutdown(fd_); }
363long long Conn::close() {
364 if (fd_ < 0) return -1;
365 const long long rc = cheatah::socket::close(fd_);
366 fd_ = -1;
367 return rc;
370Listener& Listener::operator=(Listener&& other) noexcept {
371 if (this != &other) {
372 if (fd_ >= 0) cheatah::socket::close(fd_);
373 fd_ = other.fd_;
374 other.fd_ = -1;
375 }
376 return *this;
378Listener::~Listener() {
379 if (fd_ >= 0) cheatah::socket::close(fd_);
381Conn Listener::accept() const { return Conn(cheatah::socket::accept(fd_)); }
382long long Listener::local_port() const { return cheatah::socket::local_port(fd_); }
383long long Listener::close() {
384 if (fd_ < 0) return -1;
385 const long long rc = cheatah::socket::close(fd_);
386 fd_ = -1;
387 return rc;
390Conn open(const std::string& host, long long port) { return Conn(tcp_connect(host, port)); }
391Listener serve(const std::string& host, long long port, long long backlog) {
392 return Listener(tcp_listen(host, port, backlog));
395} // namespace cheatah::socket