diff --git a/components/socket/include/socket.hpp b/components/socket/include/socket.hpp index b350430a2..b6b5506f3 100644 --- a/components/socket/include/socket.hpp +++ b/components/socket/include/socket.hpp @@ -2,8 +2,11 @@ #include "socket_msvc.hpp" -#ifdef _MSC_VER -typedef unsigned int sock_type_t; +#ifdef _WIN32 +/* Windows SOCKET is UINT_PTR (pointer-sized on 64-bit); use the real type so + * handles aren't truncated. SOCKET comes from , included above via + * socket_msvc.hpp. */ +typedef SOCKET sock_type_t; #else /* Assume that any non-Windows platform uses POSIX-style sockets instead. */ #include @@ -18,6 +21,7 @@ typedef int sock_type_t; #include #include #include +#include #include #include @@ -177,6 +181,64 @@ class Socket : public BaseComponent { */ bool set_receive_timeout(const std::chrono::duration &timeout); + /** + * @brief Generic wrapper around setsockopt() so callers don't have to touch + * the raw native handle (or worry about the Windows const char* cast). + * @param level protocol level of the option (e.g. SOL_SOCKET, IPPROTO_IP). + * @param option_name option to set (e.g. SO_RCVBUF). + * @param value pointer to the option value. + * @param size size of the option value in bytes. + * @return true if setsockopt() succeeded, false otherwise (logs on failure). + */ + bool set_option(int level, int option_name, const void *value, size_t size); + + /** + * @brief Convenience wrapper around set_option() for a trivially-copyable + * option value. + * @param level protocol level of the option (e.g. SOL_SOCKET). + * @param option_name option to set (e.g. SO_RCVBUF). + * @param value option value; its address and size are forwarded to + * setsockopt(). + * @return true if setsockopt() succeeded, false otherwise (logs on failure). + */ + template bool set_option(int level, int option_name, const T &value) { + static_assert(std::is_trivially_copyable_v, + "set_option forwards the raw object bytes to setsockopt(); T must be " + "trivially copyable"); + return set_option(level, option_name, &value, sizeof(value)); + } + + /** + * @brief Set the size of the kernel receive buffer (SO_RCVBUF). + * @note The kernel may clamp or double the requested value. + * @param bytes requested receive buffer size in bytes. + * @return true if SO_RCVBUF was successfully set. + */ + bool set_receive_buffer_size(size_t bytes); + + /** + * @brief Set the size of the kernel send buffer (SO_SNDBUF). + * @note The kernel may clamp or double the requested value. + * @param bytes requested send buffer size in bytes. + * @return true if SO_SNDBUF was successfully set. + */ + bool set_send_buffer_size(size_t bytes); + + /** + * @brief Set (or clear) SO_REUSEADDR on the socket. + * @note Unlike enable_reuse()/disable_reuse(), this only touches SO_REUSEADDR + * (not SO_REUSEPORT / SO_BROADCAST). + * @param enable true to allow address reuse, false to disallow it. + * @return true if SO_REUSEADDR was successfully set. + */ + bool set_reuse_address(bool enable); + + /** + * @brief Get the size of the kernel receive buffer (SO_RCVBUF). + * @return the receive buffer size in bytes, or std::nullopt on failure. + */ + std::optional get_receive_buffer_size(); + /** * @brief Allow others to use this address/port combination after we're done * with it. @@ -264,6 +326,15 @@ class Socket : public BaseComponent { */ void cleanup(); +#ifdef _WIN32 + /** + * @brief Initialize Winsock (WSAStartup) exactly once for the process. + * @note Thread-safe: uses std::call_once so concurrent Socket construction + * cannot race the one-time initialization. + */ + void initialize_winsock(); +#endif + static constexpr int address_family_{AF_INET}; static constexpr int ip_protocol_{IPPROTO_IP}; diff --git a/components/socket/include/socket_msvc.hpp b/components/socket/include/socket_msvc.hpp index f63882b29..30dd78681 100644 --- a/components/socket/include/socket_msvc.hpp +++ b/components/socket/include/socket_msvc.hpp @@ -1,4 +1,4 @@ -#ifdef _MSC_VER +#ifdef _WIN32 extern "C" { // if we don't define NOMINMAX, windows.h will define min and max as macros // which will conflict with std::min and std::max diff --git a/components/socket/include/tcp_socket.hpp b/components/socket/include/tcp_socket.hpp index b2583138c..5a5aca2d3 100644 --- a/components/socket/include/tcp_socket.hpp +++ b/components/socket/include/tcp_socket.hpp @@ -2,9 +2,9 @@ #include "socket_msvc.hpp" -#ifndef _MSC_VER +#ifndef _WIN32 #include -#endif // _MSC_VER +#endif // _WIN32 #include #include diff --git a/components/socket/src/socket.cpp b/components/socket/src/socket.cpp index 71d9abfbd..584ba1e27 100644 --- a/components/socket/src/socket.cpp +++ b/components/socket/src/socket.cpp @@ -1,5 +1,8 @@ #include "socket.hpp" +#include +#include + using namespace espp; void Socket::Info::init_ipv4(const std::string &addr, size_t prt) { @@ -73,35 +76,34 @@ void Socket::Info::from_sockaddr(const struct sockaddr_in6 &source_address) { } #endif // !defined(ESP_PLATFORM) || LWIP_IPV6 -[[maybe_unused]] static bool _socket_initialized = false; -Socket::Socket(sock_type_t socket_fd, const Logger::Config &logger_config) - : BaseComponent(logger_config) { -#ifdef _MSC_VER - if (!_socket_initialized) { +#ifdef _WIN32 +// Ensure Winsock is initialized exactly once, even if multiple Sockets are +// constructed concurrently (std::call_once serializes the check-then-act). +void Socket::initialize_winsock() { + static std::once_flag winsock_once; + std::call_once(winsock_once, [this]() { logger_.debug("Initializing Winsock"); WSADATA wsa_data; int err = WSAStartup(MAKEWORD(1, 1), &wsa_data); if (err != 0) { logger_.error("WSAStartup failed: {}", error_string(err)); } - _socket_initialized = true; - } + }); +} +#endif + +Socket::Socket(sock_type_t socket_fd, const Logger::Config &logger_config) + : BaseComponent(logger_config) { +#ifdef _WIN32 + initialize_winsock(); #endif socket_ = socket_fd; } Socket::Socket(Type type, const Logger::Config &logger_config) : BaseComponent(logger_config) { -#ifdef _MSC_VER - if (!_socket_initialized) { - logger_.debug("Initializing Winsock"); - WSADATA wsa_data; - int err = WSAStartup(MAKEWORD(1, 1), &wsa_data); - if (err != 0) { - logger_.error("WSAStartup failed: {}", error_string(err)); - } - _socket_initialized = true; - } +#ifdef _WIN32 + initialize_winsock(); #endif init(type); } @@ -109,7 +111,7 @@ Socket::Socket(Type type, const Logger::Config &logger_config) Socket::~Socket() { cleanup(); } bool Socket::is_valid() const { -#ifdef _MSC_VER +#ifdef _WIN32 return socket_ != INVALID_SOCKET; #else return socket_ >= 0; @@ -117,7 +119,7 @@ bool Socket::is_valid() const { } bool Socket::is_valid_fd(sock_type_t socket_fd) { -#ifdef _MSC_VER +#ifdef _WIN32 return socket_fd != INVALID_SOCKET; #else return socket_fd >= 0; @@ -162,6 +164,70 @@ bool Socket::set_receive_timeout(const std::chrono::duration &timeout) { return true; } +bool Socket::set_option(int level, int option_name, const void *value, size_t size) { + // setsockopt's optlen is a signed int (Windows) / socklen_t; guard against a + // size_t that would narrow/overflow when cast. + if (size > static_cast(INT_MAX)) { + logger_.error("set_option size too large (level={}, option={}): {} > {}", level, option_name, + size, INT_MAX); + return false; + } +#if defined(_WIN32) + int err = setsockopt(socket_, level, option_name, reinterpret_cast(value), + static_cast(size)); +#else + int err = setsockopt(socket_, level, option_name, value, static_cast(size)); +#endif + if (err < 0) { + logger_.error("Couldn't set socket option (level={}, option={}): {}", level, option_name, + error_string()); + return false; + } + return true; +} + +bool Socket::set_receive_buffer_size(size_t bytes) { + // SO_RCVBUF takes an int; reject values that would overflow the cast. + if (bytes > static_cast(INT_MAX)) { + logger_.error("set_receive_buffer_size too large: {} > {}", bytes, INT_MAX); + return false; + } + int value = static_cast(bytes); + return set_option(SOL_SOCKET, SO_RCVBUF, value); +} + +bool Socket::set_send_buffer_size(size_t bytes) { + // SO_SNDBUF takes an int; reject values that would overflow the cast. + if (bytes > static_cast(INT_MAX)) { + logger_.error("set_send_buffer_size too large: {} > {}", bytes, INT_MAX); + return false; + } + int value = static_cast(bytes); + return set_option(SOL_SOCKET, SO_SNDBUF, value); +} + +bool Socket::set_reuse_address(bool enable) { + int value = enable ? 1 : 0; + return set_option(SOL_SOCKET, SO_REUSEADDR, value); +} + +std::optional Socket::get_receive_buffer_size() { + int value = 0; +#if defined(_WIN32) + // Winsock's getsockopt takes the optlen as int*, not socklen_t*. + int len = sizeof(value); + int err = getsockopt(socket_, SOL_SOCKET, SO_RCVBUF, reinterpret_cast(&value), &len); +#else + socklen_t len = sizeof(value); + int err = getsockopt(socket_, SOL_SOCKET, SO_RCVBUF, &value, &len); +#endif + if (err < 0) { + logger_.error("Couldn't get SO_RCVBUF: {}", error_string()); + return {}; + } + return static_cast(value); +} + bool Socket::disable_reuse() { #if !CONFIG_LWIP_SO_REUSE && defined(ESP_PLATFORM) // reuse is not compiled into lwip, so it is already effectively disabled @@ -175,14 +241,14 @@ bool Socket::disable_reuse() { fmt::print(fg(fmt::color::red), "Couldn't clear SO_REUSEADDR: {}\n", error_string()); return false; } -#if !defined(ESP_PLATFORM) && !defined(_MSC_VER) +#if !defined(ESP_PLATFORM) && !defined(_WIN32) err = setsockopt(socket_, SOL_SOCKET, SO_REUSEPORT, reinterpret_cast(&disabled), sizeof(disabled)); if (err < 0) { fmt::print(fg(fmt::color::red), "Couldn't clear SO_REUSEPORT: {}\n", error_string()); return false; } -#endif // !defined(ESP_PLATFORM) && !defined(_MSC_VER) +#endif // !defined(ESP_PLATFORM) && !defined(_WIN32) return true; #endif // !CONFIG_LWIP_SO_REUSE && defined(ESP_PLATFORM) } @@ -201,7 +267,7 @@ bool Socket::enable_reuse() { return false; } #if !defined(ESP_PLATFORM) -#ifdef _MSC_VER +#ifdef _WIN32 // NOTE: according to stackoverflow, we have to set broadcast instead of reuseport err = setsockopt(socket_, SOL_SOCKET, SO_BROADCAST, reinterpret_cast(&enabled), sizeof(enabled)); @@ -216,7 +282,7 @@ bool Socket::enable_reuse() { fmt::print(fg(fmt::color::red), "Couldn't set SO_REUSEPORT: {}\n", error_string()); return false; } -#endif // _MSC_VER +#endif // _WIN32 #endif // !defined(ESP_PLATFORM) return true; #endif // !CONFIG_LWIP_SO_REUSE && defined(ESP_PLATFORM) @@ -233,7 +299,7 @@ static bool resolve_interface_address(const std::string &interface_address, stru #endif return true; } -#ifdef _MSC_VER +#ifdef _WIN32 return inet_pton(AF_INET, interface_address.c_str(), &out) == 1; #else return inet_aton(interface_address.c_str(), &out) == 1; @@ -297,11 +363,11 @@ bool Socket::add_multicast_group(const std::string &multicast_group, #if defined(ESP_PLATFORM) err = inet_aton(multicast_group.c_str(), &imreq.imr_multiaddr.s_addr); #else -#ifdef _MSC_VER +#ifdef _WIN32 err = inet_pton(AF_INET, multicast_group.c_str(), &imreq.imr_multiaddr); #else err = inet_aton(multicast_group.c_str(), &imreq.imr_multiaddr); -#endif // _MSC_VER +#endif // _WIN32 #endif // defined(ESP_PLATFORM) if (err != 1 || !IN_MULTICAST(ntohl(imreq.imr_multiaddr.s_addr))) { @@ -373,7 +439,7 @@ bool Socket::init(Socket::Type type) { } std::string Socket::error_string() const { -#ifdef _MSC_VER +#ifdef _WIN32 int err = WSAGetLastError(); return error_string(err); #else @@ -382,7 +448,7 @@ std::string Socket::error_string() const { } std::string Socket::error_string(int err) const { -#ifdef _MSC_VER +#ifdef _WIN32 if (err == WSAEWOULDBLOCK) { return "WSAEWOULDBLOCK"; } else if (err == WSAECONNRESET) { @@ -410,7 +476,7 @@ std::string Socket::error_string(int err) const { void Socket::cleanup() { if (is_valid()) { auto socket_fd = socket_; -#ifdef _MSC_VER +#ifdef _WIN32 socket_ = INVALID_SOCKET; int status = shutdown(socket_fd, SD_BOTH); if (status != 0) { diff --git a/components/socket/src/socket_reactor.cpp b/components/socket/src/socket_reactor.cpp index b4ad3c0d6..337245ec4 100644 --- a/components/socket/src/socket_reactor.cpp +++ b/components/socket/src/socket_reactor.cpp @@ -3,7 +3,7 @@ #include #include -#ifndef _MSC_VER +#ifndef _WIN32 #include #endif @@ -26,7 +26,7 @@ struct DispatchGuard { // Close a socket with the platform-correct call (Winsock sockets must use // closesocket(), not ::close()). void close_socket(sock_type_t fd) { -#if defined(_MSC_VER) +#if defined(_WIN32) closesocket(fd); #else ::close(fd); @@ -144,7 +144,7 @@ bool SocketReactor::check_fd(sock_type_t fd) const { logger_.error("register: invalid socket fd"); return false; } -#if !defined(_MSC_VER) +#if !defined(_WIN32) // The select() backend uses fd_set, a bitmap indexed by fd value on // POSIX/lwip; FD_SET(fd) with fd >= FD_SETSIZE is undefined behavior. (On // Winsock, fd_set is a bounded array of SOCKETs, so the value is not the @@ -458,7 +458,7 @@ bool SocketReactor::create_wakeup_socket() { logger_.error("Could not create wakeup socket"); return false; } -#if !defined(_MSC_VER) +#if !defined(_WIN32) // The wakeup fd is FD_SET into the select set every iteration, so it too must // be below FD_SETSIZE (see check_fd). It is created before any registrations, // so on lwip it takes a low-offset fd - but guard anyway. @@ -505,7 +505,7 @@ void SocketReactor::close_wakeup_socket() { } bool SocketReactor::set_nonblocking(sock_type_t fd) { -#if defined(_MSC_VER) +#if defined(_WIN32) u_long mode = 1; return ioctlsocket(fd, FIONBIO, &mode) == 0; #else diff --git a/components/socket/src/udp_socket.cpp b/components/socket/src/udp_socket.cpp index a09858963..b0b38cbfd 100644 --- a/components/socket/src/udp_socket.cpp +++ b/components/socket/src/udp_socket.cpp @@ -5,14 +5,14 @@ using namespace espp; namespace { -#ifdef _MSC_VER +#ifdef _WIN32 int last_socket_error() { return WSAGetLastError(); } #else int last_socket_error() { return errno; } #endif bool is_transient_send_error(int err) { -#ifdef _MSC_VER +#ifdef _WIN32 return err == WSAEWOULDBLOCK || err == WSAENOBUFS; #else return err == EAGAIN || err == EWOULDBLOCK || err == ENOBUFS || err == ENOMEM;