上一篇:lwip-2.1.3自带的httpd网页服务器使用教程(二)使用SSI动态生成网页部分内容
认识URL参数
在上网的时候,我们经常会见到在网址后面带有?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参数的方式呈现的。
<form name="form1" method="get">
<label>
器件名称:
<input name="devname" type="text" id="devname" value="STM32">
</label><br>
<label>
器件类型:
<select name="devtype" id="devtype">
<option value="1">单片机芯片</option>
<option value="2">网络芯片</option>
<option value="3">音频芯片</option>
</select>
</label><br>
<input type="submit" value="搜索">
</form>
<input type="submit" value="搜索">是表单提交按钮,点击按钮后凡是带有name属性的控件的名称和值都会出现在URL中。
例如,上面的表单提交后,浏览器跳转的网址就是:http://stm32f103ze/info.ssi?devname=STM32&devtype=1。
lwip httpd服务器提供的CGI功能就是用来获取这样的URL参数的。lwip提供的CGI功能分为两种:旧式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功能(标签替换功能)直接交互。
示例1:使用URL参数控制LED灯的亮灭和数码管的显示
(本节例程名称:cgi_test)
首先,在lwip-2.1.3/apps/http/fs下放入动态网页devctrl.ssi,然后运行makefsdata程序打包。devctrl.ssi的内容如下:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
<title>设备控制页</title>
<style type="text/css">
<!--
body {
font-family: "Times New Roman", Times, serif;
background: #666666;
margin: 0;
padding: 0;
text-align: center;
color: #000000;
}
#container {
width: 780px;
background: #FFFFFF;
margin: 0 auto;
border: 1px solid #000000;
text-align: left;
}
#header {
background: #DDDDDD;
padding: 0 10px 0 20px;
}
#header h1 {
margin: 0;
padding: 10px 0;
}
#mainContent {
padding: 30px 20px;
background: #FFFFFF;
font-size: 14px;
}
#footer {
padding: 0 10px;
background: #DDDDDD;
}
#footer p {
margin: 0;
padding: 10px 0;
}
-->
</style>
</head>
<body>
<div id="container">
<div id="header">
<h1>设备控制页</h1>
</div>
<div id="mainContent">
<form name="form1" method="get" action="">
<table width="100%" border="0">
<tr>
<td width="30%" align="right">LED1: </td>
<td align="left">
<label>
<input type="radio" name="led1" value="on"<!--#led1_on-->>
亮
</label>
<label>
<input type="radio" name="led1" value="off"<!--#led1_off-->>
灭
</label>
</td>
</tr>
<tr>
<td align="right">LED2: </td>
<td align="left">
<label>
<input type="radio" name="led2" value="on"<!--#led2_on-->>
亮
</label>
<label>
<input type="radio" name="led2" value="off"<!--#led2_off-->>
灭
</label>
</td>
</tr>
<tr>
<td align="right">LED3: </td>
<td align="left">
<label>
<input type="radio" name="led3" value="on"<!--#led3_on-->>
亮
</label>
<label>
<input type="radio" name="led3" value="off"<!--#led3_off-->>
灭
</label>
</td>
</tr>
<tr>
<td align="right"><label for="num">数码管: </label></td>
<td align="left"><input type="text" name="num" id="num" style="width: 100px" value="<!--#segnum-->"></td>
</tr>
<tr>
<td align="right"></td>
<td align="left"><input type="submit" value="确定"></td>
</tr>
</table>
</form>
</div>
<div id="footer">
<p><b>当前时间: </b><!--#datetime--></p>
</div>
</div>
</body>
</html>
修改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 <lwip/apps/httpd.h>
#include <lwip/def.h>
#include <stm32f1xx.h>
#include <string.h>
#include <time.h>
#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功能
旧式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分别是参数名称数组和参数值数组。
示例2:在网页表单中显示URL参数的值
(本节例程名称: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 <lwip/apps/httpd.h>
#include <lwip/mem.h>
#include <stm32f1xx.h>
#include <string.h>
#include <time.h>
#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对象。
示例3:根据URL参数动态生成虚拟网页文件
(本节例程名称: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 <lwip/apps/fs.h>
#include <lwip/mem.h>
#include <string.h>
#include <time.h>
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),
"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\"\r\n"
"\"http://www.w3.org/TR/html4/loose.dtd\">\r\n"
"<html>\r\n"
"<head>\r\n"
"<meta http-equiv=\"Content-Type\" content=\"text/html; charset=gb2312\">\r\n"
"<title>随机数</title>\r\n"
"<style>\r\n"
"body {\r\n"
"\tfont-family: Arial;\r\n"
"\tsize: 14px;\r\n"
"}\r\n"
"\r\n"
".red {\r\n"
"\tcolor: red;\r\n"
"}\r\n"
"</style>\r\n"
"</head>\r\n"
"\r\n"
"<body>\r\n"
"<b>当前时间:</b> %s<br>\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), "<b>随机数%d:</b> <span class=\"red\">%lu</span><br>\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, "</body>\r\n</html>\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);
}
}
}
}
程序运行结果:
示例4:URL参数中的特殊符号和汉字的处理
(本节例程名称:cgi_test4)
lwip httpd服务器的CGI功能获取完URL参数后,并不会帮我们处理其中的汉字和特殊符号。
例如,在网页表单内的文本框中输入“#include <stm32f1xx.h>”,提交表单后,得到的URL参数(pcValue[i]变量)的内容是%23include+%3Cstm32f1xx.h%3E。
输入“简体中文”,得到的内容是“%BC%F2%CC%E5%D6%D0%CE%C4”。
所以,我们必须要自己写代码将这些特殊符号和汉字还原。
将“%BC%F2%CC%E5%D6%D0%CE%C4”解码为“简体中文”的函数,起名为urldecode函数。
int urldecode(char *s);
由于解码后的字符串长度只可能比原文短,不可能比原文长,所以此函数直接在缓冲区s上操作,不需要指定缓冲区的大小。
执行函数前,缓冲区s里面放的是原文。
执行函数后,缓冲区s里面放的是解码后的文本,函数的返回值为解码后文本的长度。
将“简体中文”编码为“%BC%F2%CC%E5%D6%D0%CE%C4”的函数,起名为urlencode和urlencode_buf函数。
char *urlencode(const char *s);
int urlencode_buf(char *buf, int *bufsize);
由于编码后的字符串长度只可能比原文长,不可能比原文短,所以这里编写了两个版本的函数。
urlencode函数是保持原文s不变,用mem_malloc新开辟一块缓冲区,把编码后文本放到新缓冲区里面,然后返回。用完之后要记得调用mem_free释放内存。
urlencode_buf是直接在缓冲区buf上操作,缓冲区的大小由指针bufsize指向的变量决定。
如果缓冲区的大小足够,则把编码后文本放到buf里面,函数返回编码后文本的长度。
如果缓冲区的大小不够,则编码失败,将*bufsize的值改写为所需要的缓冲区的大小,函数返回-1。
#include <ctype.h>
#include <lwip/mem.h>
#include <string.h>
int urldecode(char *s)
{
int i = 0;
int j = 0;
int k;
while (s[i] != '\0')
{
if (s[i] == '%' && isxdigit(s[i + 1]) && isxdigit(s[i + 2]))
{
for (k = 1; k <= 2; k++)
{
if (k == 1)
s[j] = 0;
else
s[j] <<= 4;
if (s[i + k] >= '0' && s[i + k] <= '9')
s[j] |= s[i + k] - '0';
else if (s[i + k] >= 'a' && s[i + k] <= 'f')
s[j] |= s[i + k] - 'a' + 10;
else if (s[i + k] >= 'A' && s[i + k] <= 'F')
s[j] |= s[i + k] - 'A' + 10;
}
i += 3;
}
else
{
if (s[i] == '+')
s[j] = ' ';
else
s[j] = s[i];
i++;
}
j++;
}
s[j] = '\0';
return j;
}
char *urlencode(const char *s)
{
char *p;
int size = 0;
urlencode_buf((char *)s, &size);
p = mem_malloc(size);
if (p != NULL)
{
if (s != NULL)
strcpy(p, s);
else
*p = '\0';
urlencode_buf(p, &size);
}
return p;
}
int urlencode_buf(char *buf, int *bufsize)
{
char x;
int i, j, len;
if (buf == NULL)
{
*bufsize = 1;
return -1;
}
len = 0;
for (i = 0; buf[i] != '\0'; i++)
{
if (isalnum(buf[i]) || strchr(" -_.", buf[i]) != NULL)
len++;
else
len += 3;
}
if (*bufsize < len + 1)
{
if (len >= i)
*bufsize = len + 1;
else
*bufsize = i + 1;
return -1;
}
j = len - 1;
for (i--; i >= 0; i--)
{
if (isalnum(buf[i]) || strchr("-_.", buf[i]) != NULL)
{
buf[j] = buf[i];
j--;
}
else if (buf[i] == ' ')
{
buf[j] = '+';
j--;
}
else
{
x = buf[i] & 15;
buf[j] = (x < 10) ? (x + '0') : (x - 10 + 'A');
x = (buf[i] >> 4) & 15;
buf[j - 1] = (x < 10) ? (x + '0') : (x - 10 + 'A');
buf[j - 2] = '%';
j -= 3;
}
}
buf[len] = '\0';
return len;
}
测试用的网页form_test.ssi(这个叫做网页前端):
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
<title>表单测试</title>
<style type="text/css">
<!--
body {
background-color: #CCCCCC;
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
line-height: 1.8em;
}
form {
background-color: #FFFFFF;
width: 650px;
padding: 20px 15px;
display: block;
border: 1px solid #333333;
margin: 40px auto;
}
form h1 {
font-size: 26px;
text-align: center;
}
form label, form .left-block {
display: inline-block;
width: 100px;
text-align: right;
padding-right: 20px;
vertical-align: top;
}
form .textfield, form textarea {
width: 400px;
}
-->
</style>
</head>
<body>
<form name="form1" method="get" action="">
<h1>表单测试</h1>
<p>
<label for="textfield">文本框1:</label>
<input type="text" name="textfield" id="textfield" class="textfield" value="<!--#textfield-->">
</p>
<p>
<label for="textfield2">文本框2:</label>
<input type="text" name="textfield2" id="textfield2" class="textfield" value="<!--#textfield2-->">
</p>
<p>
<label for="textfield3">文本框3:</label>
<input type="text" name="textfield3" id="textfield3" class="textfield" value="<!--#textfield3-->">
</p>
<p>
<label for="textarea">文本框4:</label>
<textarea name="textarea" id="textarea" rows="10"><!--#textarea--></textarea>
</p>
<p>
<span class="left-block"></span>
<input type="submit" value="提交">
</p>
</form>
</body>
</html>
lwipopts.h选项:
网页中用到的SSI标签名textfield2和textfield3长度为10个字符,超过了lwip默认的8个字符,所以要在lwipopts.h中将LWIP_HTTPD_MAX_TAG_NAME_LEN定义为10(或更大),否则这些长名标签将会被lwip忽略。
// 配置HTTPD
#define LWIP_HTTPD_CGI_SSI 1
#define LWIP_HTTPD_FILE_STATE 1
#define LWIP_HTTPD_MAX_TAG_NAME_LEN 10 // SSI标签名最大长度
#define LWIP_HTTPD_SSI 1
#define LWIP_HTTPD_SSI_INCLUDE_TAG 0
#define LWIP_HTTPD_SSI_RAW 1
test.c(这个叫做网页后端):
#include <lwip/apps/httpd.h>
#include <lwip/mem.h>
#include <string.h>
#include "strutil.h"
#include "test.h"
struct form_fields
{
char textfield[LWIP_HTTPD_MAX_TAG_INSERT_LEN];
char textfield2[LWIP_HTTPD_MAX_TAG_INSERT_LEN];
char textfield3[LWIP_HTTPD_MAX_TAG_INSERT_LEN];
char textarea[LWIP_HTTPD_MAX_TAG_INSERT_LEN];
};
void httpd_cgi_handler(struct fs_file *file, const char *uri, int iNumParams, char **pcParam, char **pcValue, void *connection_state)
{
int i;
struct form_fields *fields = connection_state;
if (strcmp(uri, "/form_test.ssi") == 0 && fields != NULL)
{
for (i = 0; i < iNumParams; i++)
{
printf("[Param] name=%s, value=%s\n", pcParam[i], pcValue[i]);
if (strcasecmp(pcParam[i], "textfield") == 0)
{
strlcpy(fields->textfield, pcValue[i], sizeof(fields->textfield));
urldecode(fields->textfield);
}
else if (strcasecmp(pcParam[i], "textfield2") == 0)
{
strlcpy(fields->textfield2, pcValue[i], sizeof(fields->textfield2));
urldecode(fields->textfield2);
}
else if (strcasecmp(pcParam[i], "textfield3") == 0)
{
strlcpy(fields->textfield3, pcValue[i], sizeof(fields->textfield3));
urldecode(fields->textfield3);
}
else if (strcasecmp(pcParam[i], "textarea") == 0)
{
strlcpy(fields->textarea, pcValue[i], sizeof(fields->textarea));
urldecode(fields->textarea);
}
}
}
}
void *fs_state_init(struct fs_file *file, const char *name)
{
struct form_fields *fields;
if (strcmp(name, "/form_test.ssi") == 0)
{
fields = mem_malloc(sizeof(struct form_fields));
if (fields == NULL)
return NULL;
memset(fields, 0, sizeof(struct form_fields));
printf("%s: new state(0x%p)\n", __func__, fields);
return fields;
}
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)
{
int len;
struct form_fields *fields = connection_state;
if (fields == NULL)
return HTTPD_SSI_TAG_UNKNOWN;
if (strcmp(ssi_tag_name, "textfield") == 0)
strlcpy(pcInsert, fields->textfield, iInsertLen);
else if (strcmp(ssi_tag_name, "textfield2") == 0)
strlcpy(pcInsert, fields->textfield2, iInsertLen);
else if (strcmp(ssi_tag_name, "textfield3") == 0)
strlcpy(pcInsert, fields->textfield3, iInsertLen);
else if (strcmp(ssi_tag_name, "textarea") == 0)
strlcpy(pcInsert, fields->textarea, iInsertLen);
else
return HTTPD_SSI_TAG_UNKNOWN;
len = htmlspecialchars_buf(pcInsert, 0, &iInsertLen);
if (len == -1)
return HTTPD_SSI_TAG_UNKNOWN;
return len;
}
void test_init(void)
{
http_set_ssi_handler(test_ssi_handler, NULL, 0);
}
在httpd_cgi_handler函数中,用for循环遍历所有的URL参数,iNumParams为URL参数的个数,pcParam[i]为URL参数的名称,pcValue[i]为URL参数解码前的内容。
用strlcpy函数把解码前的内容拷贝到字符数组中,用urldecode函数解码,就能得到原文了。
strlcpy函数和strncpy函数很类似。区别是strncpy不能保证复制后的字符串一定以\0结束,而strlcpy函数可以保证。
最终我们要把这些内容显示到网页的文本框里面,而不只是在串口里面打印一下就完事。这又有另外一个问题。如果文本里面包含双引号的话,比如a"b"c,那么直接放到HTML里面就是value="a"b"c",这肯定是不符合HTML语法的。如果文本里面包含HTML标签,比如<p>、<span>的话,同样也不能正确在网页的文本框上显示。所以我们还需要用htmlspecialchars函数再转换一下,把双引号转换成",把<span>转换成<span">。
char *htmlspecialchars(const char *s, int nbsp)
{
char *p;
int size = 0;
htmlspecialchars_buf((char *)s, nbsp, &size);
p = mem_malloc(size);
if (p != NULL)
{
if (s != NULL)
strcpy(p, s);
else
*p = '\0';
htmlspecialchars_buf(p, nbsp, &size);
}
return p;
}
int htmlspecialchars_buf(char *buf, int nbsp, int *bufsize)
{
char list[] = "&\"'<> \t";
char *replacements[] = {"&", """, "'", "<", ">", " ", " "};
char *p;
int i, j, k, len;
if (buf == NULL)
{
*bufsize = 1;
return -1;
}
if (!nbsp)
list[5] = '\0';
len = 0;
for (i = 0; buf[i] != '\0'; i++)
{
p = strchr(list, buf[i]);
if (p != NULL)
len += strlen(replacements[p - list]);
else
len++;
}
if (*bufsize < len + 1)
{
if (len >= i)
*bufsize = len + 1;
else
*bufsize = i + 1;
return -1;
}
j = len - 1;
for (i--; i >= 0; i--)
{
p = strchr(list, buf[i]);
if (p != NULL)
{
p = replacements[p - list];
k = strlen(p);
memcpy(buf + j - k + 1, p, k);
j -= k;
}
else
{
buf[j] = buf[i];
j--;
}
}
buf[len] = '\0';
return len;
}
程序运行结果:
访问的网址是http://stm32f103ze/form_test.ssi?textfield=%BC%F2%CC%E5%D6%D0%CE%C4&textfield2=++a++b+++&textfield3=%3Ctd+align%3D%22right%22%3E%3Clabel+for%3D%22num%22%3E%CA%FD%C2%EB%B9%DC%3A+%3C%2Flabel%3E%3C%2Ftd%3E&textarea=int+ENC28J60_GetNextPacketPointer%28void%29%0D%0A%7B%0D%0A++return+enc28j60_next_packet%3B%0D%0A%7D。
在多行文本框中显示换行符直接用\r\n就行了。但是如果要直接在网页上显示换行符,没有文本框的话,就得把\r去掉,把\n替换为<br>标签。可以编写一个nl2br函数实现这个功能:
char *nl2br(const char *s)
{
char *p, *q;
int bufsize;
p = str_replace("\n", "<br>", s);
if (p != NULL)
{
bufsize = strlen(p) + 1;
str_replace_buf("\r", "", p, &bufsize);
}
return p;
}
nl2br函数先是把\n替换为<br>,把短文本替换为长文本,结果会变长,所以要新分配内存。
然后再把\r替换成空字符串,是把长文本替换成短文本,结果会变短,所以不需要再分配内存,直接在原来的字符串上操作就行。
替换字符串用的str_replace函数的实现如下:
char *str_replace(const char *search, const char *replace, const char *subject)
{
char *p;
int size = 0;
str_replace_buf(search, replace, (char *)subject, &size);
p = mem_malloc(size);
if (p != NULL)
{
if (subject != NULL)
strcpy(p, subject);
else
*p = '\0';
str_replace_buf(search, replace, p, &size);
}
return p;
}
int str_replace_buf(const char *search, const char *replace, char *subject, int *bufsize)
{
int i, j = 0;
int n = 0; // 需要替换的次数
int len; // 替换完毕后新字符串的长度
int slen; // search字符串的长度
int rlen; // replace字符串的长度
int ulen; // 原始字符串的长度
int clen; // 当前长度 (已执行部分替换后)
/* 检查传入的参数 */
if (subject == NULL)
{
*bufsize = 1;
return -1;
}
else
ulen = strlen(subject);
if (search == NULL || search[0] == '\0')
{
if (*bufsize >= ulen + 1)
return ulen;
else
{
*bufsize = ulen + 1;
return -1;
}
}
else
slen = strlen(search);
if (replace == NULL)
replace = "";
rlen = strlen(replace);
/* 检查需要进行多少次替换 */
for (i = 0; subject[i] != '\0'; i++)
{
if (subject[i] == search[j])
{
// 字符匹配
j++;
if (j == slen)
{
// 字符串匹配
j = 0;
n++;
}
}
else
j = 0; // 字符不匹配, 重新开始
}
/* 计算替换后字符串的长度 */
len = ulen + n * (rlen - slen);
if (*bufsize < len + 1)
{
// 缓冲区容量不够
if (rlen >= slen)
*bufsize = len + 1;
else
*bufsize = ulen + 1; // 必须要能装下原始字符串
return -1;
}
/* 执行替换操作 */
if (n > 0)
{
i = 0;
j = 0;
clen = ulen;
while (subject[i] != '\0')
{
if (subject[i] == search[j])
{
i++;
j++;
if (j == slen)
{
i -= slen;
j = 0;
if (slen != rlen && clen - i - slen > 0)
memmove(subject + i + rlen, subject + i + slen, clen - i - slen);
if (rlen > 0)
memcpy(subject + i, replace, rlen);
clen += rlen - slen;
subject[clen] = '\0';
i += rlen;
}
}
else
{
i++;
j = 0;
}
}
}
return len;
}
trim函数可以把字符串两端的空格和tab字符去掉,比如把" abc def "变成"abc def",函数的实现如下:
int trim(char *s)
{
int i, j = -1;
int len = 0;
for (i = 0; s[i] != '\0'; i++)
{
if (j == -1 && s[i] != ' ' && s[i] != '\t')
j = 0;
if (j != -1)
{
s[j] = s[i];
j++;
if (s[i] != ' ' && s[i] != '\t')
len = j;
}
}
s[len] = '\0';
return len;
}