1. 简介
1.1 HTTP
HTTP(Hyper Text Transfer Protocol),全称超文本传输协议,用于从网络服务器传输超文本到本地浏览器的传送协议。它可以使浏览器更加高效,使网络传输减少。它不仅保证计算机正确快速地传输超文本文档,还能确定传输文档中的哪一部分,以及哪部分内容首先显示(如文本、图形等)。HTTP是一个应用层协议,由请求和响应构成,是一个标准的客户端服务器(C/S)模型。HTTP是一个无状态的协议,基于TCP协议传输数据,默认使用端口80。
1.1.1 请求
HTTP把请求分成多种类型,其中最常用的是GET请求和POST请求。
1. GET请求
GET请求一般用于信息的获取,如访问网站使用的就是GET请求。GET请求仅仅只是获取资源信息,就像数据库查询一样,不会修改、增加数据,不会影响资源的状态。如果我们想在请求资源的同时附带数据,那么这些数据会被显式地放在请求URL上面。
2. POST请求
POST请求则表示可能会修改服务器上的资源,GET请求能做的,POST请求也能做。但最大的区别是请求参数的存放位置,POST请求会把参数隐式地放在请求报文中,所以对于敏感参数如账号密码等,会是更推荐的。
1.2 HTML
HTML(HyperText Markup Language),全称超文本标记语言,是一种用于创建网页的标准标记语言。使用 HTML ,可以建立自己的 WEB 站点,HTML 运行在浏览器上,由浏览器来解析。
最基础的HTML由以下组成:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My HTML</title>
</head>
<body>
<h1>This is a title</h1>
<p>This is a paragraph</p>
</body>
</html>
- <!DOCTYPE html>:声明这时一个HTML文档;
- <html></html>:HTML内容;
- <head></head>:头部内容;
- <body></body>:页面内容。
2. 例程
这个例程会在ESP32上面搭建一个简单的HTTP服务器,供局域网中的设备访问,包含基本的GET和POST请求演示。
2.1 函数API
2.1.1 启动HTTP服务器
esp_err_t httpd_start(httpd_handle_t *handle, const httpd_config_t *config);
- handle:HTTP服务器句柄;
- config:配置参数。
typedef struct httpd_config {
unsigned task_priority;
size_t stack_size;
BaseType_t core_id;
uint32_t task_caps;
uint16_t server_port;
uint16_t ctrl_port;
uint16_t max_open_sockets;
uint16_t max_uri_handlers;
uint16_t max_resp_headers;
uint16_t backlog_conn;
bool lru_purge_enable;
uint16_t recv_wait_timeout;
uint16_t send_wait_timeout;
void * global_user_ctx;
httpd_free_ctx_fn_t global_user_ctx_free_fn;
void * global_transport_ctx;
httpd_free_ctx_fn_t global_transport_ctx_free_fn;
bool enable_so_linger;
int linger_timeout;
bool keep_alive_enable;
int keep_alive_idle;
int keep_alive_interval;
int keep_alive_count;
httpd_open_func_t open_fn;
httpd_close_func_t close_fn;
httpd_uri_match_func_t uri_match_fn;
} httpd_config_t;
配置参数比较多,ESP-IDF也提供了HTTPD_DEFAULT_CONFIG宏来初始化默认配置。如果要自定义,可以关注几个比较常用的:
- stack_size:HTTP服务器任务的栈空间;
- server_port:服务器端口,默认是80;
- max_open_sockets:最大可开启socket,即可以连接的客户端数量;
- max_uri_handlers:最大URI句柄数量;
- recv_wait_timeout:接收超时时间;
- send_wait_timeout:发送超时时间;
- keep_alive_enable:网页保活使能;
- keep_alive_idle:保活空闲时间;
- keep_alive_interval:保活间隔时间;
- keep_alive_count:保活包失败重传次数。
2.1.2 注册URI处理
esp_err_t httpd_register_uri_handler(httpd_handle_t handle, const httpd_uri_t *uri_handler);
- handle:HTTP句柄;
- uri_handler:URI处理结构体。
typedef struct httpd_uri {
const char *uri;
httpd_method_t method;
esp_err_t (*handler)(httpd_req_t *r);
void *user_ctx;
} httpd_uri_t;
- uri:URI;
- method:请求类型,格式如HTTP_XXX;
- handler:处理函数;
- user_ctx:用户上下文。
2.1.3 获取请求头字段内容长度
size_t httpd_req_get_hdr_value_len(httpd_req_t *r, const char *field);
- r:HTTP请求句柄;
- field:字段名。
2.1.4 获取请求头字段内容
esp_err_t httpd_req_get_hdr_value_str(httpd_req_t *r, const char *field, char *val, size_t val_size);
- r:HTTP请求句柄;
- field:字段名;
- val:输出数组;
- val_size:数组长度。
2.1.5 获取URL参数长度
size_t httpd_req_get_url_query_len(httpd_req_t *r)
- r:HTTP句柄。
2.1.6 获取URL参数
esp_err_t httpd_req_get_url_query_str(httpd_req_t *r, char *buf, size_t buf_len)
- r:HTTP句柄;
- buf:输出数组;
- buf_len:数组长度。
2.1.7 发送响应
esp_err_t httpd_resp_send(httpd_req_t *r, const char *buf, ssize_t buf_len);
esp_err_t httpd_resp_send_chunk(httpd_req_t *r, const char *buf, ssize_t buf_len);
static inline esp_err_t httpd_resp_sendstr(httpd_req_t *r, const char *str);
static inline esp_err_t httpd_resp_sendstr_chunk(httpd_req_t *r, const char *str);
发送响应有几个函数,send结尾的就是一次性把数据发送完;send后面接str的就是发送字符串,这样就不需要传长度;chunk结尾的就是可以多次发送数据,最后一定要发一个长度为0的包,表示发送完成。
2.1.8 接收请求内容
int httpd_req_recv(httpd_req_t *r, char *buf, size_t buf_len)
- r:HTTP句柄;
- buf:输出数组;
- buf_len:数组长度。
2.2 代码
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/semphr.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "nvs_flash.h"
#include "sys/socket.h"
#include "lwip/err.h"
#include "lwip/sys.h"
#include "netdb.h"
#include "arpa/inet.h"
#include "esp_http_server.h"
#include <string.h>
#define TAG "app"
static httpd_handle_t http_server;
static const char index_html[] = " \
<!DOCTYPE html> \
<html> \
<head> \
<meta charset=\"utf-8\"> \
<title>index</title> \
</head> \
\
<body> \
<h1>Hello from ESP32</h1> \
<form action=\"/hello\" method=\"post\"> \
<label for=\"name\">What's your name:</label> \
<input type=\"text\" id=\"name\" name=\"name\" required> \
<input type=\"submit\" value=\"OK\"></button> \
</form> \
</body> \
</html> \
";
static const char hello_html_template[] = " \
<!DOCTYPE html> \
<html> \
<head> \
<meta charset=\"utf-8\"> \
<title>hello</title> \
</head> \
\
<body> \
<h1>Oh, Hello %s</h1> \
</body> \
\
</html> \
";
static esp_err_t index_get_handler(httpd_req_t *req)
{
/* 获取Host信息 */
size_t buf_len = httpd_req_get_hdr_value_len(req, "Host") + 1;
if (buf_len > 1) {
char *buf = malloc(buf_len);
if (httpd_req_get_hdr_value_str(req, "Host", buf, buf_len) == ESP_OK) {
ESP_LOGI(TAG, "Get request to host: %s", buf);
}
free(buf);
}
/* 回复数据包 */
httpd_resp_sendstr(req, index_html);
return ESP_OK;
}
static const httpd_uri_t index_uri = {
.uri = "/index",
.method = HTTP_GET,
.handler = index_get_handler,
.user_ctx = NULL
};
static esp_err_t hello_post_handler(httpd_req_t *req)
{
/* 获取Host信息 */
size_t buf_len = httpd_req_get_hdr_value_len(req, "Host") + 1;
if (buf_len > 1) {
char *buf = malloc(buf_len);
if (httpd_req_get_hdr_value_str(req, "Host", buf, buf_len) == ESP_OK) {
ESP_LOGI(TAG, "Post request to host: %s", buf);
}
free(buf);
}
/* 获取内容长度 */
int len = 0;
{
char *buf = malloc(128);
memset(buf, 0, 128);
if (httpd_req_get_hdr_value_str(req, "Content-Length", buf, 128) != ESP_OK) {
ESP_LOGE(TAG, "Get content length failed");
return ESP_FAIL;
}
len = atoi(buf) + 1;
free(buf);
}
/* 获取表单数据 */
char *buf = malloc(len);
memset(buf, 0, len);
if (httpd_req_recv(req, buf, len) <= 0) {
ESP_LOGE(TAG, "Receive request content failed");
return ESP_FAIL;
}
if (strstr(buf, "name=") == NULL) {
ESP_LOGE(TAG, "Can't found fleid \"name\"");
free(buf);
return ESP_FAIL;
}
/* 发送数据 */
char *hello_html = malloc(1024);
snprintf(hello_html, 1024, hello_html_template, buf + strlen("name="));
httpd_resp_sendstr(req, hello_html);
free(buf);
free(hello_html);
return ESP_OK;
}
static const httpd_uri_t hello_uri = {
.uri = "/hello",
.method = HTTP_POST,
.handler = hello_post_handler,
.user_ctx = NULL
};
static void wifi_event_handler(void* arg,
esp_event_base_t event_base,
int32_t event_id,
void* event_data)
{
if (event_base == IP_EVENT) {
if (event_id == IP_EVENT_STA_GOT_IP) {
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
if (httpd_start(&http_server, &config) == ESP_OK) {
httpd_register_uri_handler(http_server, &index_uri);
httpd_register_uri_handler(http_server, &hello_uri);
}
ESP_LOGI(TAG, "HTTP server on port %d", config.server_port);
}
} else if (event_base == WIFI_EVENT) {
if (event_id == WIFI_EVENT_STA_DISCONNECTED) {
httpd_stop(http_server);
ESP_LOGI(TAG, "HTTP server stopped");
} else if (event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
}
}
}
int app_main()
{
/* 初始化NVS */
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());
ESP_ERROR_CHECK(nvs_flash_init());
}
/* 初始化WiFi协议栈 */
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
ESP_EVENT_ANY_ID,
&wifi_event_handler,
NULL,
NULL));
ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
IP_EVENT_STA_GOT_IP,
&wifi_event_handler,
NULL,
NULL));
wifi_config_t wifi_config = {
.sta = {
.ssid = "Your SSID",
.password = "Your password",
.threshold.authmode = WIFI_AUTH_WPA_WPA2_PSK,
},
};
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
return 0;
}
WiFi基站模式和AP连接在之前的文章有讲过,这里不再赘述。
AP连接成功后会启动HTTP服务器,我这里全部使用默认的配置,注册了两个URI处理,一个是“/index”,用来演示GET请求,获取主页;一个是“/hello”,用来演示POST请求。
第一个URI处理函数,演示一下请求头字段的获取,一般先获取字段的长度,接着请求对应大小的堆内存,再copy数据到数组中。不建议在函数内直接定义数组,因为处理函数是在HTTP任务中调用的,这样做很容易导致栈溢出。最后就是返回HTML页面文本给客户端。
如果ESP32在接受请求时报413错误,在SDK的配置文件(sdkconfig)中,修改CONFIG_HTTPD_MAX_REQ_HDR_LEN配置,增大请求头的长度。
这个URI处理会返回一个HTML页面,如果你用的是浏览器请求的话,就会有自动解析并显示画面。
这个HTML包含一个标题和一些表单控件,我们可以在文本框这里填写自己的名字,点击“OK”按钮,会向ESP32提交表单数据,其实就是向“/hello”这个URI发起POST请求。
对于POST请求,在HTTP的请求头中会有一个“Content-Length”字段来描述数据包的大小。我们首先获取这个字段内容,然后去请求相应的内存空间,最后copy数据包数据到数组中;如果数据包非常大的话也可以多次获取。
对于表单数据,一般都是以键值对的形式组成的,中间用等于号连接。接收到POST请求后需要返回对应的数据,这里就是HTML文件,我们把表单获取到的数据附到HTML文档中,显示的效果如下。