ESP32 网络服务器 slows/hangs 有超过 1 个设备访问页面
ESP32 webserver slows/hangs with more than 1 device accessing pages
我正在为 ESP32 编写网络服务器,主要用 C 语言编写,并在 VSCode 上使用 platformio 和 ESP-IDF。对于单个客户端,服务器运行良好,但一旦我尝试在多个设备上加载页面(无论所有页面相同还是不同),一切都会变慢。我可以看到页面请求很快到达服务器,但是这些页面 load/be 一分钟之内不会发送。
发生这种情况时,服务器似乎偏爱任意设备;例如,我的 android phone 会以正常速度加载页面,而我的 PC 和 iPhone 会卡住,或者在其他情况下 PC 会快速加载而其他两个设备卡住.这让我想知道它是否与我的 server/socket 处理任务有关,但 ESP-IDF 是一个如此复杂的依赖关系网,我不确定从哪里开始寻找。我在乐鑫论坛上发了post,但还没有任何回复。这不仅仅是 wifi 连接,就好像我连接了所有 3 台设备但只从一个设备访问网页,该设备仍然加载一切正常。
更新:
07/02 - 我越来越相信罪魁祸首是套接字任务,因为它似乎不允许同时使用多个套接字;在接受一个之后,它会处理那个套接字的所有事情,然后再循环并接受另一个套接字。
08/02 - 我修改了 tcp_server_task
为每个套接字启动一个新任务(下面是修改后的功能和附加任务),但这如果有什么让事情变得更糟的话,让我比以前更加困惑.
09/02 - 我尝试将 listen()
的积压参数从 1 增加到 32,这也没有任何区别。
尝试(失败)的解决方案现在包括:
将 keepAlive 设置为 0
将 dest_addr_ip4 从 INADDR_ANY 更改为 192.168.4.1,设备连接所需的地址
将套接字处理任务固定到与 tcp_server_task
不同的核心
在具有不同 x 值的不同点添加 vTaskDelay(pdMS_TO_TICKS(x))
无论加载多长时间(通常为 30 到 60 秒,有时更长),卡住的设备仍会持续同时加载
10/02 - 我忘了说,ESP 也启用了蓝牙,因为我们希望用户能够通过两种无线方法控制内容。
套接字处理任务:
// wifi_functions.c
void tcp_server_task(void *pvParameters)
{
char addr_str[128];
int addr_family = (int)pvParameters;
int ip_protocol = 0;
int keepAlive = 0;
int keepIdle = KEEPALIVE_IDLE;
int keepInterval = KEEPALIVE_INTERVAL;
int keepCount = KEEPALIVE_COUNT;
struct sockaddr_storage dest_addr;
if (addr_family == AF_INET) {
struct sockaddr_in *dest_addr_ip4 = (struct sockaddr_in *)&dest_addr;
dest_addr_ip4->sin_addr.s_addr = htonl(INADDR_ANY);
dest_addr_ip4->sin_family = AF_INET;
dest_addr_ip4->sin_port = htons(PORT);
ip_protocol = IPPROTO_IP;
}
#ifdef CONFIG_EXAMPLE_IPV6
else if (addr_family == AF_INET6) {
struct sockaddr_in6 *dest_addr_ip6 = (struct sockaddr_in6 *)&dest_addr;
bzero(&dest_addr_ip6->sin6_addr.un, sizeof(dest_addr_ip6->sin6_addr.un));
dest_addr_ip6->sin6_family = AF_INET6;
dest_addr_ip6->sin6_port = htons(PORT);
ip_protocol = IPPROTO_IPV6;
}
#endif
int listen_sock = socket(addr_family, SOCK_STREAM, ip_protocol);
if (listen_sock < 0) {
// ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
vTaskDelete(NULL);
return;
}
int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
#if defined(CONFIG_EXAMPLE_IPV4) && defined(CONFIG_EXAMPLE_IPV6)
// Note that by default IPV6 binds to both protocols, it is must be disabled
// if both protocols used at the same time (used in CI)
setsockopt(listen_sock, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
#endif
// ESP_LOGI(TAG, "Socket created");
int err = bind(listen_sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
if (err != 0) {
// ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
// ESP_LOGE(TAG, "IPPROTO: %d", addr_family);
goto CLEAN_UP;
}
// ESP_LOGI(TAG, "Socket bound, port %d", PORT);
err = listen(listen_sock, 1);
if (err != 0) {
// ESP_LOGE(TAG, "Error occurred during listen: errno %d", errno);
goto CLEAN_UP;
}
while (1) {
// ESP_LOGI("socket task", "Socket listening");
struct sockaddr_storage source_addr; // Large enough for both IPv4 or IPv6
socklen_t addr_len = sizeof(source_addr);
int sock = accept(listen_sock, (struct sockaddr *)&source_addr, &addr_len);
if (sock < 0) {
// ESP_LOGE(TAG, "Unable to accept connection: errno %d", errno);
break;
}
set_sock(sock);
// Set tcp keepalive option
setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keepAlive, sizeof(int));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, &keepIdle, sizeof(int));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepInterval, sizeof(int));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, &keepCount, sizeof(int));
// Convert ip address to string
if (source_addr.ss_family == PF_INET) {
inet_ntoa_r(((struct sockaddr_in *)&source_addr)->sin_addr, addr_str, sizeof(addr_str) - 1);
}
#ifdef CONFIG_EXAMPLE_IPV6
else if (source_addr.ss_family == PF_INET6) {
inet6_ntoa_r(((struct sockaddr_in6 *)&source_addr)->sin6_addr, addr_str, sizeof(addr_str) - 1);
}
#endif
// ESP_LOGI(TAG, "Socket accepted ip address: %s", addr_str);
xTaskCreate(socket_handler_task, "socket_handler", 10*1024, &sock, 10, NULL);
}
CLEAN_UP:
close(listen_sock);
vTaskDelete(NULL);
}
void socket_handler_task(void *params)
{
int sock = *((int*)params);
do_retransmit(sock);
shutdown(sock, 0);
close(sock);
vTaskDelete(NULL);
}
...
// main.c
xTaskCreatePinnedToCore(tcp_server_task, "tcp_server1", 4096, (void*)AF_INET, 10, NULL, 1);
更改优先级或切换到核心 0 似乎没有任何影响。
此函数是处理 file/page 请求的 hppt 服务器的一部分:
// file_server.c
/* Handler to download a file kept on the server */
static esp_err_t download_get_handler(httpd_req_t *req)
{
char filepath[FILE_PATH_MAX];
FILE *fd = NULL;
struct stat file_stat;
const char *filename = get_path_from_uri(filepath, ((struct file_server_data *)req->user_ctx)->base_path,
req->uri, sizeof(filepath));
esp_log_buffer_char_internal("path", filename, strlen(filename), ESP_LOG_INFO);
if (!filename) {
// ESP_LOGE(TAG, "Filename is too long");
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filename too long");
return ESP_FAIL;
}
if(strlen(filename) == 1 || strcmp(filename, "/index.html") == 0){
strcpy(filepath, "/spiffs/index.htm");
}
/* If name has trailing '/', respond with directory contents */
if (filename[strlen(filename) - 1] == '/') {
return http_resp_dir_html(req, filepath);
}
if(strcmp(filename, "/setup.txt") == 0){
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "This file is not accessible");
return ESP_FAIL;
}
if (stat(filepath, &file_stat) == -1) {
/* If file not present on SPIFFS check if URI
* corresponds to one of the hardcoded paths */
if (strcmp(filename, "/favicon.ico") == 0) {
return favicon_get_handler(req);
} else if (strcmp(filename, "/config") == 0) {
return config_get_handler(req);
} else if (strcmp(filename, "/rits") == 0) {
return rits_get_handler(req);
} else if (strcmp(filename, "/setup") == 0) {
return setup_get_handler(req);
} else if (strcmp(filename, "/access") == 0) {
return access_get_handler(req);
} else if (strcmp(filename, "/login") == 0) {
return login_get_handler(req);
} else if (strcmp(filename, "/check") == 0) {
return debug_get_handler(req);
} else if (strcmp(filename, "/funeral") == 0) {
return debug_get_handler(req);
} else if (strcmp(filename, "/imageEdit") == 0) {
// No need to do anything
return debug_get_handler(req);
} else if (strcmp(filename, "/restart") == 0) {
return restart_handler(req);
}
// ESP_LOGE(TAG, "Failed to stat file : %s", filepath);
/* Respond with 404 Not Found */
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File does not exist");
return ESP_FAIL;
}
fd = fopen(filepath, "r");
if (!fd) {
// ESP_LOGE(TAG, "Failed to read existing file : %s", filepath);
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read existing file");
return ESP_FAIL;
}
// ESP_LOGI(TAG, "Sending file : %s (%ld bytes)...", filename, file_stat.st_size);
set_content_type_from_file(req, filename);
/* Retrieve the pointer to scratch buffer for temporary storage */
char *chunk = ((struct file_server_data *)req->user_ctx)->scratch;
size_t chunksize;
do {
/* Read file in chunks into the scratch buffer */
chunksize = fread(chunk, 1, SCRATCH_BUFSIZE, fd);
if (chunksize > 0) {
/* Send the buffer contents as HTTP response chunk */
if (httpd_resp_send_chunk(req, chunk, chunksize) != ESP_OK) {
fclose(fd);
// ESP_LOGE(TAG, "File sending failed!");
/* Abort sending file */
httpd_resp_sendstr_chunk(req, NULL);
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file");
return ESP_FAIL;
}
}
/* Keep looping till the whole file is sent */
} while (chunksize != 0);
/* Close file after sending complete */
fclose(fd);
// ESP_LOGI(TAG, "File sending complete");
/* Respond with an empty chunk to signal HTTP response completion */
#ifdef CONFIG_EXAMPLE_HTTPD_CONN_CLOSE_HEADER
httpd_resp_set_hdr(req, "Connection", "close");
#endif
httpd_resp_send_chunk(req, NULL, 0);
return ESP_OK;
}
已修复!最终有效的解决方案实际上与套接字无关,我所做的只是将 httpd_config_t
的 lru_purge_enable
设置为 true
。所有 3 台设备现在都非常愉快地加载所有内容。我仍然很好奇是否有人知道为什么没有删除 http(连接?)并且需要清除。
我正在为 ESP32 编写网络服务器,主要用 C 语言编写,并在 VSCode 上使用 platformio 和 ESP-IDF。对于单个客户端,服务器运行良好,但一旦我尝试在多个设备上加载页面(无论所有页面相同还是不同),一切都会变慢。我可以看到页面请求很快到达服务器,但是这些页面 load/be 一分钟之内不会发送。
发生这种情况时,服务器似乎偏爱任意设备;例如,我的 android phone 会以正常速度加载页面,而我的 PC 和 iPhone 会卡住,或者在其他情况下 PC 会快速加载而其他两个设备卡住.这让我想知道它是否与我的 server/socket 处理任务有关,但 ESP-IDF 是一个如此复杂的依赖关系网,我不确定从哪里开始寻找。我在乐鑫论坛上发了post,但还没有任何回复。这不仅仅是 wifi 连接,就好像我连接了所有 3 台设备但只从一个设备访问网页,该设备仍然加载一切正常。
更新:
07/02 - 我越来越相信罪魁祸首是套接字任务,因为它似乎不允许同时使用多个套接字;在接受一个之后,它会处理那个套接字的所有事情,然后再循环并接受另一个套接字。
08/02 - 我修改了 tcp_server_task
为每个套接字启动一个新任务(下面是修改后的功能和附加任务),但这如果有什么让事情变得更糟的话,让我比以前更加困惑.
09/02 - 我尝试将 listen()
的积压参数从 1 增加到 32,这也没有任何区别。
尝试(失败)的解决方案现在包括:
将 keepAlive 设置为 0
将 dest_addr_ip4 从 INADDR_ANY 更改为 192.168.4.1,设备连接所需的地址
将套接字处理任务固定到与 tcp_server_task
不同的核心在具有不同 x 值的不同点添加 vTaskDelay(pdMS_TO_TICKS(x))
无论加载多长时间(通常为 30 到 60 秒,有时更长),卡住的设备仍会持续同时加载
10/02 - 我忘了说,ESP 也启用了蓝牙,因为我们希望用户能够通过两种无线方法控制内容。
套接字处理任务:
// wifi_functions.c
void tcp_server_task(void *pvParameters)
{
char addr_str[128];
int addr_family = (int)pvParameters;
int ip_protocol = 0;
int keepAlive = 0;
int keepIdle = KEEPALIVE_IDLE;
int keepInterval = KEEPALIVE_INTERVAL;
int keepCount = KEEPALIVE_COUNT;
struct sockaddr_storage dest_addr;
if (addr_family == AF_INET) {
struct sockaddr_in *dest_addr_ip4 = (struct sockaddr_in *)&dest_addr;
dest_addr_ip4->sin_addr.s_addr = htonl(INADDR_ANY);
dest_addr_ip4->sin_family = AF_INET;
dest_addr_ip4->sin_port = htons(PORT);
ip_protocol = IPPROTO_IP;
}
#ifdef CONFIG_EXAMPLE_IPV6
else if (addr_family == AF_INET6) {
struct sockaddr_in6 *dest_addr_ip6 = (struct sockaddr_in6 *)&dest_addr;
bzero(&dest_addr_ip6->sin6_addr.un, sizeof(dest_addr_ip6->sin6_addr.un));
dest_addr_ip6->sin6_family = AF_INET6;
dest_addr_ip6->sin6_port = htons(PORT);
ip_protocol = IPPROTO_IPV6;
}
#endif
int listen_sock = socket(addr_family, SOCK_STREAM, ip_protocol);
if (listen_sock < 0) {
// ESP_LOGE(TAG, "Unable to create socket: errno %d", errno);
vTaskDelete(NULL);
return;
}
int opt = 1;
setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
#if defined(CONFIG_EXAMPLE_IPV4) && defined(CONFIG_EXAMPLE_IPV6)
// Note that by default IPV6 binds to both protocols, it is must be disabled
// if both protocols used at the same time (used in CI)
setsockopt(listen_sock, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
#endif
// ESP_LOGI(TAG, "Socket created");
int err = bind(listen_sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
if (err != 0) {
// ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
// ESP_LOGE(TAG, "IPPROTO: %d", addr_family);
goto CLEAN_UP;
}
// ESP_LOGI(TAG, "Socket bound, port %d", PORT);
err = listen(listen_sock, 1);
if (err != 0) {
// ESP_LOGE(TAG, "Error occurred during listen: errno %d", errno);
goto CLEAN_UP;
}
while (1) {
// ESP_LOGI("socket task", "Socket listening");
struct sockaddr_storage source_addr; // Large enough for both IPv4 or IPv6
socklen_t addr_len = sizeof(source_addr);
int sock = accept(listen_sock, (struct sockaddr *)&source_addr, &addr_len);
if (sock < 0) {
// ESP_LOGE(TAG, "Unable to accept connection: errno %d", errno);
break;
}
set_sock(sock);
// Set tcp keepalive option
setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keepAlive, sizeof(int));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, &keepIdle, sizeof(int));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepInterval, sizeof(int));
setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, &keepCount, sizeof(int));
// Convert ip address to string
if (source_addr.ss_family == PF_INET) {
inet_ntoa_r(((struct sockaddr_in *)&source_addr)->sin_addr, addr_str, sizeof(addr_str) - 1);
}
#ifdef CONFIG_EXAMPLE_IPV6
else if (source_addr.ss_family == PF_INET6) {
inet6_ntoa_r(((struct sockaddr_in6 *)&source_addr)->sin6_addr, addr_str, sizeof(addr_str) - 1);
}
#endif
// ESP_LOGI(TAG, "Socket accepted ip address: %s", addr_str);
xTaskCreate(socket_handler_task, "socket_handler", 10*1024, &sock, 10, NULL);
}
CLEAN_UP:
close(listen_sock);
vTaskDelete(NULL);
}
void socket_handler_task(void *params)
{
int sock = *((int*)params);
do_retransmit(sock);
shutdown(sock, 0);
close(sock);
vTaskDelete(NULL);
}
...
// main.c
xTaskCreatePinnedToCore(tcp_server_task, "tcp_server1", 4096, (void*)AF_INET, 10, NULL, 1);
更改优先级或切换到核心 0 似乎没有任何影响。
此函数是处理 file/page 请求的 hppt 服务器的一部分:
// file_server.c
/* Handler to download a file kept on the server */
static esp_err_t download_get_handler(httpd_req_t *req)
{
char filepath[FILE_PATH_MAX];
FILE *fd = NULL;
struct stat file_stat;
const char *filename = get_path_from_uri(filepath, ((struct file_server_data *)req->user_ctx)->base_path,
req->uri, sizeof(filepath));
esp_log_buffer_char_internal("path", filename, strlen(filename), ESP_LOG_INFO);
if (!filename) {
// ESP_LOGE(TAG, "Filename is too long");
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Filename too long");
return ESP_FAIL;
}
if(strlen(filename) == 1 || strcmp(filename, "/index.html") == 0){
strcpy(filepath, "/spiffs/index.htm");
}
/* If name has trailing '/', respond with directory contents */
if (filename[strlen(filename) - 1] == '/') {
return http_resp_dir_html(req, filepath);
}
if(strcmp(filename, "/setup.txt") == 0){
httpd_resp_send_err(req, HTTPD_403_FORBIDDEN, "This file is not accessible");
return ESP_FAIL;
}
if (stat(filepath, &file_stat) == -1) {
/* If file not present on SPIFFS check if URI
* corresponds to one of the hardcoded paths */
if (strcmp(filename, "/favicon.ico") == 0) {
return favicon_get_handler(req);
} else if (strcmp(filename, "/config") == 0) {
return config_get_handler(req);
} else if (strcmp(filename, "/rits") == 0) {
return rits_get_handler(req);
} else if (strcmp(filename, "/setup") == 0) {
return setup_get_handler(req);
} else if (strcmp(filename, "/access") == 0) {
return access_get_handler(req);
} else if (strcmp(filename, "/login") == 0) {
return login_get_handler(req);
} else if (strcmp(filename, "/check") == 0) {
return debug_get_handler(req);
} else if (strcmp(filename, "/funeral") == 0) {
return debug_get_handler(req);
} else if (strcmp(filename, "/imageEdit") == 0) {
// No need to do anything
return debug_get_handler(req);
} else if (strcmp(filename, "/restart") == 0) {
return restart_handler(req);
}
// ESP_LOGE(TAG, "Failed to stat file : %s", filepath);
/* Respond with 404 Not Found */
httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "File does not exist");
return ESP_FAIL;
}
fd = fopen(filepath, "r");
if (!fd) {
// ESP_LOGE(TAG, "Failed to read existing file : %s", filepath);
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read existing file");
return ESP_FAIL;
}
// ESP_LOGI(TAG, "Sending file : %s (%ld bytes)...", filename, file_stat.st_size);
set_content_type_from_file(req, filename);
/* Retrieve the pointer to scratch buffer for temporary storage */
char *chunk = ((struct file_server_data *)req->user_ctx)->scratch;
size_t chunksize;
do {
/* Read file in chunks into the scratch buffer */
chunksize = fread(chunk, 1, SCRATCH_BUFSIZE, fd);
if (chunksize > 0) {
/* Send the buffer contents as HTTP response chunk */
if (httpd_resp_send_chunk(req, chunk, chunksize) != ESP_OK) {
fclose(fd);
// ESP_LOGE(TAG, "File sending failed!");
/* Abort sending file */
httpd_resp_sendstr_chunk(req, NULL);
/* Respond with 500 Internal Server Error */
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file");
return ESP_FAIL;
}
}
/* Keep looping till the whole file is sent */
} while (chunksize != 0);
/* Close file after sending complete */
fclose(fd);
// ESP_LOGI(TAG, "File sending complete");
/* Respond with an empty chunk to signal HTTP response completion */
#ifdef CONFIG_EXAMPLE_HTTPD_CONN_CLOSE_HEADER
httpd_resp_set_hdr(req, "Connection", "close");
#endif
httpd_resp_send_chunk(req, NULL, 0);
return ESP_OK;
}
已修复!最终有效的解决方案实际上与套接字无关,我所做的只是将 httpd_config_t
的 lru_purge_enable
设置为 true
。所有 3 台设备现在都非常愉快地加载所有内容。我仍然很好奇是否有人知道为什么没有删除 http(连接?)并且需要清除。