ESP32 网络HTTP数据流API使用浅析

1、背景

写的HTTP数据流下载不好用,特别在网速慢,任务多、中断切换频繁,出现下载中断,任务出错等等问题。

1.1 参考资料

 

2、常用API浅析

ESP-IDF对HTTP/S请求的API封装实在是太好了,单看一些API的说明,很难理清它的运行逻辑。因此还是阅读源码最能理解。

常用的API有esp_http_client_init()、esp_http_client_perform()、esp_http_client_open()、esp_http_client_write()、esp_http_client_fetch_header()、esp_http_client_read();

/**
 * @brief HTTP configuration
 */
typedef struct {
    const char                  *url;                /*!< HTTP URL, the information on the URL is most important, it overrides the other fields below, if any */
    const char                  *host;               /*!< Domain or IP as string */
    int                         port;                /*!< Port to connect, default depend on esp_http_client_transport_t (80 or 443) */
    const char                  *username;           /*!< Using for Http authentication */
    const char                  *password;           /*!< Using for Http authentication */
    esp_http_client_auth_type_t auth_type;           /*!< Http authentication type, see `esp_http_client_auth_type_t` */
    const char                  *path;               /*!< HTTP Path, if not set, default is `/` */
    const char                  *query;              /*!< HTTP query */
    const char                  *cert_pem;           /*!< SSL Certification, PEM format as string, if the client requires to verify server */
    esp_http_client_method_t    method;                   /*!< HTTP Method */
    int                         timeout_ms;               /*!< Network timeout in milliseconds */
    bool                        disable_auto_redirect;    /*!< Disable HTTP automatic redirects */
    int                         max_redirection_count;    /*!< Max redirection number, using default value if zero*/
    http_event_handle_cb        event_handler;             /*!< HTTP Event Handle */
    esp_http_client_transport_t transport_type;           /*!< HTTP transport type, see `esp_http_client_transport_t` */
    int                         buffer_size;              /*!< HTTP buffer size (both send and receive) */
    void                        *user_data;               /*!< HTTP user_data context */
    bool                        is_async;                 /*!< Set asynchronous mode, only supported with HTTPS for now */
} esp_http_client_config_t;

先看初始化API接口

2.1 esp_http_client_handle_t esp_http_client_init(const esp_http_client_config_t *config)

开始HTTP会话之前必须首先调用这个初始化函数。它的返回值esp_http_client_handle_t型变量作为其他HTTP API的实参用。

在所有操作完成后用esp_http_client_cleanup()来清除。

esp_http_client_handle_t esp_http_client_init(const esp_http_client_config_t *config)
{

    esp_http_client_handle_t client;
    esp_transport_handle_t tcp;
    bool _success;
//1、分配空间
    _success = (
                   (client                         = calloc(1, sizeof(esp_http_client_t)))           &&
                   (client->parser                 = calloc(1, sizeof(struct http_parser)))          &&
                   (client->parser_settings        = calloc(1, sizeof(struct http_parser_settings))) &&
                   (client->auth_data              = calloc(1, sizeof(esp_http_auth_data_t)))        &&
                   (client->request                = calloc(1, sizeof(esp_http_data_t)))             &&
                   (client->request->headers       = http_header_init())                             &&
                   (client->request->buffer        = calloc(1, sizeof(esp_http_buffer_t)))           &&
                   (client->response               = calloc(1, sizeof(esp_http_data_t)))             &&
                   (client->response->headers      = http_header_init())                             &&
                   (client->response->buffer       = calloc(1, sizeof(esp_http_buffer_t)))
               );

    if (!_success) {
        ESP_LOGE(TAG, "Error allocate memory");
        esp_http_client_cleanup(client);
        return NULL;
    }
//2、初始化transport 普通tcp,ssl_tcp
    _success = (
                   (client->transport_list = esp_transport_list_init()) &&
                   (tcp = esp_transport_tcp_init()) &&
                   (esp_transport_set_default_port(tcp, DEFAULT_HTTP_PORT) == ESP_OK) &&
                   (esp_transport_list_add(client->transport_list, tcp, "http") == ESP_OK)
               );
    if (!_success) {
        ESP_LOGE(TAG, "Error initialize transport");
        esp_http_client_cleanup(client);
        return NULL;
    }
#ifdef CONFIG_ESP_HTTP_CLIENT_ENABLE_HTTPS
    esp_transport_handle_t ssl;
    _success = (
                   (ssl = esp_transport_ssl_init()) &&
                   (esp_transport_set_default_port(ssl, DEFAULT_HTTPS_PORT) == ESP_OK) &&
                   (esp_transport_list_add(client->transport_list, ssl, "https") == ESP_OK)
               );

    if (!_success) {
        ESP_LOGE(TAG, "Error initialize SSL Transport");
        esp_http_client_cleanup(client);
        return NULL;
    }

    if (config->cert_pem) {
        esp_transport_ssl_set_cert_data(ssl, config->cert_pem, strlen(config->cert_pem));
    }
#endif
    //3、按照形参config来设置client
    if (_set_config(client, config) != ESP_OK) {
        ESP_LOGE(TAG, "Error set configurations");
        esp_http_client_cleanup(client);
        return NULL;
    }
    //4、分配请求和应答的buffer
    _success = (
                   (client->request->buffer->data  = malloc(client->buffer_size))  &&
                   (client->response->buffer->data = malloc(client->buffer_size))
               );

    if (!_success) {
        ESP_LOGE(TAG, "Allocation failed");
        esp_http_client_cleanup(client);
        return NULL;
    }
    //5、设置URL和请求头信息
    _success = (
                   (esp_http_client_set_url(client, config->url) == ESP_OK) &&
                   (esp_http_client_set_header(client, "User-Agent", DEFAULT_HTTP_USER_AGENT) == ESP_OK) &&
                   (esp_http_client_set_header(client, "Host", client->connection_info.host) == ESP_OK)
               );

    if (!_success) {
        ESP_LOGE(TAG, "Error set default configurations");
        esp_http_client_cleanup(client);
        return NULL;
    }
    //6、设置解析器
    client->parser_settings->on_message_begin = http_on_message_begin;
    client->parser_settings->on_url = http_on_url;
    client->parser_settings->on_status = http_on_status;
    client->parser_settings->on_header_field = http_on_header_field;
    client->parser_settings->on_header_value = http_on_header_value;
    client->parser_settings->on_headers_complete = http_on_headers_complete;
    client->parser_settings->on_body = http_on_body;
    client->parser_settings->on_message_complete = http_on_message_complete;
    client->parser_settings->on_chunk_complete = http_on_chunk_complete;
    client->parser->data = client;
    client->event.client = client;
    //7、设置HTTP状态为HTTP_STATE_INIT
    client->state = HTTP_STATE_INIT;
    return client;
}

client->state记录HTTP会话的状态。下面是这个状态变化的一个枚举类型

一般来说:是一个fall through的从0到7过程。

typedef enum {
    HTTP_STATE_UNINIT = 0,
    HTTP_STATE_INIT,
    HTTP_STATE_CONNECTED,
    HTTP_STATE_REQ_COMPLETE_HEADER,
    HTTP_STATE_REQ_COMPLETE_DATA,
    HTTP_STATE_RES_COMPLETE_HEADER,
    HTTP_STATE_RES_COMPLETE_DATA,
    HTTP_STATE_CLOSE
} esp_http_state_t;

2.2 esp_err_t esp_http_client_perform(esp_http_client_handle_t client)

该API以阻塞或非阻塞方式执行整个请求。默认情况下,API以阻塞的方式执行请求,并在完成时返回,或者在以非阻塞的方式下遇到EAGAIN/EWOULDBLOCK/EINPROGRESS返回。在非阻塞状态下的请求可以调用这个API多次除非这个请求&应答完成或失败。使能非阻塞的方法是设置esp_http_client_config_t配置项中的is_async.这样,你就可以对同一个esp_http-client_handle_t变量进行多次调用。如果服务器允许,底层连接可以保持为打开状态,我们鼓励传输多个文件。esp_http_client重用相同的连接,可以使操作更快、CPU更少,使用的网络资源更少。

注意:永远不要同时对同一个client句柄调用这个API。要想并行运行,请调用不同的client句柄。这个函数包括如下操作:esp_http_client_open->esp_http_client_write->esp_http_client_fetch_headers->esp_http_client_read或者esp_http_client_close


esp_err_t esp_http_client_perform(esp_http_client_handle_t client)
{
    esp_err_t err;
    do {
        if (client->process_again) {
            esp_http_client_prepare(client);
        }
        switch (client->state) {
        /* In case of blocking esp_http_client_perform(), the following states will fall through one after the after;
           in case of non-blocking esp_http_client_perform(), if there is an error condition, like EINPROGRESS or EAGAIN,
           then the esp_http_client_perform() API will return ESP_ERR_HTTP_EAGAIN error. The user may call
           esp_http_client_perform API again, and for this reason, we maintain the states */
            case HTTP_STATE_INIT:
                if ((err = esp_http_client_connect(client)) != ESP_OK) {
                    if (client->is_async && err == ESP_ERR_HTTP_CONNECTING) {
                        return ESP_ERR_HTTP_EAGAIN;
                    }
                    return err;
                }
                /* falls through */
            case HTTP_STATE_CONNECTED:
                if ((err = esp_http_client_request_send(client)) != ESP_OK) {
                    if (client->is_async && errno == EAGAIN) {
                        return ESP_ERR_HTTP_EAGAIN;
                    }
                    return err;
                }
                /* falls through */
            case HTTP_STATE_REQ_COMPLETE_HEADER:
                if ((err = esp_http_client_send_post_data(client)) != ESP_OK) {
                    if (client->is_async && errno == EAGAIN) {
                        return ESP_ERR_HTTP_EAGAIN;
                    }
                    return err;
                }
                /* falls through */
            case HTTP_STATE_REQ_COMPLETE_DATA:
                if (esp_http_client_fetch_headers(client) < 0) {
                    if (client->is_async && errno == EAGAIN) {
                        return ESP_ERR_HTTP_EAGAIN;
                    }
                    return ESP_ERR_HTTP_FETCH_HEADER;
                }
                /* falls through */
            case HTTP_STATE_RES_COMPLETE_HEADER:
                if ((err = esp_http_check_response(client)) != ESP_OK) {
                    ESP_LOGE(TAG, "Error response");
                    return err;
                }
                while (client->response->is_chunked && !client->is_chunk_complete) {
                    if (esp_http_client_get_data(client) <= 0) {
                        if (client->is_async && errno == EAGAIN) {
                            return ESP_ERR_HTTP_EAGAIN;
                        }
                        ESP_LOGD(TAG, "Read finish or server requests close");
                        break;
                    }
                }
                while (client->response->data_process < client->response->content_length) {
                    if (esp_http_client_get_data(client) <= 0) {
                        if (client->is_async && errno == EAGAIN) {
                            return ESP_ERR_HTTP_EAGAIN;
                        }
                        ESP_LOGD(TAG, "Read finish or server requests close");
                        break;
                    }
                }
                http_dispatch_event(client, HTTP_EVENT_ON_FINISH, NULL, 0);

                if (!http_should_keep_alive(client->parser)) {
                    ESP_LOGD(TAG, "Close connection");
                    esp_http_client_close(client);
                } else {
                    if (client->state > HTTP_STATE_CONNECTED) {
                        client->state = HTTP_STATE_CONNECTED;
                        client->first_line_prepared = false;
                    }
                }
                break;
                default:
                break;
        }
    } while (client->process_again);
    return ESP_OK;
}

注意到一点case 和case之间是没有break的,因此程序会falls through 向下执行。这点把握很重要,从当前状态开始将之后的要执行的操作按顺序昨晚。出现错误时,对于非阻塞态的会返回ESP_ERR_HTTP_EAGAIN;对于阻塞态的回返回当前的错误。

 

2.3 esp_err_t esp_http_client_open(esp_http_client_handle_tclient, int write_len)

这个接口打开连接,并发送请求头并返回

esp_err_t esp_http_client_open(esp_http_client_handle_t client, int write_len)
{
    esp_err_t err;
    if ((err = esp_http_client_connect(client)) != ESP_OK) {
        return err;
    }
    if ((err = esp_http_client_request_send(client)) != ESP_OK) {
        return err; 
    }
    return ESP_OK;
}

和这个函数配合使用的esp_http_client_fetch_header()和esp_http_client_read()这两个API,可以帮助我们接受HTTP服务器的响应数据。

 

2.4 int esp_http_client_fetch_headers(esp_http_client_handle_tclient)

这个函数在esp_http_client_open()之后调用,从数据流中读取,并处理所有接受到的应答HTTP头。

如果应答的HTTP头包括content-length域值,则返回这个域值或者分块的大小。

int esp_http_client_fetch_headers(esp_http_client_handle_t client)
{
    if (client->state < HTTP_STATE_REQ_COMPLETE_HEADER) {
        return ESP_FAIL;
    }

    client->state = HTTP_STATE_REQ_COMPLETE_DATA;
    esp_http_buffer_t *buffer = client->response->buffer;
    client->response->status_code = -1;
    //执行读取buffer数据以及解析
    while (client->state < HTTP_STATE_RES_COMPLETE_HEADER) {
        buffer->len = esp_transport_read(client->transport, buffer->data, client->buffer_size, client->timeout_ms);
        if (buffer->len <= 0) {
            return ESP_FAIL;
        }
        http_parser_execute(client->parser, client->parser_settings, buffer->data, buffer->len);
    }
    ESP_LOGD(TAG, "content_length = %d", client->response->content_length);
    if (client->response->content_length <= 0) {
        client->response->is_chunked = true;
        return 0;
    }
    return client->response->content_length;  //返回body长度
}

2.5 int esp_http_client_read(esp_http_client_handle_tclient, char *buffer, int len)

这个API的作用是从数据流中读取数据。

int esp_http_client_read(esp_http_client_handle_t client, char *buffer, int len)
{
    esp_http_buffer_t *res_buffer = client->response->buffer;
    //从client->response->buffer 最多读取len长度的数据
    int rlen = ESP_FAIL, ridx = 0;
    if (res_buffer->raw_len) {
        int remain_len = client->response->buffer->raw_len;
        if (remain_len > len) {
            remain_len = len;
        }
        memcpy(buffer, res_buffer->raw_data, remain_len);
        res_buffer->raw_len -= remain_len;
        res_buffer->raw_data += remain_len;
        ridx = remain_len;
    }
    int need_read = len - ridx;
    bool is_data_remain = true;
    //没有读够,可以等待接收,等接收到了再填充到我的buffer中
    while (need_read > 0 && is_data_remain) {
        if (client->response->is_chunked) {
            is_data_remain = !client->is_chunk_complete;
        } else {
            is_data_remain = client->response->data_process < client->response->content_length;
        }
        ESP_LOGD(TAG, "is_data_remain=%d, is_chunked=%d", is_data_remain, client->response->is_chunked);
        if (!is_data_remain) {
            break;
        }
        int byte_to_read = need_read;
        if (byte_to_read > client->buffer_size) {
            byte_to_read = client->buffer_size;
        }
        rlen = esp_transport_read(client->transport, res_buffer->data, byte_to_read, client->timeout_ms);
        ESP_LOGD(TAG, "need_read=%d, byte_to_read=%d, rlen=%d, ridx=%d", need_read, byte_to_read, rlen, ridx);

        if (rlen <= 0) {
            return ridx;
        }
        res_buffer->output_ptr = buffer + ridx;
        http_parser_execute(client->parser, client->parser_settings, res_buffer->data, rlen);
        ridx += res_buffer->raw_len;
        need_read -= res_buffer->raw_len;

        res_buffer->raw_len = 0; //clear
        res_buffer->output_ptr = NULL;
    }
     //实际读取到多少数据
    return ridx;
}

对于esp_http_client_read()先从client句柄的接收buffer中取数据来填充,如果没有读够且没有读完,则可以考虑接收数据并解析来填充。

3、小节

对于esp_http_client_perform()的接口的使用还是没有搞明白,仔细分析一下。

主要看看里面读取数据的函数

static int esp_http_client_get_data(esp_http_client_handle_t client)
{
    if (client->state < HTTP_STATE_RES_COMPLETE_HEADER) {
        return ESP_FAIL;
    }

    if (client->connection_info.method == HTTP_METHOD_HEAD) {
        return 0;
    }

    esp_http_buffer_t *res_buffer = client->response->buffer;

    ESP_LOGD(TAG, "data_process=%d, content_length=%d", client->response->data_process, client->response->content_length);

    int rlen = esp_transport_read(client->transport, res_buffer->data, client->buffer_size, client->timeout_ms);
    if (rlen >= 0) {
        http_parser_execute(client->parser, client->parser_settings, res_buffer->data, rlen);
    }
    return rlen;
}

这个函数是用来读取HTTP应答的消息体BODY数据的,读取数据并解析。仔细看看这个client->response->buffer->data,其空间在初始化函数client->response->buffer->data = malloc(client->buffer_size)中分配,默认是512字节。

那它在解析过程中又是如何对他处理的

static int http_on_body(http_parser *parser, const char *at, size_t length)
{
    esp_http_client_t *client = parser->data;
    ESP_LOGD(TAG, "http_on_body %d", length);
    client->response->buffer->raw_data = (char *)at;
    if (client->response->buffer->output_ptr) {
        memcpy(client->response->buffer->output_ptr, (char *)at, length);
        client->response->buffer->output_ptr += length;
    }

    client->response->data_process += length;
    client->response->buffer->raw_len += length;
    http_dispatch_event(client, HTTP_EVENT_ON_DATA, (void *)at, length);
    return 0;
}

利用esp_http_client_read()时将client->response->buffer->data 赋值到client->response->buffer->output_ptr指向的空间,指针指向向前移动。那么client->response->buffer->output_ptr指向的空间read函数形参剩余空间部分;

而在esp_http_client_get_data,由于client->response->buffer->output_ptr==NULL,在这里也有HTTP_EVENT_ON_DATA,且这里最关键的事情是分发了一个HTTP_EVENT_ON_DATA的事件给事件循环。因此我们可以在事件循环里完成其他操作。 good,这样就顺畅了。还是要向ESP-IDF的开发人员致敬。

再看看http_dispatch_event(client, HTTP_EVENT_ON_DATA, (void *)at, length);

static esp_err_t http_dispatch_event(esp_http_client_t *client, esp_http_client_event_id_t event_id, void *data, int len)
{
    esp_http_client_event_t *event = &client->event;

    if (client->event_handler) {
        event->event_id = event_id;
        event->user_data = client->user_data;
        event->data = data;
        event->data_len = len;
        return client->event_handler(event);
    }
    return ESP_OK;
}

在这里调用了预先设置好的事件回调函数

client->event_handler(HTTP_EVENT_ON_DATA);

默认是这样的

case HTTP_EVENT_ON_DATA:
        ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
        if (!esp_http_client_is_chunked_response(evt->client)) {
            // Write out data
            // printf("%.*s", evt->data_len, (char*)evt->data);  //
        }

        break;

当我们使用esp_http_client_perform时,可以从这里获取应答的body信息体。具体自己处理。

 

 

 

 

©️2020 CSDN 皮肤主题: 技术黑板 设计师: CSDN官方博客 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值