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参数的方式呈现的。



是表单提交按钮,点击按钮后凡是带有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的内容如下:





设备控制页




LED1:
LED2:
LED3:

修改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和数码管的状态。

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

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

使用新式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 
#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对象。

示例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:

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

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

最后实现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网页服务器使用教程(三)使用CGI获取URL参数(GET类型表单)_第5张图片

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

你可能感兴趣的:(STM32,服务器,运维,lwip,httpd,stm32)