You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ESP32以太网HTTPS下载大文件连接断开,求解决方法

ESP32以太网HTTPS大文件下载断开问题

我在ESP32设备上通过以太网(未使用WiFi),采用HTTP 1.1协议发起HTTPS的GET请求。下载极小文件(25字节)时可成功完成,但文件尺寸增大至20KB或2MB时,连接会很快断开,无法完成下载。以下是我的代码:

#include <EthernetUdp.h>

#include <EthernetLarge.h>
#include <SSLClient.h>

// Choose the analog pin to get semi-random data from for SSL
// Pick a pin that's not connected or attached to a randomish voltage source
const int rand_pin = A5;
 
// Initialize the SSL client library
// We input an EthernetClient, our trust anchors, and the analog pin
EthernetClient base_client;
SSLClient client(base_client, TAs, (size_t)TAs_NUM, rand_pin);

bool NanoEthernet::httpsGet(
    char const *hostname, char const *path, 
    byte *buffer, size_t bufferCapacity, 
    size_t &bodyLength, bool &bufferOverflows,
    void (*downloadChunkCallback)(byte * buffer, size_t bufferReadCount, size_t contentLength)
    ) {

    log("Time is: %d %d %d %02d:%02d:%02d", day(), month(), year(), hour(), minute(), second());

    char const * subModule = "httpsGet: ";
    log("%sbegin %s%s…",subModule, hostname, path);
    
    // === if you get a connection, report back via serial:
    if (!client.connect(hostname, 443)) {
        // if you didn't get a connection to the server:
        error("%sconnection failed", subModule);
        return false;
    }

    success("%sConnected!",subModule);
    // Make a HTTP request:
    client.printf("GET %s HTTP/1.1", path);             client.println();
    client.printf("Host: %s", hostname);                client.println();
    // client.println("Connection: keep-alive");            client.println();
    // client.println("Keep-Alive: timeout=5, max=100");    client.println();
    //client.println("Connection: close");
    client.println();

    //while(client.connected() && !client.available()) delay(1); //waits for data

    // success("%sSent query headers (connected:%d)",
    //  subModule,
    //  client.connected()
    // );

    
    /// Body counter
    int bodyCounter = 0;

    /// Chunk counter
    int chunkReadCount = 0;

    /// Whether to read response header?
    bool readResponseHeader = true;
    byte *p = buffer;

    int contentLength = 0;

    const unsigned timeoutNoData = 10*1000; // 10 seconds
    unsigned long long lastTimeNoData = millis();

    unsigned waitForConnection = 10;

    while (true) {
        // log("%sResponse: connected: %d | readResponseHeader: %d", 
        //  subModule,
        //  client.connected(),
        //  readResponseHeader
        // );
        if (!client.connected()) {
            delay(1);
            error("%sNo more connected, aborting", subModule);
            if (waitForConnection-- <= 0)
                continue;
            else
                break;
        }

        if (!client.available()) {
            delay(1);

            // == Still reading the header then continue, otherwise hanle timeout
            if (!readResponseHeader) {
                if (millis() - lastTimeNoData >= timeoutNoData) {
                    if (contentLength==0) {
                        log("%sNo more data are available after %d seconds timeout", 
                            subModule,
                            timeoutNoData/1000
                        );
                    }
                    else { 
                        error("%sNo more data are available timeout", subModule);
                    }
                    // == Stopping the connection
                    break;
                }
            }

            //log(">.");
            continue;
        }   
        lastTimeNoData = millis();

    
        // = Skip response header
        if (readResponseHeader) {
            const String header = client.readStringUntil('\n'); // End of line is "\r\n"

            log("%sResponse headers: %s", subModule, header.c_str());

            const int nextData = client.peek();
            // End of stream?
            if (nextData==-1) {
                error("%sEnd of stream (received data -1) while reading headers");
                break;
            }

            const char nextChar = char(nextData);
            //log("Response headers: => next char is %d 0x%02x (%c)", nextChar, nextChar, nextChar);
            // == End of headers? (empty line)      
            if (nextChar == '\r') {
                // Actually read the end of line
                client.read();  // eat \r
                client.read();  // eat \n
                readResponseHeader=false;
            }
            else {

                char const *headerString = "Content-Length: ";
                const int headerStringLength = 16;
                if (header.startsWith(headerString)) {
                    const String sContentLength = header.substring(headerStringLength);
                    contentLength = header.substring(headerStringLength).toInt();
                    //log("Read Content-Length: string: '%s'| int: %d bytes", sContentLength.c_str(), byteCountToRead);
                }
            }
            continue;
        }

        const int data = client.read(); //gets byte from ethernet buffer
        // == End of stream?
        if (data==-1) {
            log("%sEnd of stream (data received: -1)",subModule);
            break;
        }

        // == Keep the actual byte
        const byte b = byte(data);

        #if LOG_LEVEL >= LOG_LEVEL_DEBUG
        //log(">%02x (%d)", b, bodyCounter);
        log(">%02x ", b);
        #endif

        *p++ = b;
        bodyCounter++;
        chunkReadCount++;

        // = When no chunk callback, overflow means stop reading
        if (downloadChunkCallback==NULL) {
            // == If buffer overflows, stop
            if (bodyCounter >= bufferCapacity) {

                bufferOverflows = true;
                break;
            }
        }
        // = But when chunk callback has been provided, pass it the 
        // chunk, rewind the buffer and continue
        else {
                // == If buffer overflows, stop
            if (chunkReadCount >= bufferCapacity) {
                downloadChunkCallback(buffer, chunkReadCount, contentLength);
                // Now rewind the chunk to the satrt of the buffer prior to 
                // continue reading the reponsse body
                chunkReadCount = 0;
                p = buffer;
            }
        }

        // == We read everything?
        if (contentLength>0) {
            if (bodyCounter>=contentLength) {
                log("%sEnd of stream (received ontentLength bytes)", subModule);
                break;
            }
        }
    }
    

    // = When chunk callback, pass it the potential remaining chunk
    if(downloadChunkCallback!=NULL 
    && chunkReadCount>0) {
        downloadChunkCallback(buffer, chunkReadCount, contentLength);
    }

    // == Write the length we read
    bodyLength = bodyCounter;
    
    #if LOG_LEVEL >= LOG_LEVEL_DEBUG
    log("");
    log("%s: did the get and read %d bytes", subModule, bodyCounter);
    #endif
    
    client.stop();
    return true;    
}

问题排查与修复方案

1. 修正连接状态判断逻辑

当前代码中!client.connected()的处理逻辑反转,导致连接断开时错误地无限重试或提前退出。修正为:

if (!client.connected()) {
    delay(1);
    if (waitForConnection-- <= 0) {
        error("%sNo more connected, aborting", subModule);
        break;
    }
    continue;
}

重试10次后仍未恢复连接则退出循环,避免无效等待。

2. 批量读取数据,提升处理效率

单字节读取client.read()会导致IO效率极低,TCP接收窗口被填满后服务器会停止发送甚至断开连接。改用批量读取:

// 替换原单字节读取的代码块
size_t availableBytes = client.available();
if (availableBytes == 0) continue;

// 计算可读取的最大字节数,避免缓冲区溢出
size_t readSize = min(availableBytes, bufferCapacity - chunkReadCount);
size_t bytesRead = client.readBytes(p, readSize);

if (bytesRead == 0) {
    log("%sEnd of stream", subModule);
    break;
}

p += bytesRead;
bodyCounter += bytesRead;
chunkReadCount += bytesRead;

3. 显式关闭HTTP持久连接

HTTP/1.1默认启用长连接,ESP32的SSLClient在长连接下易因超时或资源不足断开。添加Connection: close请求头:

// 在请求头中添加以下行
client.println("Connection: close");

让服务器在传输完成后主动关闭连接,避免长连接超时问题。

4. 调整SSL接收缓冲区大小

SSLClient默认缓冲区较小,大文件下载时容易溢出。初始化时指定更大的接收缓冲区(根据ESP32内存情况调整,示例为4KB):

// 修改SSLClient初始化代码
SSLClient client(base_client, TAs, (size_t)TAs_NUM, rand_pin, 4096);

5. 修复Content-Length类型与解析

将contentLength改为size_t类型(支持更大文件尺寸),并使用无符号整数解析:

// 替换原contentLength定义
size_t contentLength = 0;

// 替换原解析代码
if (header.startsWith(headerString)) {
    contentLength = header.substring(headerStringLength).toUInt();
}

6. 减少调试输出

调试输出会占用大量CPU时间,导致数据处理不及时。生产环境关闭LOG_LEVEL_DEBUG或移除不必要的日志打印。


内容的提问来源于stack exchange,提问作者Stéphane de Luca

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 10:57:03