基于LWIP的HTTP开发:嵌入式web服务器
通过stm32板卡的网络功能,实现通过网页浏览器直接访问设备,对设备的信息进行查询和设置。此方法常用于路由器,可以直接省去上位机软件下载。简单轻便的访问设备。
通过stm32板卡的网络功能,实现通过网页浏览器直接访问设备,对设备的信息进行查询和设置。
此方法常用于路由器,可以直接省去上位机软件下载。简单轻便的访问设备。
开发步骤如下:
第一步:lwip加入http插件
在cubemx工程中的lwip三方库勾选http后进行生成

生成完成后可以在代码的该工程下看到以下源码已经被添加进来

代码在lwip初始化后添加httpd_init()即可
makefsdata工具
除了http源码,还有一个工具:makefsdata也很重要,该工具可以将你编写好的html代码转为文件系统的格式(通过内部flash存储的静态数组)。相关工具在lwip官网下载

使用方法:把编写的.html或.shtml文件放入fs目录下,执行makefsdata.exe程序。会生成对应的fsdata.c文件。把fsdata.c文件拷贝到http目录下进行编译。
#define file_NULL (struct fsdata_file *) NULL
static const unsigned int dummy_align__index_shtml = 0;
static const unsigned char data__index_shtml[] = {
/* /index.shtml (13 chars) */
0x2f,0x69,0x6e,0x64,0x65,0x78,0x2e,0x73,0x68,0x74,0x6d,0x6c,0x00,0x00,0x00,0x00,
/* HTTP header */
/* "HTTP/1.0 200 OK
" (17 bytes) */
0x48,0x54,0x54,0x50,0x2f,0x31,0x2e,0x30,0x20,0x32,0x30,0x30,0x20,0x4f,0x4b,0x0d,
0x0a,
/* "Server: lwIP/1.3.1 (http://savannah.nongnu.org/projects/lwip)
" (63 bytes) */
0x53,0x65,0x72,0x76,0x65,0x72,0x3a,0x20,0x6c,0x77,0x49,0x50,0x2f,0x31,0x2e,0x33,
0x2e,0x31,0x20,0x28,0x68,0x74,0x74,0x70,0x3a,0x2f,0x2f,0x73,0x61,0x76,0x61,0x6e,
0x6e,0x61,0x68,0x2e,0x6e,0x6f,0x6e,0x67,0x6e,0x75,0x2e,0x6f,0x72,0x67,0x2f,0x70,
0x72,0x6f,0x6a,0x65,0x63,0x74,0x73,0x2f,0x6c,0x77,0x69,0x70,0x29,0x0d,0x0a,
/* "Content-type: text/html
Expires: Fri, 10 Apr 2008 14:00:00 GMT
Pragma: no-cache
" (85 bytes) */
0x43,0x6f,0x6e,0x74,0x65,0x6e,0x74,0x2d,0x74,0x79,0x70,0x65,0x3a,0x20,0x74,0x65,
0x78,0x74,0x2f,0x68,0x74,0x6d,0x6c,0x0d,0x0a,0x45,0x78,0x70,0x69,0x72,0x65,0x73,
0x3a,0x20,0x46,0x72,0x69,0x2c,0x20,0x31,0x30,0x20,0x41,0x70,0x72,0x20,0x32,0x30,
0x30,0x38,0x20,0x31,0x34,0x3a,0x30,0x30,0x3a,0x30,0x30,0x20,0x47,0x4d,0x54,0x0d,
0x0a,0x50,0x72,0x61,0x67,0x6d,0x61,0x3a,0x20,0x6e,0x6f,0x2d,0x63,0x61,0x63,0x68,
0x65,0x0d,0x0a,0x0d,0x0a,
/* raw file data (6767 bytes) */
0x3c,0x21,0x44,0x4f,0x43,0x54,0x59,0x50,0x45,0x20,0x68,0x74,0x6d,0x6c,0x3e,0x0d,
0x0a,0x3c,0x68,0x74,0x6d,0x6c,0x3e,0x0d,0x0a,0x3c,0x68,0x65,0x61,0x64,0x3e,0x0d,
0x0a,0x3c,0x6d,0x65,0x74,0x61,0x20,0x63,0x68,0x61,0x72,0x73,0x65,0x74,0x3d,0x22,}
const struct fsdata_file file__index_shtml[] = { {
file_NULL,
data__index_shtml,
data__index_shtml + 16,
sizeof(data__index_shtml) - 16,
1,
}};
#define FS_ROOT file__index_shtml
#define FS_NUMFILES 1
file__index_shtml的第一个变量指向下一个文件,结构体如下。
lwip自带的fs.c(文件系统源码)通过链表遍历所有文件数据,比较文件名来找到对应文件。
如果想要使用其他文件系统和外部flash,需要对这些代码进行更换。
struct fsdata_file {
const struct fsdata_file *next;
const unsigned char *name;
const unsigned char *data;
int len;
u8_t flags;
#if HTTPD_PRECALCULATED_CHECKSUM
u16_t chksum_count;
const struct fsdata_chksum *chksum;
#endif /* HTTPD_PRECALCULATED_CHECKSUM */
};
支持协议
lwip的http只支持ssi和cgi两种,如果想要使用传输数据更多,实时性更强的web socket。需要对http进行改造,此处就不再介绍。
SSI:是一种类似于 ASP 的基于服务器的网页制作技术。大多数的 WEB 服务器等均支持 SSI 命令。将内容发送到浏览器之前,可以使用“服务器端包含 (SSI)”指令将文本、图形或应用程序信息包含到网页中。因为包含 SSI 指令的文件要求特殊处理,所以必须为所有 SSI 文件赋予 SSI 文件扩展名。默认扩展名是 .stm、.shtm 和 .shtml。
可以理解为SSI接口只能进行简单的显示,不能进行参数设置。
CGI: 规范允许 Web 服务器执行外部程序,并将它们的输出发送给Web 浏览器,CGI 在物理上是一段程序,运行在服务器上,提供同客户端 HTML 页面的接口。
可以理解为网页下发http://192.168.5.10/led=on到服务器,服务器会获取后缀后的led=on对应的意思。然后服务器执行对应已经预设好了的程序。返回结果到网页上显示。
测试过程中通过F12查看网页调试工具中的network可以看到请求包与服务器回包。更具体的数据(包含tcp包头,mac数据等)建议使用wireshark进行抓包。
LWIP宏配置
#define LWIP_HTTPD_SSI 1 //使能SSI接口
#define LWIP_HTTPD_CGI 1 //使能CGI接口
#define LWIP_HTTPD_DYNAMIC_HEADERS 1
#define HTTP_IS_DATA_VOLATILE(hs) TCP_WRITE_FLAG_COPY
#define MEM_SIZE 30*1024
// 增加TCP发送缓冲区(例如:16KB),确保能装下整个页面
#define TCP_SND_BUF 4096*2
//
//// 增加TCP接收窗口,与发送端匹配
#define TCP_WND 4096
//// 调整TCP报文段数量,公式一般为 4 * TCP_SND_BUF/TCP_MSS
//#define TCP_MSS 1460//TCP每包的数据量
#define TCP_SND_QUEUELEN (4 * TCP_SND_BUF/TCP_MSS)
#define MEMP_NUM_TCP_SEG (TCP_SND_QUEUELEN+1)
打印调试方法
LWIP源码在函数调用过程中已经添加了很多打印帮助定位问题与分析。
只需要设置 #define LWIP_DEBUG 1 进行,同时还有很多分模块宏对不同功能模块的打印进行区分。如
#define LWIP_DEBUG 1
#define HTTPD_DEBUG LWIP_DBG_ON
#define LWIP_DBG_TYPES_ON (LWIP_DBG_ON|LWIP_DBG_TRACE|LWIP_DBG_STATE|LWIP_DBG_FRESH)
#define LWIP_DBG_MIN_LEVEL LWIP_DBG_LEVEL_ALL
//关闭以太网打印
#define ETHARP_DEBUG LWIP_DBG_OFF
#define ETH_DEBUG LWIP_DBG_OFF
#define NETIF_DEBUG LWIP_DBG_OFF
#define ETHERNET_DEBUG LWIP_DBG_OFF
SSI接口
初始化
#define NUM_CONFIG_SSI_TAGS 6
static const char *ppcTAGs[] = {
"V_OPEN", // 阀门开度 (原VALVE_OPENING)
"FLOW", // 流量 (原FLOW_RATE)
"PRESS", // 压力 (原PRESSURE)
"TEMP", // 温度 (原TEMPERATURE)
"V_STAT", // 阀门状态 (原VALVE_STATUS)
"RUN_T" // 运行时间 (原RUNNING_TIME)
};
// SSI处理器函数
u16_t SSIHandler(int iIndex, char *pcInsert, int iInsertLen) {
static unsigned char i = 1;
int valveOpening = 1; // 读取阀门开度
float flowRate = 2.0; // 读取流量
float pressure = 3.0; // 读取压力
float temperature = 4.0; // 读取温度
int valveStatus = 5; // 阀门状态
int runningTime = i++; // 运行时间
if(i > 6000){
i = 1;
}
// 根据索引返回对应的数据
switch(iIndex) {
case 0: // VALVE_OPENING
return snprintf(pcInsert, iInsertLen, "%d", valveOpening);
case 1: // FLOW_RATE
return snprintf(pcInsert, iInsertLen, "%.2f", flowRate);
case 2: // PRESSURE
return snprintf(pcInsert, iInsertLen, "%.2f", pressure);
case 3: // TEMPERATURE
return snprintf(pcInsert, iInsertLen, "%.1f", temperature);
case 4: // VALVE_STATUS
return snprintf(pcInsert, iInsertLen, "%d", valveStatus);
case 5: // RUNNING_TIME
return snprintf(pcInsert, iInsertLen, "%d", runningTime);
default:
return 0;
}
}
void http_Init(void){
httpd_init();
printf("start http server\r\n");
http_set_ssi_handler(SSIHandler, ppcTAGs, NUM_CONFIG_SSI_TAGS);
}
调用关系:
http_send被调用时会触发Http_send_data_ssi(比如当web get html时候,http协议栈会把html文件发送,在发送前http_send_data_ssi会解析html中有<!-- >的部分,替换成对应的数值进行上报)
/**
* Try to send more data on this pcb.
*
* @param pcb the pcb to send data
* @param hs connection state
*/
static u8_t
http_send(struct altcp_pcb *pcb, struct http_state *hs)
{
u8_t data_to_send = HTTP_NO_DATA_TO_SEND;
LWIP_DEBUGF(HTTPD_DEBUG | LWIP_DBG_TRACE, ("http_send: pcb=%p hs=%p left=%d\n", (void *)pcb,
(void *)hs, hs != NULL ? (int)hs->left : 0));
#if LWIP_HTTPD_SUPPORT_POST && LWIP_HTTPD_POST_MANUAL_WND
if (hs->unrecved_bytes != 0) {
return 0;
}
#endif /* LWIP_HTTPD_SUPPORT_POST && LWIP_HTTPD_POST_MANUAL_WND */
/* If we were passed a NULL state structure pointer, ignore the call. */
if (hs == NULL) {
return 0;
}
#if LWIP_HTTPD_FS_ASYNC_READ
/* Check if we are allowed to read from this file.
(e.g. SSI might want to delay sending until data is available) */
if (!fs_is_file_ready(hs->handle, http_continue, hs)) {
return 0;
}
#endif /* LWIP_HTTPD_FS_ASYNC_READ */
#if LWIP_HTTPD_DYNAMIC_HEADERS
/* Do we have any more header data to send for this file? */
if (hs->hdr_index < NUM_FILE_HDR_STRINGS) {
data_to_send = http_send_headers(pcb, hs);
if ((data_to_send == HTTP_DATA_TO_SEND_FREED) ||
((data_to_send != HTTP_DATA_TO_SEND_CONTINUE) &&
(hs->hdr_index < NUM_FILE_HDR_STRINGS))) {
return data_to_send;
}
}
#endif /* LWIP_HTTPD_DYNAMIC_HEADERS */
/* Have we run out of file data to send? If so, we need to read the next
* block from the file. */
if (hs->left == 0) {
if (!http_check_eof(pcb, hs)) {
return 0;
}
}
#if LWIP_HTTPD_SSI
if (hs->ssi) {
data_to_send = http_send_data_ssi(pcb, hs);
} else
#endif /* LWIP_HTTPD_SSI */
{
data_to_send = http_send_data_nonssi(pcb, hs);
}
if ((hs->left == 0) && (fs_bytes_left(hs->handle) <= 0)) {
/* We reached the end of the file so this request is done.
* This adds the FIN flag right into the last data segment. */
LWIP_DEBUGF(HTTPD_DEBUG, ("End of file.\n"));
http_eof(pcb, hs);
return 0;
}
LWIP_DEBUGF(HTTPD_DEBUG | LWIP_DBG_TRACE, ("send_data end.\n"));
return data_to_send;
}
htttp_send_data_ssi解析index.html文件中的ssi标签<!--#vale_opening-->,然后使用定义的ssihandler把对应数据插入html文件中发给客户端
ssi->tag_name和注册时额http_tags[tags]一致,执行对应的函数
把处理后的返回值放到ssi->tag_insert中。
注意:
LWIP_HTTPD_MAX_TAG_NAME_LEN 需要调整为大于ppcTAGS数组的长度,不然会匹配不上
CGI接口
初始化
static const tCGI ppcURLs[] = /* cgi程序 */
{
{"/leds.cgi", LEDS_CGI_Handler},//网页发来的name和对应的处理函数
// {"/valve_data.cgi", VALVE_DATA_CGI_Handler},
};
#define NUM_CONFIG_CGI_URIS 1 //cgi的数量
http_set_cgi_handlers(ppcURLs, NUM_CONFIG_CGI_URIS); /* 配置CGI句柄 */
调用关系:
//把句柄赋值给httpd_cgis
void http_set_cgi_handlers(const tCGI *cgis, int num_handlers)
{
LWIP_ASSERT("no cgis given", cgis != NULL);
LWIP_ASSERT("invalid number of handlers", num_handlers > 0);
httpd_cgis = cgis;
httpd_num_cgis = num_handlers;
}
lwip收到http协议,http_recv调用http_parse_request,http_parse_request进行安全和长度判断后解析出URI指令,调用http_find_file函数。
/** Try to find the file specified by uri and, if found, initialize hs
* accordingly.
*
* @param hs the connection state
* @param uri the HTTP header URI
* @param is_09 1 if the request is HTTP/0.9 (no HTTP headers in response)
* @return ERR_OK if file was found and hs has been initialized correctly
* another err_t otherwise
*/
static err_t
http_find_file(struct http_state *hs, const char *uri, int is_09)
{
size_t loop;
struct fs_file *file = NULL;
char *params = NULL;
err_t err;
#if LWIP_HTTPD_CGI
int i;
#endif /* LWIP_HTTPD_CGI */
#if !LWIP_HTTPD_SSI
const
#endif /* !LWIP_HTTPD_SSI */
/* By default, assume we will not be processing server-side-includes tags */
u8_t tag_check = 0;
printf("%s\r\n",uri);
/* Have we been asked for the default file (in root or a directory) ? */
#if LWIP_HTTPD_MAX_REQUEST_URI_LEN
size_t uri_len = strlen(uri);
if ((uri_len > 0) && (uri[uri_len - 1] == '/') &&
((uri != http_uri_buf) || (uri_len == 1))) {
size_t copy_len = LWIP_MIN(sizeof(http_uri_buf) - 1, uri_len - 1);
if (copy_len > 0) {
MEMCPY(http_uri_buf, uri, copy_len);
http_uri_buf[copy_len] = 0;
}
#else /* LWIP_HTTPD_MAX_REQUEST_URI_LEN */
if ((uri[0] == '/') && (uri[1] == 0)) {
#endif /* LWIP_HTTPD_MAX_REQUEST_URI_LEN */
/* Try each of the configured default filenames until we find one
that exists. */
for (loop = 0; loop < NUM_DEFAULT_FILENAMES; loop++) {
const char *file_name;
#if LWIP_HTTPD_MAX_REQUEST_URI_LEN
if (copy_len > 0) {
size_t len_left = sizeof(http_uri_buf) - copy_len - 1;
if (len_left > 0) {
size_t name_len = strlen(httpd_default_filenames[loop].name);
size_t name_copy_len = LWIP_MIN(len_left, name_len);
MEMCPY(&http_uri_buf[copy_len], httpd_default_filenames[loop].name, name_copy_len);
http_uri_buf[copy_len + name_copy_len] = 0;
}
file_name = http_uri_buf;
} else
#endif /* LWIP_HTTPD_MAX_REQUEST_URI_LEN */
{
file_name = httpd_default_filenames[loop].name;
}
LWIP_DEBUGF(HTTPD_DEBUG | LWIP_DBG_TRACE, ("Looking for %s...\n", file_name));
printf("%s filename :%s\r\n",__func__,file_name);
err = fs_open(&hs->file_handle, file_name);
if (err == ERR_OK) {
uri = file_name;
file = &hs->file_handle;
LWIP_DEBUGF(HTTPD_DEBUG | LWIP_DBG_TRACE, ("Opened.\n"));
printf("file name opne ok\r\n");
#if LWIP_HTTPD_SSI
tag_check = httpd_default_filenames[loop].shtml;
#endif /* LWIP_HTTPD_SSI */
break;
}
}
}
if (file == NULL) {
/* No - we've been asked for a specific file. */
/* First, isolate the base URI (without any parameters) */
params = (char *)strchr(uri, '?');
if (params != NULL) {
/* URI contains parameters. NULL-terminate the base URI */
*params = '\0';
params++;
}
#if LWIP_HTTPD_CGI
http_cgi_paramcount = -1;
/* Does the base URI we have isolated correspond to a CGI handler? */
if (httpd_num_cgis && httpd_cgis) {
for (i = 0; i < httpd_num_cgis; i++) {
if (strcmp(uri, httpd_cgis[i].pcCGIName) == 0) {
/*
* We found a CGI that handles this URI so extract the
* parameters and call the handler.
*/
http_cgi_paramcount = extract_uri_parameters(hs, params);
uri = httpd_cgis[i].pfnCGIHandler(i, http_cgi_paramcount, hs->params,
hs->param_vals);
break;
}
}
}
#endif /* LWIP_HTTPD_CGI */
LWIP_DEBUGF(HTTPD_DEBUG | LWIP_DBG_TRACE, ("Opening %s\n", uri));
err = fs_open(&hs->file_handle, uri);
if (err == ERR_OK) {
file = &hs->file_handle;
} else {
file = http_get_404_file(hs, &uri);
}
#if LWIP_HTTPD_SSI
if (file != NULL) {
if (file->flags & FS_FILE_FLAGS_SSI) {
tag_check = 1;
} else {
#if LWIP_HTTPD_SSI_BY_FILE_EXTENSION
tag_check = http_uri_is_ssi(file, uri);
#endif /* LWIP_HTTPD_SSI_BY_FILE_EXTENSION */
}
}
#endif /* LWIP_HTTPD_SSI */
}
if (file == NULL) {
/* None of the default filenames exist so send back a 404 page */
file = http_get_404_file(hs, &uri);
}
return http_init_file(hs, file, is_09, uri, tag_check, params);
}
http_find_file函数有三个主要功能:
根据uri返回对应的html文件给web,当uri = /时,会从fsdata.c的文件系统中选出与httpd_default_filenames数组中文件名相同的html文件返回给web。这个html文件显示的内容也就是你网页上可以浏览到的初始内容。
httpd_default_filenames不做更改时默认名字有
static const default_filename httpd_default_filenames[] = {
{"/index.shtml", 1 },
{"/index.ssi", 1 },
{"/index.shtm", 1 },
{"/index.html", 0 },
{"/index.htm", 0 }
};
所以fsdata.c中的Html文件名也最好叫index.html。如果你要用新的名字,把新名字加 在这个数组中。
另外两个功能就是进行ssi和cgi的处理
代码示例参照:https://blog.csdn.net/qq_44712722/article/details/148290192
更多推荐



所有评论(0)