1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-25 06:32:22 +01:00

Optimize socket operations by checking readiness in the main loop (#8918)

This commit is contained in:
J. Nick Koston
2025-05-28 18:16:37 -05:00
committed by GitHub
parent ffc66f539f
commit 43e88af28a
13 changed files with 386 additions and 55 deletions

View File

@@ -135,31 +135,35 @@ void APIConnection::loop() {
api_error_to_str(err), errno);
return;
}
ReadPacketBuffer buffer;
err = this->helper_->read_packet(&buffer);
if (err == APIError::WOULD_BLOCK) {
// pass
} else if (err != APIError::OK) {
on_fatal_error();
if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) {
ESP_LOGW(TAG, "%s: Connection reset", this->client_combined_info_.c_str());
} else if (err == APIError::CONNECTION_CLOSED) {
ESP_LOGW(TAG, "%s: Connection closed", this->client_combined_info_.c_str());
} else {
ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err),
errno);
}
return;
} else {
this->last_traffic_ = App.get_loop_component_start_time();
// read a packet
if (buffer.data_len > 0) {
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
} else {
this->read_message(0, buffer.type, nullptr);
}
if (this->remove_)
// Check if socket has data ready before attempting to read
if (this->helper_->is_socket_ready()) {
ReadPacketBuffer buffer;
err = this->helper_->read_packet(&buffer);
if (err == APIError::WOULD_BLOCK) {
// pass
} else if (err != APIError::OK) {
on_fatal_error();
if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) {
ESP_LOGW(TAG, "%s: Connection reset", this->client_combined_info_.c_str());
} else if (err == APIError::CONNECTION_CLOSED) {
ESP_LOGW(TAG, "%s: Connection closed", this->client_combined_info_.c_str());
} else {
ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err),
errno);
}
return;
} else {
this->last_traffic_ = App.get_loop_component_start_time();
// read a packet
if (buffer.data_len > 0) {
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
} else {
this->read_message(0, buffer.type, nullptr);
}
if (this->remove_)
return;
}
}
if (!this->deferred_message_queue_.empty() && this->helper_->can_write_without_blocking()) {

View File

@@ -13,6 +13,7 @@
#include "api_noise_context.h"
#include "esphome/components/socket/socket.h"
#include "esphome/core/application.h"
namespace esphome {
namespace api {
@@ -90,6 +91,8 @@ class APIFrameHelper {
virtual uint8_t frame_header_padding() = 0;
// Get the frame footer size required by this protocol
virtual uint8_t frame_footer_size() = 0;
// Check if socket has data ready to read
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
protected:
// Struct for holding parsed frame data

View File

@@ -43,7 +43,7 @@ void APIServer::setup() {
}
#endif
this->socket_ = socket::socket_ip(SOCK_STREAM, 0);
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
if (this->socket_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket");
this->mark_failed();
@@ -112,18 +112,20 @@ void APIServer::setup() {
}
void APIServer::loop() {
// Accept new clients
while (true) {
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
auto sock = this->socket_->accept((struct sockaddr *) &source_addr, &addr_len);
if (!sock)
break;
ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str());
// Accept new clients only if the socket has incoming connections
if (this->socket_->ready()) {
while (true) {
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
auto sock = this->socket_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
if (!sock)
break;
ESP_LOGD(TAG, "Accepted %s", sock->getpeername().c_str());
auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn);
conn->start();
auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn);
conn->start();
}
}
// Process clients and remove disconnected ones in a single pass

View File

@@ -26,7 +26,7 @@ void ESPHomeOTAComponent::setup() {
ota::register_ota_platform(this);
#endif
server_ = socket::socket_ip(SOCK_STREAM, 0);
server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
if (server_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket");
this->mark_failed();
@@ -100,9 +100,12 @@ void ESPHomeOTAComponent::handle_() {
#endif
if (client_ == nullptr) {
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
client_ = server_->accept((struct sockaddr *) &source_addr, &addr_len);
// Check if the server socket is ready before accepting
if (this->server_->ready()) {
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);
client_ = server_->accept((struct sockaddr *) &source_addr, &addr_len);
}
}
if (client_ == nullptr)
return;

View File

@@ -35,5 +35,7 @@ async def to_code(config):
cg.add_define("USE_SOCKET_IMPL_LWIP_TCP")
elif impl == IMPLEMENTATION_LWIP_SOCKETS:
cg.add_define("USE_SOCKET_IMPL_LWIP_SOCKETS")
cg.add_define("USE_SOCKET_SELECT_SUPPORT")
elif impl == IMPLEMENTATION_BSD_SOCKETS:
cg.add_define("USE_SOCKET_IMPL_BSD_SOCKETS")
cg.add_define("USE_SOCKET_SELECT_SUPPORT")

View File

@@ -5,6 +5,7 @@
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
#include <cstring>
#include "esphome/core/application.h"
#ifdef USE_ESP32
#include <esp_idf_version.h>
@@ -40,7 +41,20 @@ std::string format_sockaddr(const struct sockaddr_storage &storage) {
class BSDSocketImpl : public Socket {
public:
BSDSocketImpl(int fd) : fd_(fd) {}
BSDSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
#ifdef USE_SOCKET_SELECT_SUPPORT
// Register new socket with the application for select() if monitoring requested
if (monitor_loop && fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds
loop_monitored_ = App.register_socket_fd(fd_);
} else {
loop_monitored_ = false;
}
#else
// Without select support, ignore monitor_loop parameter
(void) monitor_loop;
#endif
}
~BSDSocketImpl() override {
if (!closed_) {
close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall)
@@ -48,16 +62,35 @@ class BSDSocketImpl : public Socket {
}
int connect(const struct sockaddr *addr, socklen_t addrlen) override { return ::connect(fd_, addr, addrlen); }
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
return accept_impl_(addr, addrlen, false);
}
std::unique_ptr<Socket> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override {
return accept_impl_(addr, addrlen, true);
}
private:
std::unique_ptr<Socket> accept_impl_(struct sockaddr *addr, socklen_t *addrlen, bool loop_monitored) {
int fd = ::accept(fd_, addr, addrlen);
if (fd == -1)
return {};
return make_unique<BSDSocketImpl>(fd);
return make_unique<BSDSocketImpl>(fd, loop_monitored);
}
public:
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return ::bind(fd_, addr, addrlen); }
int close() override {
int ret = ::close(fd_);
closed_ = true;
return ret;
if (!closed_) {
#ifdef USE_SOCKET_SELECT_SUPPORT
// Unregister from select() before closing if monitored
if (loop_monitored_) {
App.unregister_socket_fd(fd_);
}
#endif
int ret = ::close(fd_);
closed_ = true;
return ret;
}
return 0;
}
int shutdown(int how) override { return ::shutdown(fd_, how); }
@@ -126,16 +159,27 @@ class BSDSocketImpl : public Socket {
return 0;
}
int get_fd() const override { return fd_; }
protected:
int fd_;
bool closed_ = false;
};
std::unique_ptr<Socket> socket(int domain, int type, int protocol) {
// Helper to create a socket with optional monitoring
static std::unique_ptr<Socket> create_socket(int domain, int type, int protocol, bool loop_monitored = false) {
int ret = ::socket(domain, type, protocol);
if (ret == -1)
return nullptr;
return std::unique_ptr<Socket>{new BSDSocketImpl(ret)};
return std::unique_ptr<Socket>{new BSDSocketImpl(ret, loop_monitored)};
}
std::unique_ptr<Socket> socket(int domain, int type, int protocol) {
return create_socket(domain, type, protocol, false);
}
std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol) {
return create_socket(domain, type, protocol, true);
}
} // namespace socket

View File

@@ -606,6 +606,11 @@ std::unique_ptr<Socket> socket(int domain, int type, int protocol) {
return std::unique_ptr<Socket>{sock};
}
std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol) {
// LWIPRawImpl doesn't use file descriptors, so monitoring is not applicable
return socket(domain, type, protocol);
}
} // namespace socket
} // namespace esphome

View File

@@ -5,6 +5,7 @@
#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS
#include <cstring>
#include "esphome/core/application.h"
namespace esphome {
namespace socket {
@@ -33,7 +34,20 @@ std::string format_sockaddr(const struct sockaddr_storage &storage) {
class LwIPSocketImpl : public Socket {
public:
LwIPSocketImpl(int fd) : fd_(fd) {}
LwIPSocketImpl(int fd, bool monitor_loop = false) : fd_(fd) {
#ifdef USE_SOCKET_SELECT_SUPPORT
// Register new socket with the application for select() if monitoring requested
if (monitor_loop && fd_ >= 0) {
// Only set loop_monitored_ to true if registration succeeds
loop_monitored_ = App.register_socket_fd(fd_);
} else {
loop_monitored_ = false;
}
#else
// Without select support, ignore monitor_loop parameter
(void) monitor_loop;
#endif
}
~LwIPSocketImpl() override {
if (!closed_) {
close(); // NOLINT(clang-analyzer-optin.cplusplus.VirtualCall)
@@ -41,16 +55,35 @@ class LwIPSocketImpl : public Socket {
}
int connect(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_connect(fd_, addr, addrlen); }
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
return accept_impl_(addr, addrlen, false);
}
std::unique_ptr<Socket> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) override {
return accept_impl_(addr, addrlen, true);
}
private:
std::unique_ptr<Socket> accept_impl_(struct sockaddr *addr, socklen_t *addrlen, bool loop_monitored) {
int fd = lwip_accept(fd_, addr, addrlen);
if (fd == -1)
return {};
return make_unique<LwIPSocketImpl>(fd);
return make_unique<LwIPSocketImpl>(fd, loop_monitored);
}
public:
int bind(const struct sockaddr *addr, socklen_t addrlen) override { return lwip_bind(fd_, addr, addrlen); }
int close() override {
int ret = lwip_close(fd_);
closed_ = true;
return ret;
if (!closed_) {
#ifdef USE_SOCKET_SELECT_SUPPORT
// Unregister from select() before closing if monitored
if (loop_monitored_) {
App.unregister_socket_fd(fd_);
}
#endif
int ret = lwip_close(fd_);
closed_ = true;
return ret;
}
return 0;
}
int shutdown(int how) override { return lwip_shutdown(fd_, how); }
@@ -98,16 +131,27 @@ class LwIPSocketImpl : public Socket {
return 0;
}
int get_fd() const override { return fd_; }
protected:
int fd_;
bool closed_ = false;
};
std::unique_ptr<Socket> socket(int domain, int type, int protocol) {
// Helper to create a socket with optional monitoring
static std::unique_ptr<Socket> create_socket(int domain, int type, int protocol, bool loop_monitored = false) {
int ret = lwip_socket(domain, type, protocol);
if (ret == -1)
return nullptr;
return std::unique_ptr<Socket>{new LwIPSocketImpl(ret)};
return std::unique_ptr<Socket>{new LwIPSocketImpl(ret, loop_monitored)};
}
std::unique_ptr<Socket> socket(int domain, int type, int protocol) {
return create_socket(domain, type, protocol, false);
}
std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol) {
return create_socket(domain, type, protocol, true);
}
} // namespace socket

View File

@@ -4,12 +4,35 @@
#include <cstring>
#include <string>
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace socket {
Socket::~Socket() {}
bool Socket::ready() const {
#ifdef USE_SOCKET_SELECT_SUPPORT
if (!loop_monitored_) {
// Non-monitored sockets always return true (assume data may be available)
return true;
}
// For loop-monitored sockets, check with the Application's select() results
int fd = this->get_fd();
if (fd < 0) {
// No valid file descriptor, assume ready (fallback behavior)
return true;
}
return App.is_socket_ready(fd);
#else
// Without select() support, we can't monitor sockets in the loop
// Always return true (assume data may be available)
return true;
#endif
}
std::unique_ptr<Socket> socket_ip(int type, int protocol) {
#if USE_NETWORK_IPV6
return socket(AF_INET6, type, protocol);
@@ -18,6 +41,14 @@ std::unique_ptr<Socket> socket_ip(int type, int protocol) {
#endif /* USE_NETWORK_IPV6 */
}
std::unique_ptr<Socket> socket_ip_loop_monitored(int type, int protocol) {
#if USE_NETWORK_IPV6
return socket_loop_monitored(AF_INET6, type, protocol);
#else
return socket_loop_monitored(AF_INET, type, protocol);
#endif /* USE_NETWORK_IPV6 */
}
socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port) {
#if USE_NETWORK_IPV6
if (ip_address.find(':') != std::string::npos) {

View File

@@ -17,6 +17,11 @@ class Socket {
Socket &operator=(const Socket &) = delete;
virtual std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) = 0;
/// Accept a connection and monitor it in the main loop
/// NOTE: This function is NOT thread-safe and must only be called from the main loop
virtual std::unique_ptr<Socket> accept_loop_monitored(struct sockaddr *addr, socklen_t *addrlen) {
return accept(addr, addrlen); // Default implementation for backward compatibility
}
virtual int bind(const struct sockaddr *addr, socklen_t addrlen) = 0;
virtual int close() = 0;
// not supported yet:
@@ -44,14 +49,35 @@ class Socket {
virtual int setblocking(bool blocking) = 0;
virtual int loop() { return 0; };
/// Get the underlying file descriptor (returns -1 if not supported)
virtual int get_fd() const { return -1; }
/// Check if socket has data ready to read
/// For loop-monitored sockets, checks with the Application's select() results
/// For non-monitored sockets, always returns true (assumes data may be available)
bool ready() const;
protected:
#ifdef USE_SOCKET_SELECT_SUPPORT
bool loop_monitored_{false}; ///< Whether this socket is monitored by the event loop
#endif
};
/// Create a socket of the given domain, type and protocol.
std::unique_ptr<Socket> socket(int domain, int type, int protocol);
/// Create a socket in the newest available IP domain (IPv6 or IPv4) of the given type and protocol.
std::unique_ptr<Socket> socket_ip(int type, int protocol);
/// Create a socket and monitor it for data in the main loop.
/// Like socket() but also registers the socket with the Application's select() loop.
/// WARNING: These functions are NOT thread-safe. They must only be called from the main loop
/// as they register the socket file descriptor with the global Application instance.
/// NOTE: On ESP platforms, FD_SETSIZE is typically 10, limiting the number of monitored sockets.
/// File descriptors >= FD_SETSIZE will not be monitored and will log an error.
std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol);
std::unique_ptr<Socket> socket_ip_loop_monitored(int type, int protocol);
/// Set a sockaddr to the specified address and port for the IP version used by socket_ip().
socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port);