lwip-2.1.3自带的httpd网页服务器使用教程(三)使用CGI获取URL参数(GET类型表单)

上一篇: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函数再转换一下,把双引号转换成&quot;,把<span>转换成&lt;span"&gt;。

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[] = {"&amp;", "&quot;", "&#039;", "&lt;", "&gt;", "&nbsp;", "&nbsp;&nbsp;&nbsp;&nbsp;"};
  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;
}

下一篇:lwip-2.1.3自带的httpd网页服务器使用教程(四)POST类型表单的解析和文件上传

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

巨大八爪鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值