本博文会简要介绍ESP-IDF下实现HTTP客户端的方法
TCP协议栈
ESP使用lwIP作为嵌入式的TCP/IP协议栈支持
lwIP是一套在MCU层级上用C实现的IP协议栈,可以运行在裸机/RTOS/嵌入式Linux,乐鑫为ESP32提供了相关移植包
相关内容可以参考lwIP库函数,在LWIP和ESP-NETIF组件中得到支持
1 | esp_err_t esp_netif_init(void); |
esp_netif组件建立在lwip基础上,如上面的API所示,实现了
- TCP/IP协议初始化与内存分配
- 建立基于IP协议的通信
- 控制本机IP地址与查找目标IP
- DHCP功能
- 收发TCP报文的底层实现
注意:这个组件并没有实现DNS功能,需要使用单独的DNS组件才能实现DNS服务器/DNS解析功能
HTTP客户端
ESP-IDF提供了可以实现稳定链接的HTTP-Client组件<esp_http_client>
,实现从ESP-IDF应用程序发出HTTP/S请求的API
HTTP-Client可以理解成为一个没有画面的“浏览器”——它与服务器建立TCP/IP连接,并收发符合HTTP协议标准的TCP报文,其中包含消息头和数据包,数据包会以json格式传输
综上我们可以知道,如果要在ESP-IDF设备和HTTP网站(服务器)之间建立稳定的连接,需要五个组件:
- wifi或ethernet组件,提供底层联网功能
- lwip组件,提供IP协议的MCU实现
- netif组件,提供TCP协议的MCU实现
- esp_http_client组件,提供HTTP-Client/Server数据解析和连接处理的实现,其中HTTP-Server组件在之前的博文中已经介绍过
- cJSON组件,用于解析服务器回传的json数据/处理本地数据为json格式并POST到服务器
如果有必要,还需要使用freertos组件以方便多任务处理
使用HTTP-Client相关API的步骤如下:
在开始之前,需要先建立NVS存储、连接WiFi、初始化netif网络接口
1
2
3
4
5
6
7
8
9
10 esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
ESP_ERROR_CHECK(internet_connect()); //连接wifi或ethernet
esp_http_client_init()
创建一个esp_http_client_config_t的实例(实例化对象),并配置HTTP-Client句柄
1
2
3
4
5
6
7
8esp_http_client_config_t config = {
.host = WEB_SERVER,
.path = WEB_PATH_GET_TIME,
.query = WEB_QUERY,
.transport_type = HTTP_TRANSPORT_OVER_TCP,
.event_handler = _http_event_handler,
.user_data = local_response_buffer
};执行HTTP客户端的各种操作
包括打开链接、进行数据交换、抑或是关闭链接
所有这些操作都可以通过封装好的函数配合上面步骤中指定的event_handler回调函数进行实现
1
esp_http_client_handle_t client = esp_http_client_init(&config);
其中event_handler是基于状态机的,如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79esp_err_t _http_event_handler(esp_http_client_event_t* evt)
{
static char* output_buffer; // Buffer to store response of http request from event handler
static int output_len; // Stores number of bytes read
switch (evt->event_id)
{
case HTTP_EVENT_ERROR:
ESP_LOGD(TAG, "HTTP_EVENT_ERROR");
break;
case HTTP_EVENT_ON_CONNECTED:
ESP_LOGD(TAG, "HTTP_EVENT_ON_CONNECTED");
break;
case HTTP_EVENT_HEADER_SENT:
ESP_LOGD(TAG, "HTTP_EVENT_HEADER_SENT");
break;
case HTTP_EVENT_ON_HEADER:
ESP_LOGD(TAG, "HTTP_EVENT_ON_HEADER, key=%s, value=%s", evt->header_key, evt->header_value);
break;
case HTTP_EVENT_ON_DATA:
ESP_LOGD(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
/*
* Check for chunked encoding is added as the URL for chunked encoding used in this example returns binary data.
* However, event handler can also be used in case chunked encoding is used.
*/
if (!esp_http_client_is_chunked_response(evt->client))
{
// If user_data buffer is configured, copy the response into the buffer
if (evt->user_data)
{
memcpy(evt->user_data + output_len, evt->data, evt->data_len);
}
else
{
if (output_buffer == NULL)
{
output_buffer = (char*)malloc(esp_http_client_get_content_length(evt->client));
output_len = 0;
if (output_buffer == NULL)
{
ESP_LOGE(TAG, "Failed to allocate memory for output buffer");
return ESP_FAIL;
}
}
memcpy(output_buffer + output_len, evt->data, evt->data_len);
}
output_len += evt->data_len;
}
break;
case HTTP_EVENT_ON_FINISH:
ESP_LOGD(TAG, "HTTP_EVENT_ON_FINISH");
if (output_buffer != NULL)
{
// Response is accumulated in output_buffer. Uncomment the below line to print the accumulated response
// ESP_LOG_BUFFER_HEX(TAG, output_buffer, output_len);
free(output_buffer);
output_buffer = NULL;
output_len = 0;
}
break;
case HTTP_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "HTTP_EVENT_DISCONNECTED");
int mbedtls_err = 0;
esp_err_t err = esp_tls_get_and_clear_last_error(evt->data, &mbedtls_err, NULL);
if (err != 0)
{
if (output_buffer != NULL)
{
free(output_buffer);
output_buffer = NULL;
output_len = 0;
}
ESP_LOGI(TAG, "Last esp error code: 0x%x", err);
ESP_LOGI(TAG, "Last mbedtls failure: 0x%x", mbedtls_err);
}
break;
}
return ESP_OK;
}通过
esp_http_client_cleanup()
关闭链接,并释放系统资源需要注意:这个函数必须是操作完成后调用的最后一个函数
需要注意一点:esp_http_client_init()建立的连接是持久性的,因此HTTP客户端可以在多个交换中重用相同的连接,只要服务器没有使用报头connection: close强行关闭,或者没有使*用esp_http_client_cleanup()*关闭链接,设备的HTTP链接就会保持打开状态
常用的HTTP-Client操作
HTTP:GET
1 | // GET |
这里展示了两个常用的API
1 | esp_http_client_get_status_code(client) //获取HTTP返回状态码 |
其中esp_http_client_get_content_length()比较特殊,仅能应用于返回数据长度定长的状态下,也就是HTTP报头中不能有chunked
情况,如果想要接收非定长数据,需要使用专用的API:esp_http_client_is_chunked_response(esp_http_client_handle_t client)
先获取报文长度,再针对这个长度在回调函数中接收数据到HTTP报文缓存区
GitHub上的ESP-IDF repo中热心网友给出了下面的代码来实现简单快捷的request
1 | int http_request(char *http_response, int *http_response_length, int range_start, int range_end, int client_id) |
这段代码是基于esp_http_client_open的,速度也相对较快,推荐使用
HTTP:POST
如下面示例:
1 | // POST |
注意:这里面的数据包一定要设置为格式正确的字符串,推荐使用cJSON组件而不是手动格式化
其他操作
可以查看官方示例代码<esp-idf目录>/example/protocols/esp_http_client来获得指令格式
篇幅所限不再过多介绍
cJSON组件
ESP32提供了cJSON的移植(如果不提供其实自己移植也很简单)
cJSON是一套用于格式化处理JSON数据的C库,可以针对使用使用情景分为两类API:
- 将字符串处理为JSON对象
- 将JSON对象处理为字符串
C中的字符串用char类型数组描述,而JSON对象则被cJSON定义为一个结构体,如下所示
1 | typedef struct cJSON |
它的“基类”是一个双向链表,同时支持扩展更多子类
type属性代表JSON的类型
valuestring、valueint、valuedouble分别表示JSON中可能包含的三个数据类型:字符串、整型、浮点型
string属性代表子类的名字或JSON实例化对象的名字
在代码中使用
1 |
即可调用cJSON组件
使用
1 | CJSON_PUBLIC(const char*) cJSON_Version(void) |
可以输出cJSON的版本号到一个指定字符串中,只要再将字符串输出就可以检查当前的cJSON版本是否符合要求
将字符串处理为JSON对象
常用于从服务器获取数据后将数据保存在本地时候的解析工作
json以嵌套的键值对或值的有序列表形式存在,通常如下
1 | { |
它采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(包括C/C++/C#、Java、JavaScript、Perl、Python等),很多语言都有自己的JSON库实现
嵌套的键值对在不同语言中都有自己的理解:对象(Object)、记录(Record)、结构体(struct)、字典(dictionary)、哈希表(hash table)、键值对(Key-Value)、关联数组(associative array)等等,但都具有一对一或一对多的形式
值的有序列表,大部分语言都将其理解为数组(array)
这些常见的数据结构在同样基于这些结构的编程语言之间交换成为可能,这也是为什么JSON格式会在互联网中流行(甚至当今很多嵌入式设备也在考虑于实时性较低的应用中使用json)
json自己的实现中,将每个json串视为json对象,它是一个无序的名称-值成对组合的集合(可嵌套的无序的键值对)。一个对象以左括号{开始,以右括号}结束,要求每个名称后都接一个冒号:,同时成对组合之间用逗号,分隔
cJSON解析API的基本使用可以参考以下示例代码(收取的json数据如上面给出的第一个json格式示例):
1 | /* 确认从local_response_buffer(HTTP报文缓存区)获得的数据是否为JSON格式 */ |
将JSON对象处理为字符串
上面说过,JSON对象是以链表为基础的
在创建JSON对象时要先创建一个”根节点“,再从这个根节点上”生长“出更多数据。每多一个节点,就意味着大括号多了一层,比如上面给出的两个JSON示例,第一个就有一个根节点;第二个则有一个根节点、三个message子节点
可以参考以下示例代码:
1 | cJSON* cjson_root = cJSON_CreateObject(); //创建JSON根节点 |
这里需要注意:建立了JSON对象就一定要记得删除,每建立一个就要记得删除对应的数据,C可没有内存管理机制,都是要手动分配回收的!
如果存在多个子节点和一个根节点,要先从顶层的子节点删除,最后删除根节点