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 <cstring>
7#include <string>
8#include <vector>
10#if defined(_WIN32)
11// Windows: the BSD socket API lives in Winsock2 (closesocket, WSAStartup, SOCKET type).
12#include <winsock2.h>
13#include <ws2tcpip.h>
14#pragma comment(lib, "ws2_32.lib")
15#else
16#include <netdb.h>
17#include <netinet/in.h>
18#include <netinet/tcp.h> // TCP_NODELAY, TCP_QUICKACK — the throughput-tuning options
19#include <sys/socket.h>
20#include <unistd.h>
21#endif
23// Suppress SIGPIPE on send() to a closed peer. Linux passes MSG_NOSIGNAL per-call; macOS/
24// BSD have no such flag and instead use the SO_NOSIGPIPE socket option (set in socket()).
25// Define MSG_NOSIGNAL to 0 where it's absent (macOS, Windows) so the send() calls stay portable.
26#ifndef MSG_NOSIGNAL
27#define MSG_NOSIGNAL 0
28#endif
30namespace cheatah::socket {
31namespace {
33#if defined(_WIN32)
34// Winsock must be initialized once per process before any socket call. A function-local
35// static does it lazily and tears it down at exit.
36void ensure_winsock() {
37 struct WinsockInit {
38 WinsockInit() { WSADATA d; WSAStartup(MAKEWORD(2, 2), &d); }
39 ~WinsockInit() { WSACleanup(); }
40 };
41 static WinsockInit init;
43SOCKET as_fd(long long fd) { return static_cast<SOCKET>(fd); }
44#else
45void ensure_winsock() {}
46int as_fd(long long fd) { return static_cast<int>(fd); }
47#endif
49// Resolve host:port to an IPv4 TCP address. Returns true and fills `out`/`len` on
50// success. Used by bind/connect so "localhost", dotted IPs, and DNS names all work.
51bool resolve(const std::string& host, long long port, sockaddr_storage& out, socklen_t& len) {
52 ensure_winsock();
53 addrinfo hints{};
54 hints.ai_family = AF_INET;
55 hints.ai_socktype = SOCK_STREAM;
56 hints.ai_protocol = IPPROTO_TCP;
57 const std::string service = std::to_string(port);
58 addrinfo* res = nullptr;
59 if (::getaddrinfo(host.c_str(), service.c_str(), &hints, &res) != 0 || res == nullptr) {
60 errno = EADDRNOTAVAIL;
61 return false;
62 }
63 std::memcpy(&out, res->ai_addr, res->ai_addrlen);
64 len = static_cast<socklen_t>(res->ai_addrlen);
65 ::freeaddrinfo(res);
66 return true;
69// The receive-buffer / send-buffer size we request on every connected stream socket. Bumping the
70// window off the kernel default is what lets a bulk download stay in flight instead of crawling one
71// TLS record per round-trip. 4 MiB is clamped down by the kernel to net.core.rmem_max, and is far
72// larger than any realistic bandwidth-delay product for the downloads this serves. See NOTES —
73// "TLS download throughput".
74constexpr int kStreamBufBytes = 4 * 1024 * 1024;
76// Apply the high-throughput options to a CONNECTED stream socket (best-effort — tuning never
77// changes correctness, so a failed setsockopt is silently ignored). Called from connect() and
78// accept() so every connected socket is tuned no matter the entry point:
79// TCP_NODELAY — disable Nagle; small control writes (the HTTP request, TLS records) go at once.
80// TCP_QUICKACK — send ACKs immediately instead of delaying them ~40 ms; without this a pure
81// download stalls in delayed-ACK slow start (the server's cwnd never ramps). It is
82// one-shot on Linux, so recv() re-arms it after every read.
83// SO_RCVBUF/SNDBUF — open the window so the peer can keep the pipe full.
84void tune_stream_socket(long long fd) {
85#if !defined(_WIN32)
86 int one = 1;
87#ifdef TCP_NODELAY
88 ::setsockopt(as_fd(fd), IPPROTO_TCP, TCP_NODELAY, &one, sizeof(one));
89#endif
90#ifdef TCP_QUICKACK
91 ::setsockopt(as_fd(fd), IPPROTO_TCP, TCP_QUICKACK, &one, sizeof(one));
92#endif
93 int buf = kStreamBufBytes;
94 ::setsockopt(as_fd(fd), SOL_SOCKET, SO_RCVBUF, &buf, sizeof(buf));
95 ::setsockopt(as_fd(fd), SOL_SOCKET, SO_SNDBUF, &buf, sizeof(buf));
96#else
97 BOOL one = TRUE;
98 ::setsockopt(as_fd(fd), IPPROTO_TCP, TCP_NODELAY,
99 reinterpret_cast<const char*>(&one), sizeof(one));
100 int buf = kStreamBufBytes;
101 ::setsockopt(as_fd(fd), SOL_SOCKET, SO_RCVBUF,
102 reinterpret_cast<const char*>(&buf), sizeof(buf));
103 ::setsockopt(as_fd(fd), SOL_SOCKET, SO_SNDBUF,
104 reinterpret_cast<const char*>(&buf), sizeof(buf));
105#endif
108} // namespace
110long long socket() {
111 ensure_winsock();
112 const auto fd = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
113#if defined(_WIN32)
114 if (fd == INVALID_SOCKET) return -1; // Winsock signals failure with INVALID_SOCKET
115#else
116#ifdef SO_NOSIGPIPE
117 // macOS/BSD: ask the kernel not to raise SIGPIPE on this socket (Linux uses
118 // MSG_NOSIGNAL per send() instead — SO_NOSIGPIPE isn't defined there).
119 if (fd >= 0) {
120 int on = 1;
121 ::setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &on, sizeof(on));
122 }
123#endif
124#endif
125 return static_cast<long long>(fd);
128long long set_reuseaddr(long long fd) {
129 int yes = 1;
130 // optval is const void* on POSIX, const char* on Winsock — char* converts to both.
131 return ::setsockopt(as_fd(fd), SOL_SOCKET, SO_REUSEADDR,
132 reinterpret_cast<const char*>(&yes), sizeof(yes));
135long long bind(long long fd, const std::string& host, long long port) {
136 sockaddr_storage addr{};
137 socklen_t len = 0;
138 if (!resolve(host, port, addr, len)) return -1;
139 return ::bind(as_fd(fd), reinterpret_cast<sockaddr*>(&addr), len);
142long long listen(long long fd, long long backlog) {
143 return ::listen(as_fd(fd), static_cast<int>(backlog));
146long long connect(long long fd, const std::string& host, long long port) {
147 sockaddr_storage addr{};
148 socklen_t len = 0;
149 if (!resolve(host, port, addr, len)) return -1;
150 const int rc = ::connect(as_fd(fd), reinterpret_cast<sockaddr*>(&addr), len);
151 if (rc == 0) {
152 tune_stream_socket(fd); // every connected client socket gets the throughput options
153 }
154 return rc;
157long long accept(long long fd) {
158 const auto c = ::accept(as_fd(fd), nullptr, nullptr);
159#if defined(_WIN32)
160 if (c == INVALID_SOCKET) return -1;
161#endif
162 if (static_cast<long long>(c) >= 0) {
163 tune_stream_socket(static_cast<long long>(c)); // and every accepted server socket
164 }
165 return static_cast<long long>(c);
168long long local_port(long long fd) {
169 sockaddr_storage addr{};
170 socklen_t len = sizeof(addr);
171 if (::getsockname(as_fd(fd), reinterpret_cast<sockaddr*>(&addr), &len) != 0) return -1;
172 // sin_port sits at the same offset for IPv4/IPv6, and we only ever make
173 // AF_INET sockets, so reading it back is safe.
174 return ntohs(reinterpret_cast<sockaddr_in*>(&addr)->sin_port);
177long long send(long long fd, const std::string& data) {
178 // send() returns ssize_t on POSIX, int on Winsock — long long holds both.
179 const long long n = ::send(as_fd(fd), data.data(),
180 static_cast<int>(data.size()), MSG_NOSIGNAL);
181 return n;
184long long sendall(long long fd, const std::string& data) {
185 std::size_t sent = 0;
186 while (sent < data.size()) {
187 const long long n = ::send(as_fd(fd), data.data() + sent,
188 static_cast<int>(data.size() - sent), MSG_NOSIGNAL);
189 if (n <= 0) return -1;
190 sent += static_cast<std::size_t>(n);
191 }
192 return 0;
195std::string recv(long long fd, long long bufsize) {
196 if (bufsize <= 0) return std::string();
197 // Read into a REUSED per-thread scratch buffer so we don't allocate + zero-fill a fresh
198 // `bufsize` string on every call (a 64 KiB memset per recv on the download hot path). Only the
199 // n bytes actually received are copied into the returned string.
200 static thread_local std::vector<char> scratch;
201 if (scratch.size() < static_cast<std::size_t>(bufsize)) {
202 scratch.resize(static_cast<std::size_t>(bufsize));
203 }
204 const long long n = ::recv(as_fd(fd), scratch.data(), static_cast<int>(bufsize), 0);
205 if (n <= 0) return std::string();
206 std::string buf(scratch.data(), static_cast<std::size_t>(n));
207#if !defined(_WIN32) && defined(TCP_QUICKACK)
208 // Re-arm quick-ACK: Linux clears it after each read, so without this the delayed-ACK stall
209 // creeps back mid-transfer and the server's window stops growing.
210 int one = 1;
211 ::setsockopt(as_fd(fd), IPPROTO_TCP, TCP_QUICKACK, &one, sizeof(one));
212#endif
213 return buf;
216long long close(long long fd) {
217#if defined(_WIN32)
218 return ::closesocket(as_fd(fd));
219#else
220 return ::close(as_fd(fd));
221#endif
224long long shutdown(long long fd) {
225 // Half-close both directions WITHOUT releasing the fd: a blocking recv() on
226 // ANOTHER thread returns immediately (EOF). Used to wake a reader for a clean
227 // shutdown; the fd is still owned by the caller and must be close()d afterwards.
228#if defined(_WIN32)
229 return ::shutdown(as_fd(fd), SD_BOTH);
230#else
231 return ::shutdown(as_fd(fd), SHUT_RDWR);
232#endif
235long long tcp_listen(const std::string& host, long long port, long long backlog) {
236 // If socket() fails (-1), the bind below fails too and we fall through to the
237 // error path — no separate early return needed.
238 long long fd = socket();
239 set_reuseaddr(fd);
240 if (bind(fd, host, port) != 0 || listen(fd, backlog) != 0) {
241 close(fd);
242 return -1;
243 }
244 return fd;
247long long tcp_connect(const std::string& host, long long port) {
248 long long fd = socket();
249 if (connect(fd, host, port) != 0) {
250 close(fd);
251 return -1;
252 }
253 return fd;
256long long set_timeout(long long fd, long long timeout_ms) {
257 ensure_winsock();
258#if defined(_WIN32)
259 const DWORD ms = timeout_ms > 0 ? static_cast<DWORD>(timeout_ms) : 0;
260 if (::setsockopt(as_fd(fd), SOL_SOCKET, SO_RCVTIMEO,
261 reinterpret_cast<const char*>(&ms), sizeof ms) != 0) return -1;
262 if (::setsockopt(as_fd(fd), SOL_SOCKET, SO_SNDTIMEO,
263 reinterpret_cast<const char*>(&ms), sizeof ms) != 0) return -1;
264#else
265 timeval tv{};
266 if (timeout_ms > 0) {
267 tv.tv_sec = timeout_ms / 1000;
268 tv.tv_usec = static_cast<suseconds_t>((timeout_ms % 1000) * 1000);
269 }
270 if (::setsockopt(as_fd(fd), SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv) != 0) return -1;
271 if (::setsockopt(as_fd(fd), SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv) != 0) return -1;
272#endif
273 return 0;
276std::string last_error() {
277#if defined(_WIN32)
278 return "Winsock error " + std::to_string(::WSAGetLastError());
279#else
280 return std::strerror(errno);
281#endif
284// ---- owning RAII connections ----
285// Each method forwards to the fd-based free function above; the value the guards add is
286// deterministic close() on scope exit, so a `with` block cannot leak the fd.
288Conn& Conn::operator=(Conn&& other) noexcept {
289 if (this != &other) {
290 if (fd_ >= 0) cheatah::socket::close(fd_);
291 fd_ = other.fd_;
292 other.fd_ = -1;
293 }
294 return *this;
296Conn::~Conn() {
297 if (fd_ >= 0) cheatah::socket::close(fd_);
299long long Conn::send(const std::string& data) { return cheatah::socket::send(fd_, data); }
300long long Conn::sendall(const std::string& data) { return cheatah::socket::sendall(fd_, data); }
301std::string Conn::recv(long long bufsize) { return cheatah::socket::recv(fd_, bufsize); }
302long long Conn::set_timeout(long long timeout_ms) {
303 return cheatah::socket::set_timeout(fd_, timeout_ms);
305long long Conn::local_port() const { return cheatah::socket::local_port(fd_); }
306long long Conn::shutdown() { return cheatah::socket::shutdown(fd_); }
307long long Conn::close() {
308 if (fd_ < 0) return -1;
309 const long long rc = cheatah::socket::close(fd_);
310 fd_ = -1;
311 return rc;
314Listener& Listener::operator=(Listener&& other) noexcept {
315 if (this != &other) {
316 if (fd_ >= 0) cheatah::socket::close(fd_);
317 fd_ = other.fd_;
318 other.fd_ = -1;
319 }
320 return *this;
322Listener::~Listener() {
323 if (fd_ >= 0) cheatah::socket::close(fd_);
325Conn Listener::accept() { return Conn(cheatah::socket::accept(fd_)); }
326long long Listener::local_port() const { return cheatah::socket::local_port(fd_); }
327long long Listener::close() {
328 if (fd_ < 0) return -1;
329 const long long rc = cheatah::socket::close(fd_);
330 fd_ = -1;
331 return rc;
334Conn open(const std::string& host, long long port) { return Conn(tcp_connect(host, port)); }
335Listener serve(const std::string& host, long long port, long long backlog) {
336 return Listener(tcp_listen(host, port, backlog));
339} // namespace cheatah::socket