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:
@@ -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()) {
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -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")
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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) {
|
||||
|
@@ -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);
|
||||
|
||||
|
Reference in New Issue
Block a user