上一篇:lwip-2.1.3自带的httpd网页服务器使用教程(二)使用SSI动态生成网页部分内容
在上网的时候,我们经常会见到在网址后面带有?A=B&C=D这样的语法格式。例如:
https://blog.csdn.net/ZLK1214/article/details/129151458?csdn_share_tail={%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22129151458%22%2C%22source%22%3A%22ZLK1214%22}
上面这个网址就带有csdn_share_tail参数,等号后面是参数的值。如果有多个参数的话,中间可用&符号连接。
另外我们也可以把网页中的表单的method属性设为get,表单提交后表单的内容也是以URL参数的方式呈现的。
是表单提交按钮,点击按钮后凡是带有name属性的控件的名称和值都会出现在URL中。
例如,上面的表单提交后,浏览器跳转的网址就是:http://stm32f103ze/info.ssi?devname=STM32&devtype=1。
lwip httpd服务器提供的CGI功能就是用来获取这样的URL参数的。lwip提供的CGI功能分为两种:旧式CGI和新式CGI。
旧式CGI的功能比较简单,用http_set_cgi_handlers函数指定一些支持URL参数的网页,经过指定的回调函数处理后,跳转到另一页面上(也可以选择不跳转)。
http_set_cgi_handlers的原型如下。
typedef struct
{
const char *pcCGIName;
tCGIHandler pfnCGIHandler;
} tCGI;
void http_set_cgi_handlers(const tCGI *cgis, int num_handlers);
其中,参数cgis为tCGI结构体数组(必须是全局变量,不能是局部变量),参数num_handlers为tCGI结构体数组的大小,可由LWIP_ARRAYSIZE宏计算数组的大小。
tCGI结构体里面的pcCGIName是网页的名称,pfnCGIHandler是处理该网页的URL参数的回调函数。回调函数的原型如下:
const char *XXX(int iIndex, int iNumParams, char *pcParam[], char *pcValue[]);
参数iIndex是当前正在处理的网页在tCGI结构体数组中的下标,那么当前正在处理的网页名称就是“全局数组名[iIndex].pcCGIName”。
iNumParams是URL参数的个数,是pcParam数组和pcValue数组的元素个数。
pcParam数组和pcValue数组分别是参数名列表和对应的参数值的列表。
回调函数的返回值是要跳转的网页名称(浏览器的地址栏上显示的仍然还是跳转前的网页文件名),如果不想跳转到另一网页,可直接返回当前网页名称“全局数组名[iIndex].pcCGIName”。
tCGI结构体的pcCGIName成员(跳转前的页面名称),和pfnCGIHandler回调函数的返回值(要跳转的页面)都可以是虚拟页面,并不是必须要在文件系统上能找得到。
旧式CGI的不足之处是URL参数无法和当前HTTP连接的SSI功能(标签替换功能)直接交互。
(本节例程名称:cgi_test)
首先,在lwip-2.1.3/apps/http/fs下放入动态网页devctrl.ssi,然后运行makefsdata程序打包。devctrl.ssi的内容如下:
设备控制页
设备控制页
修改lwipopts.h里面的HTTPD选项,开启CGI功能和SSI功能:
// 配置HTTPD
#define LWIP_HTTPD_CGI 1
#define LWIP_HTTPD_SSI 1
#define LWIP_HTTPD_SSI_INCLUDE_TAG 0
#define LWIP_HTTPD_SSI_RAW 1
编写test.c文件,其中test_init函数在main函数中调用了httpd_init()之后调用。
#include
#include
#include
#include
#include
#include "SegDisplay.h"
#include "test.h"
static float test_num;
static tCGI test_cgis[3];
static const char *test_cgis_handler(int iIndex, int iNumParams, char *pcParam[], char *pcValue[])
{
char *endptr;
float num;
int i;
for (i = 0; i < iNumParams; i++)
{
if (strcasecmp(pcParam[i], "led1") == 0)
{
if (strcasecmp(pcValue[i], "on") == 0)
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_5, GPIO_PIN_SET);
else if (strcasecmp(pcValue[i], "off") == 0)
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_5, GPIO_PIN_RESET);
else if (strcasecmp(pcValue[i], "toggle") == 0)
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_5);
}
else if (strcasecmp(pcParam[i], "led2") == 0)
{
if (strcasecmp(pcValue[i], "on") == 0)
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
else if (strcasecmp(pcValue[i], "off") == 0)
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
else if (strcasecmp(pcValue[i], "toggle") == 0)
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
else if (strcasecmp(pcParam[i], "led3") == 0)
{
if (strcasecmp(pcValue[i], "on") == 0)
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_6, GPIO_PIN_SET);
else if (strcasecmp(pcValue[i], "off") == 0)
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_6, GPIO_PIN_RESET);
else if (strcasecmp(pcValue[i], "toggle") == 0)
HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_6);
}
else if (strcasecmp(pcParam[i], "num") == 0)
{
num = strtof(pcValue[i], &endptr);
if (*endptr == '\0')
{
test_num = num;
SegDisplay_SetFloatNumber(num);
}
}
}
return "/devctrl.ssi";
}
static u16_t test_ssi_handler(const char *ssi_tag_name, char *pcInsert, int iInsertLen)
{
struct tm tm;
time_t t;
if (strcmp(ssi_tag_name, "led1_on") == 0)
{
if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_5) == GPIO_PIN_SET)
snprintf(pcInsert, iInsertLen, " checked");
else
snprintf(pcInsert, iInsertLen, "");
}
else if (strcmp(ssi_tag_name, "led1_off") == 0)
{
if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_5) == GPIO_PIN_RESET)
snprintf(pcInsert, iInsertLen, " checked");
else
snprintf(pcInsert, iInsertLen, "");
}
else if (strcmp(ssi_tag_name, "led2_on") == 0)
{
if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_SET)
snprintf(pcInsert, iInsertLen, " checked");
else
snprintf(pcInsert, iInsertLen, "");
}
else if (strcmp(ssi_tag_name, "led2_off") == 0)
{
if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET)
snprintf(pcInsert, iInsertLen, " checked");
else
snprintf(pcInsert, iInsertLen, "");
}
else if (strcmp(ssi_tag_name, "led3_on") == 0)
{
if (HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_6) == GPIO_PIN_SET)
snprintf(pcInsert, iInsertLen, " checked");
else
snprintf(pcInsert, iInsertLen, "");
}
else if (strcmp(ssi_tag_name, "led3_off") == 0)
{
if (HAL_GPIO_ReadPin(GPIOE, GPIO_PIN_6) == GPIO_PIN_RESET)
snprintf(pcInsert, iInsertLen, " checked");
else
snprintf(pcInsert, iInsertLen, "");
}
else if (strcmp(ssi_tag_name, "segnum") == 0)
snprintf(pcInsert, iInsertLen, "%g", test_num);
else if (strcmp(ssi_tag_name, "datetime") == 0)
{
time(&t);
localtime_r(&t, &tm);
strftime(pcInsert, iInsertLen, "%Y-%m-%d %H:%M:%S", &tm);
}
else
return HTTPD_SSI_TAG_UNKNOWN;
return strlen(pcInsert);
}
static void test_led_init(void)
{
GPIO_InitTypeDef gpio;
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
gpio.Mode = GPIO_MODE_OUTPUT_PP;
gpio.Pin = GPIO_PIN_5 | GPIO_PIN_13;
gpio.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOC, &gpio);
gpio.Pin = GPIO_PIN_6;
HAL_GPIO_Init(GPIOE, &gpio);
}
void test_init(void)
{
test_led_init();
SegDisplay_Init();
test_cgis[0].pcCGIName = "/devctrl";
test_cgis[0].pfnCGIHandler = test_cgis_handler;
test_cgis[1].pcCGIName = "/devctrl.ssi";
test_cgis[1].pfnCGIHandler = test_cgis_handler;
test_cgis[2].pcCGIName = "/devctrl.html";
test_cgis[2].pfnCGIHandler = test_cgis_handler;
http_set_cgi_handlers(test_cgis, LWIP_ARRAYSIZE(test_cgis));
http_set_ssi_handler(test_ssi_handler, NULL, 0);
}
程序运行结果:
访问网址:http://stm32f103ze/devctrl?led1=toggle&led2=toggle&led3=toggle&num=-13.9
可以看到数码管显示了-13.9这个数字。每访问一次网页,三个LED灯都会切换一次状态。
还可以在网页中通过表单控件动态改变URL参数的值,表单上也会显示当前LED和数码管的状态。
旧式CGI功能最大的缺点就是没有办法在解析URL参数的时候直接控制SSI标签替换的内容,如果使用全局变量的话不同客户端的连接又会相互干扰。新式CGI功能就能解决这个问题。
旧式CGI功能是先执行回调函数,再打开网页文件。具体打开哪个网页文件由回调函数的返回值决定。
新式CGI功能是先打开网页文件,再执行回调函数。具体打开哪个网页文件完全由用户在浏览器中输入的网址决定。
所以,新式CGI不能像旧式CGI那样在程序中指定跳转的新网页名称。旧式CGI和新式CGI各有各的优缺点,谁也替代不了谁。
新式CGI功能的开启方法是在lwipopts.h中打开LWIP_HTTPD_CGI_SSI选项。打开选项后需要实现httpd_cgi_handler函数。
当LWIP_HTTPD_FILE_STATE=0时,httpd_cgi_handler函数的原型为void httpd_cgi_handler(struct fs_file *file, const char *uri, int iNumParams, char **pcParam, char **pcValue)
当LWIP_HTTPD_FILE_STATE=1时,httpd_cgi_handler函数的原型为void httpd_cgi_handler(struct fs_file *file, const char *uri, int iNumParams, char **pcParam, char **pcValue, void *connection_state),多了一个connection_state参数。
其中,file是打开的网页文件对象,uri是网页文件名,iNumParams是URL参数的个数,pcParam和pcValue分别是参数名称数组和参数值数组。
(本节例程名称:cgi_test2)
在刚才的示例1中,由于使用的是旧式CGI,SSI回调函数是无法获取到URL参数的值的,所以网页表单中显示的是LED灯和数码管的实际状态。
如果我们想要在网页表单中显示URL参数的原始内容,不去读取LED灯和数码管的实际状态的话,可以改用新式CGI。
首先修改lwipopts.h里面的HTTP选项:
// 配置HTTPD
#define LWIP_HTTPD_CGI_SSI 1
#define LWIP_HTTPD_FILE_STATE 1
#define LWIP_HTTPD_SSI 1
#define LWIP_HTTPD_SSI_INCLUDE_TAG 0
#define LWIP_HTTPD_SSI_RAW 1
修改test.c:
#include
#include
#include
#include
#include
#include "SegDisplay.h"
#include "test.h"
struct page_state
{
char led1_on[20];
char led1_off[20];
char led2_on[20];
char led2_off[20];
char led3_on[20];
char led3_off[20];
char segnum[20];
char datetime[50];
};
void httpd_cgi_handler(struct fs_file *file, const char *uri, int iNumParams, char **pcParam, char **pcValue, void *connection_state)
{
char *endptr;
float num;
int i;
struct page_state *state = connection_state;
if (strcmp(uri, "/devctrl.ssi") == 0 && state != NULL)
{
for (i = 0; i < iNumParams; i++)
{
if (strcasecmp(pcParam[i], "led1") == 0)
{
if (strcasecmp(pcValue[i], "on") == 0)
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_5, GPIO_PIN_SET);
strcpy(state->led1_on, " checked");
}
else if (strcasecmp(pcValue[i], "off") == 0)
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_5, GPIO_PIN_RESET);
strcpy(state->led1_off, " checked");
}
else if (strcasecmp(pcValue[i], "toggle") == 0)
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_5);
}
else if (strcasecmp(pcParam[i], "led2") == 0)
{
if (strcasecmp(pcValue[i], "on") == 0)
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);
strcpy(state->led2_on, " checked");
}
else if (strcasecmp(pcValue[i], "off") == 0)
{
HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);
strcpy(state->led2_off, " checked");
}
else if (strcasecmp(pcValue[i], "toggle") == 0)
HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}
else if (strcasecmp(pcParam[i], "led3") == 0)
{
if (strcasecmp(pcValue[i], "on") == 0)
{
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_6, GPIO_PIN_SET);
strcpy(state->led3_on, " checked");
}
else if (strcasecmp(pcValue[i], "off") == 0)
{
HAL_GPIO_WritePin(GPIOE, GPIO_PIN_6, GPIO_PIN_RESET);
strcpy(state->led3_off, " checked");
}
else if (strcasecmp(pcValue[i], "toggle") == 0)
HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_6);
}
else if (strcasecmp(pcParam[i], "num") == 0)
{
strcpy(state->segnum, pcValue[i]);
num = strtof(pcValue[i], &endptr);
if (*endptr == '\0')
SegDisplay_SetFloatNumber(num);
}
}
}
}
void *fs_state_init(struct fs_file *file, const char *name)
{
struct page_state *state;
struct tm tm;
time_t t;
if (strcmp(name, "/devctrl.ssi") == 0)
{
state = mem_malloc(sizeof(struct page_state));
if (state == NULL)
return NULL;
memset(state, 0, sizeof(struct page_state));
printf("%s: new state(0x%p)\n", __func__, state);
time(&t);
localtime_r(&t, &tm);
strftime(state->datetime, sizeof(state->datetime), "%Y-%m-%d %H:%M:%S", &tm);
return state;
}
else
return NULL;
}
void fs_state_free(struct fs_file *file, void *state)
{
if (state != NULL)
{
printf("%s: delete state(0x%p)\n", __func__, state);
mem_free(state);
}
}
static u16_t test_ssi_handler(const char *ssi_tag_name, char *pcInsert, int iInsertLen, void *connection_state)
{
struct page_state *state = connection_state;
if (state == NULL)
return HTTPD_SSI_TAG_UNKNOWN;
pcInsert[iInsertLen - 1] = '\0';
if (strcmp(ssi_tag_name, "led1_on") == 0)
strncpy(pcInsert, state->led1_on, iInsertLen - 1);
else if (strcmp(ssi_tag_name, "led1_off") == 0)
strncpy(pcInsert, state->led1_off, iInsertLen - 1);
else if (strcmp(ssi_tag_name, "led2_on") == 0)
strncpy(pcInsert, state->led2_on, iInsertLen - 1);
else if (strcmp(ssi_tag_name, "led2_off") == 0)
strncpy(pcInsert, state->led2_off, iInsertLen - 1);
else if (strcmp(ssi_tag_name, "led3_on") == 0)
strncpy(pcInsert, state->led3_on, iInsertLen - 1);
else if (strcmp(ssi_tag_name, "led3_off") == 0)
strncpy(pcInsert, state->led3_off, iInsertLen - 1);
else if (strcmp(ssi_tag_name, "segnum") == 0)
strncpy(pcInsert, state->segnum, iInsertLen - 1);
else if (strcmp(ssi_tag_name, "datetime") == 0)
strncpy(pcInsert, state->datetime, iInsertLen - 1);
else
return HTTPD_SSI_TAG_UNKNOWN;
return strlen(pcInsert);
}
static void test_led_init(void)
{
GPIO_InitTypeDef gpio;
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
gpio.Mode = GPIO_MODE_OUTPUT_PP;
gpio.Pin = GPIO_PIN_5 | GPIO_PIN_13;
gpio.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOC, &gpio);
gpio.Pin = GPIO_PIN_6;
HAL_GPIO_Init(GPIOE, &gpio);
}
void test_init(void)
{
test_led_init();
SegDisplay_Init();
http_set_ssi_handler(test_ssi_handler, NULL, 0);
}
代码的运行逻辑是:
打开网页时,先在fs_state_init里面创建一个struct page_state对象,把datetime字段先填好。
接着是在httpd_cgi_handler里面解析URL参数,connection_state就是刚才建立的struct page_state对象。解析参数的时候就把led1_on、led1_off、led2_on、led2_off、led3_on、led3_off和segnum字段填好。
到了SSI标签内容替换环节,在test_ssi_handler函数中直接拷贝struct page_state里面已经填好的内容。
网页内容生成完毕时,在fs_state_free函数中删除struct page_state对象。
(本节例程名称:cgi_test3)
还记得之前的virtual_webpage2程序吗?
在之前的程序里面,MAXSTEP的值是在程序里面写死了的,固定为25。
现在我们学习了URL参数的解析,就可以根据网址里面的maxstep参数,在程序运行的时候动态决定MAXSTEP的值。
如果网址里面没有指定maxstep参数的话,默认值还是原来的25。
首先,在lwipopts.h里面打开LWIP_HTTPD_CGI_SSI选项:
// 配置HTTPD
#define LWIP_HTTPD_CGI_SSI 1
#define LWIP_HTTPD_CUSTOM_FILES 1
#define LWIP_HTTPD_DYNAMIC_FILE_READ 1
#define LWIP_HTTPD_DYNAMIC_HEADERS 1
然后在struct content中新增一个maxstep成员,在fs_open_custom函数中将maxstep的默认值定义为25:
最后实现URL参数处理函数httpd_cgi_handler,在函数中通过file->pextension获取到struct content结构体指针,把解析出来的maxstep参数保存到结构体里面,之后fs_read_custom函数就能使用了。
void httpd_cgi_handler(struct fs_file *file, const char *uri, int iNumParams, char **pcParam, char **pcValue)
{
int i, n;
struct content *content;
if (strcmp(uri, "/helloworld.html") == 0)
{
for (i = 0; i < iNumParams; i++)
{
if (strcasecmp(pcParam[i], "maxstep") == 0)
{
n = atoi(pcValue[i]);
if (n < 1)
n = 1;
content = file->pextension;
content->maxstep = n;
printf("%s(%p): maxstep=%d\n", __func__, content, content->maxstep);
}
}
}
}
完整的程序代码:
#include
#include
#include
#include
struct content
{
char str[4000];
int step;
int maxstep;
int pos;
int len;
int tot_len;
int id;
};
int fs_open_custom(struct fs_file *file, const char *name)
{
struct content *content;
if (strcmp(name, "/helloworld.html") == 0)
{
content = mem_malloc(sizeof(struct content));
if (content == NULL)
return 0;
memset(content, 0, sizeof(struct content));
content->id = 1;
content->maxstep = 25;
file->len = sizeof(content->str); // 指定fs_read_custom()函数的count参数的最大值(不能设置为0)
file->pextension = content;
printf("%s(0x%p)\n", __func__, file->pextension);
return 1;
}
else
return 0;
}
void fs_close_custom(struct fs_file *file)
{
printf("%s(0x%p)\n", __func__, file->pextension);
if (file->pextension != NULL)
{
mem_free(file->pextension);
file->pextension = NULL;
}
}
int fs_read_custom(struct fs_file *file, char *buffer, int count)
{
char part[70];
int i;
struct content *content = file->pextension;
struct tm tm;
time_t t;
unsigned long value;
if (content->pos == 0)
{
if (content->step == 0)
{
time(&t);
localtime_r(&t, &tm);
strftime(part, sizeof(part), "%Y-%m-%d %H:%M:%S", &tm);
snprintf(content->str, sizeof(content->str),
"\r\n"
"\r\n"
"\r\n"
"\r\n"
"随机数 \r\n"
"\r\n"
"\r\n"
"\r\n"
"\r\n"
"当前时间: %s
\r\n", part);
}
else if (content->step < content->maxstep)
{
i = 0;
content->str[0] = '\0';
while (i < sizeof(content->str) / sizeof(part))
{
value = rand();
snprintf(part, sizeof(part), "随机数%d: %lu
\r\n", content->id + i, value);
i++;
if (strlen(content->str) + strlen(part) + 1 > sizeof(content->str))
{
printf("%s: buffer is too small\n", __func__);
break;
}
strcat(content->str, part);
}
content->id += i;
printf("%s: step=%d, id=%d~%d\n", __func__, content->step, content->id, content->id + i - 1);
}
else if (content->step == content->maxstep)
strcpy(content->str, "\r\n\r\n");
else
{
printf("%s(0x%p): end of file, tot_len=%d\n", __func__, content, content->tot_len);
return FS_READ_EOF;
}
content->len = strlen(content->str);
content->tot_len += content->len;
}
if (count > content->len - content->pos)
count = content->len - content->pos;
printf("%s(0x%p): step=%d, len=%d, current=%d~%d\n", __func__, content, content->step, content->len, content->pos, content->pos + count - 1);
memcpy(buffer, content->str + content->pos, count);
content->pos += count;
if (content->pos == content->len)
{
content->step++;
content->pos = 0;
}
return count;
}
void httpd_cgi_handler(struct fs_file *file, const char *uri, int iNumParams, char **pcParam, char **pcValue)
{
int i, n;
struct content *content;
if (strcmp(uri, "/helloworld.html") == 0)
{
for (i = 0; i < iNumParams; i++)
{
if (strcasecmp(pcParam[i], "maxstep") == 0)
{
n = atoi(pcValue[i]);
if (n < 1)
n = 1;
content = file->pextension;
content->maxstep = n;
printf("%s(%p): maxstep=%d\n", __func__, content, content->maxstep);
}
}
}
}
程序运行结果:
下一篇:lwip-2.1.3自带的httpd网页服务器使用教程(四)POST类型表单的解析和文件上传