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,这也没有任何区别。

尝试(失败)的解决方案现在包括:

无论加载多长时间(通常为 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_tlru_purge_enable 设置为 true。所有 3 台设备现在都非常愉快地加载所有内容。我仍然很好奇是否有人知道为什么没有删除 http(连接?)并且需要清除。