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

服务器 0

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

在阅读本篇内容之前,请修改httpd.c文件,修复lwip自带httpd服务器里面关于post的一个bug:
bug #64458: When tcp_err() is invoked, tcp_pcb is freed but httpd_post_finished() is not called by httpd.c
复现方法:上传一个大文件,在文件还没上传完的时候,按下浏览器的停止按钮。
现象:lwip不会调用httpd_post_finished()函数,导致内存泄露。
修复方法:将下面的代码添加到http_state_eof函数末尾。

  /* bug #64458: When tcp_err() is invoked, tcp_pcb is freed but httpd_post_finished() is not called by httpd.c */  /* Workaround: Copy the following code to the end of "static void http_state_eof(struct http_state *hs)" */#if LWIP_HTTPD_SUPPORT_POST  if ((hs->post_content_len_left != 0)#if LWIP_HTTPD_POST_MANUAL_WND      || ((hs->no_auto_wnd != 0) && (hs->unrecved_bytes != 0))#endif /* LWIP_HTTPD_POST_MANUAL_WND */     ) {    /* make sure the post code knows that the connection is closed */    http_uri_buf[0] = 0;    httpd_post_finished(hs, http_uri_buf, LWIP_HTTPD_URI_BUF_LEN);  }#endif /* LWIP_HTTPD_SUPPORT_POST*/

HTML表单的分类

HTML表单有两种提交方式:GET方式和POST方式。
表单提交方式由<form>标签的method属性决定。method="get"是GET方式,method="post"是POST方式。
另外,<form>标签的action属性指定表单要提交到哪个页面上。如果action为空字符串"",那么就是提交到当前页面上。
GET方式提交表单后,所有带有name属性的表单控件的内容都会出现在URL(浏览器网址)上,也就是说GET方式其实就是以URL参数的方式提交表单,这个之前已经讲过了。
我们今天要讲的是POST方式提交的表单。POST方式提交后,表单控件的内容不会出现在URL上,这一定程度上提高了安全性。POST方式还有一个好处,就是提交的数据量比GET方式更大,不受URL最大长度的限制。
POST表单又细分为两种类型:普通表单和文件上传表单。当<form>标签不存在enctype属性时,表单为普通表单。当<form>标签的enctype="multipart/form-data"时,表单为文件上传表单。
文件上传表单是专门用来上传文件的表单,其格式与普通表单完全不一样,需要单独解析。在Adobe Dreamweaver CS3这款网页设计软件中,只要插入了文件框控件,Dreamweaver就会自动帮我们在<form>标签上添加enctype="multipart/form-data",自动修改为文件上传类型的表单。
关于文件上传表单,我们留到后面再讲。我们先讲普通表单。

普通类型POST表单的解析

(本节例程名称:post_test)
要想接收POST表单数据,首先需要在lwip的lwipopts.h里面开启LWIP_HTTPD_SUPPORT_POST选项。

// 配置HTTPD#define LWIP_HTTPD_SUPPORT_POST 1

开启LWIP_HTTPD_SUPPORT_POST选项后,需要自己实现下面三个函数。

err_t httpd_post_begin(void *connection, const char *uri, const char *http_request,                       u16_t http_request_len, int content_len, char *response_uri,                       u16_t response_uri_len, u8_t *post_auto_wnd);err_t httpd_post_receive_data(void *connection, struct pbuf *p);void httpd_post_finished(void *connection, char *response_uri, u16_t response_uri_len);

第一个函数httpd_post_begin是开始处理某个POST表单提交请求时调用的函数。
其中,参数connection是当前HTTP连接的唯一标识,是一个内存地址,但是里面的数据是lwip httpd私有的,不允许私自去操作。
参数uri是访问的网页名称,例如“/form_test.html”。
http_request是http header的全部内容,http_request_len是http header的总长度,例如:

HTTP/1.1Accept: image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/xaml+xml, application/x-ms-xbap, application/x-ms-application, */*Referer: http://stm32f103ze/form_test.htmlAccept-Language: zh-cnUser-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; InfoPath.3; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 1.1.4322)Content-Type: application/x-www-form-urlencodedAccept-Encoding: gzip, deflateHost: stm32f103zeContent-Length: 810Connection: Keep-AliveCache-Control: no-cache

其中最重要的是Content-Type属性,如果为application/x-www-form-urlencoded就是普通表单,如果以multipart/form-data开头就是文件上传表单。

content_len是全部表单内容的总长度。
httpd_post_begin函数的返回值如果是ERR_OK,则程序表示接受当前HTTP连接,lwip会继续调用后续的httpd_post_receive_data和httpd_post_finished函数。如果返回其他非ERR_OK值,则程序表示拒绝了当前HTTP连接,后续不再调用httpd_post_receive_data和httpd_post_finished函数。拒绝连接时,可以用strlcpy或strncpy函数给字符串response_uri赋值(字符串缓冲区的大小为response_uri_len),表示拒绝连接后要显示的网页文件(浏览器URL不会发生变化)。如果拒绝连接时不修改response_uri字符串的内容,则显示的是默认的404错误页面。
*post_auto_wnd变量仅当LWIP_HTTPD_POST_MANUAL_WND=1时有效,*post_auto_wnd的默认值是1,表示http服务器自动管理TCP滑动窗口。若在httpd_post_begin函数内将*post_auto_wnd的值设为0,那么我们就可以自己调用httpd_post_data_recved函数管理TCP滑动窗口了,可以动态调节数据的接收速度,很类似于TCP里面的tcp_recved函数。

第二个函数httpd_post_receive_data是接收POST表单内容用的函数,参数p是收到的数据内容。
请注意使用完p之后一定要记得调用pbuf_free函数释放内存。
函数通常返回ERR_OK。如果返回其他值,则表明程序出错,lwip会拒收后面的数据,并调用http_handle_post_finished结束。在文件上传的时候可以用这种方法拒收大文件。
收到的表单数据大致是下面这样,也就是aaa=bbb&ccc=ddd&eee=fff这样的形式,需要我们自行分离控件名称和控件内容,还需要用urldecode函数解码。
textfield=%23include+%3Cstdio.h%3E&textfield2=if+%28strcmp%28uri%2C+%22%2Fform_test.html%22%29+%21%3D+0%29&textfield3=printf%28%22%5Bhttpd_post_begin%5D+connection%3D0x%25p%5Cn%22%2C+connection%29%3B&radio=gpio&checkbox=on&checkbox2=on&checkbox3=on&select=f103c8&fileField=BingWallpaper-20221017.jpg&textarea=err_t+httpd_post_receive_data%28void+*connection%2C+struct+pbuf+*p%29%0D%0A%7B%0D%0A++struct+httpd_post_state+*state%3B%0D%0A++%0D%0A++printf%28%22%5Bhttpd_post_receive_data%5D+connection%3D0x%25p%5Cn%22%2C+connection%29%3B%0D%0A++state+%3D+httpd_post_find_state%28connection%2C+NULL%29%3B%0D%0A++pbuf_copy_partial%28p%2C+state-%3Econtent+%2B+state-%3Econtent_pos%2C+p-%3Etot_len%2C+0%29%3B%0D%0A++state-%3Econtent_pos+%2B%3D+p-%3Etot_len%3B%0D%0A++pbuf_free%28p%29%3B%0D%0A++return+ERR_OK%3B%0D%0A%7D
IE6浏览器会在末尾多发送2字节的/r/n换行符,如果Content-Length=377的话,httpd_post_receive_data会收到379字节数据。由于我们用malloc分配的是Content-Length大小的内存,为了避免缓冲区溢出,一定要把这个换行符丢掉。

第三个函数httpd_post_finished是结束处理POST表单请求的函数。response_uri是结束时要显示的页面,response_uri_len是response_uri缓冲区的大小。如果response_uri没有赋值,则显示的是404错误页面。

由于http连接标识connection是一块存放httpd服务器私有数据的void *型内存块,里面的内容是不能乱动的。那我们要想存放网页的数据该怎么办呢?
我们可以定义一个自定义结构体struct httpd_post_state(名称可以随便起)的链表httpd_post_list,例如:

struct httpd_post_state{  struct httpd_post_state *next; // 链表的下一个节点  void *connection; // http连接标识  char *content; // 表单内容  int content_len; // 表单长度  int content_pos; // 已接收的表单内容的长度  int multipart; // 是否为文件上传表单  char **params; // 表单控件名列表  char **values; // 表单控件值表  int param_count; // 表单控件个数};static struct httpd_post_state *httpd_post_list; // 保存http post请求数据的链表

httpd_post_list链表是一个全局变量,其初始值为NULL空指针。每当有新的post请求到来时,就用mem_malloc(sizeof(struct httpd_post_state))新分配一块内存,把网页数据都存放到里面,然后把这块新分配的结构体内存加入到httpd_post_list链表中。结构体里面的connection成员的值就等于lwip调用httpd_post_begin函数时传入的connection参数值。
后面接收表单数据时,lwip调用httpd_post_receive_data函数传入了connection标识,就在httpd_post_list链表里面去寻找connection成员和connection参数相匹配的那个链表节点,就能取出网页数据了。
post请求处理结束时,httpd_post_finished函数被lwip调用,在该函数内根据connection标识找到struct httpd_post_state结构体,将其从httpd_post_list链表中移除,然后用mem_free释放结构体占用的内存。
请看代码:

/* 为新http post请求创建链表节点 */static struct httpd_post_state *httpd_post_create_state(void *connection, int content_len){  struct httpd_post_state *state, *p;    LWIP_ASSERT("connection != NULL", connection != NULL);  LWIP_ASSERT("connection is new", httpd_post_find_state(connection) == NULL);  LWIP_ASSERT("content_len >= 0", content_len >= 0);    state = mem_malloc(sizeof(struct httpd_post_state) + content_len + 1);  if (state == NULL)    return NULL;  memset(state, 0, sizeof(struct httpd_post_state));  state->connection = connection; // http连接标识  state->content = (char *)(state + 1); // 指向结构体后面content_len+1字节的内存空间, 用于保存收到的表单内容  state->content[content_len] = '/0'; // 字符串结束符  state->content_len = content_len; // 表单内容长度    // 将新分配的节点添加到链表末尾  if (httpd_post_list != NULL)  {    // 找到尾节点    for (p = httpd_post_list; p->next != NULL; p = p->next);    // 将state挂到尾节点的后面, 成为新的尾节点    p->next = state;  }  else    httpd_post_list = state; // 链表为空, 直接赋值, 成为第一个节点  return state;}/* 根据http连接标识找到链表节点 */static struct httpd_post_state *httpd_post_find_state(void *connection){  struct httpd_post_state *p;    LWIP_ASSERT("connection != NULL", connection != NULL);    for (p = httpd_post_list; p != NULL; p = p->next)  {    if (p->connection == connection)      break;  }  return p;}/* 从链表中删除节点 */static void httpd_post_delete_state(struct httpd_post_state *state){  struct httpd_post_state *p;    LWIP_ASSERT("state != NULL", state != NULL);    if (httpd_post_list != state)  {    // 找到当前节点的前一个节点    for (p = httpd_post_list; p != NULL && p->next != state; p = p->next)    LWIP_ASSERT("p != NULL", p != NULL);    // 从链表中移除    p->next = state->next;  }  else    httpd_post_list = state->next;    // 释放节点所占用的内存空间  state->next = NULL;  state->connection = NULL;  state->content = NULL;  if (state->params != NULL)  {    mem_free(state->params);    state->params = NULL;    state->values = NULL;  }  mem_free(state);}

在上面的代码中,httpd_post_create_state就是创建链表节点的函数。链表头是全局变量httpd_post_list,其初始值为NULL,代表这是一个空链表。当第一个post请求到来时,用mem_malloc分配第一个链表节点,用state表示,httpd_post_list=state, state->next=NULL。后面又来了第二个post请求,那么就又分配了个state2。此时httpd_post_list=state, state->next=state2, state2->next=NULL。每次都是把新节点插入到链表末尾。
我们在分配内存块的时候,分配的内存大小是sizeof(struct httpd_post_state) + content_len + 1。不仅为struct httpd_post_state结构体分配了内存,还同时为表单内容分配了内存。content_len是表单内容的总大小,后面再加1是为了存放字符串结束符'/0'。这样做的好处是两个内容在同一块连续的内存上,后面删除链表节点的时候就只用释放一次内存,不用先释放struct httpd_post_state再释放content内存。
state->content = (char *)(state + 1);这句话就是把struct httpd_post_state结构体后面多分配出来的content_len + 1字节的内存的首地址赋给state->content成员变量,方便访问。
(char *)(state + 1)就是(char *)&state[1]的意思。这里+1加的可不是1字节,而是加的sizeof(struct httpd_post_state)字节,请一定要和((char *)state + 1)区分开。((char *)state + 1)加的是sizeof(char)=1字节,(char *)(state + 1)加的是sizeof(state)=sizeof(struct httpd_post_state)字节。(有点绕,如果不能理解的话记住这个结论就行了)
因此,state->content指向的内存块的大小为content_len + 1字节,state->content[content_len] = '/0';这句话就是把那最后一字节赋上字符串结束符'/0'。 

httpd_post_find_state函数是根据connection连接标识寻找struct httpd_post_state *链表节点的函数,函数的返回值是找到的链表节点。
httpd_post_delete_state函数就是在post请求处理结束时删除链表节点并释放内存的函数。

接下来我们来实现lwip post功能要求我们实现的那三个函数。请看代码:

#define STRPTR(s) (((s) != NULL) ? (s) : "(null)")/* 开始处理http post请求*/err_t httpd_post_begin(void *connection, const char *uri, const char *http_request, u16_t http_request_len, int content_len, char *response_uri, u16_t response_uri_len, u8_t *post_auto_wnd){  struct httpd_post_state *state;    printf("[httpd_post_begin] connection=0x%p, uri=%s/n", connection, uri);  printf("%.*s/n", http_request_len, http_request);  if (strcmp(uri, "/form_test.html") != 0)  {    //strlcpy(response_uri, "/bad_request.html", response_uri_len);    return ERR_ARG;  }    state = httpd_post_create_state(connection, content_len);  if (state == NULL)  {    //strlcpy(response_uri, "/out_of_memory.html", response_uri_len);    return ERR_MEM;  }  state->multipart = httpd_is_multipart(http_request, http_request_len);  if (state->multipart)  {    //strlcpy(response_uri, "/bad_request.html", response_uri_len);    httpd_post_delete_state(state);    return ERR_ARG;  }  return ERR_OK;}/* 接收表单数据 */err_t httpd_post_receive_data(void *connection, struct pbuf *p){  int len;  struct httpd_post_state *state;  struct pbuf *q;    printf("[httpd_post_receive_data] connection=0x%p, payload=0x%p, len=%d/n", connection, p->payload, p->tot_len);  for (q = p; q != NULL; q = q->next)    printf("%.*s", q->len, (char *)q->payload);  printf("/n");    state = httpd_post_find_state(connection);  if (state != NULL)  {    len = p->tot_len;    if (state->content_pos + len > state->content_len)    {      // (兼容IE6) 忽略尾部多余的/r/n, 防止缓冲区溢出      printf("[httpd_post_receive_data] ignored the last %d byte(s)/n", state->content_pos + len - state->content_len);      len = state->content_len - state->content_pos;    }    pbuf_copy_partial(p, state->content + state->content_pos, (u16_t)len, 0);    state->content_pos += len;    pbuf_free(p);  }  return ERR_OK;}/* 结束处理http post请求*/void httpd_post_finished(void *connection, char *response_uri, u16_t response_uri_len){  int i;  struct httpd_post_state *state;    printf("[httpd_post_finished] connection=0x%p/n", connection);  state = httpd_post_find_state(connection);  if (state != NULL)  {    httpd_post_parse(state);        printf("param_count=%d/n", state->param_count);    for (i = 0; i < state->param_count; i++)      printf("[Param] name=%s, value=%s/n", state->params[i], STRPTR(state->values[i]));    httpd_post_delete_state(state);    //strlcpy(response_uri, "/success.html", response_uri_len);  }}

在httpd_post_begin函数中,首先判断网页名称(uri变量)是不是正确的,如果不正确就返回ERR_ARG错误码。
在response_uri没有赋值的情况下,返回非ERR_OK值后显示的是404错误页面,如果response_uri赋值了的话就是显示response_uri字符串指定的那个错误页面(比如可以赋值为/bad_request.html),lwip不再调用后续的httpd_post_receive_data和httpd_post_finished函数。
网页名称uri是正确的话就继续往后执行,调用刚才定义的httpd_post_create_state函数创建链表节点,如果链表节点创建失败同样要报错。创建成功的话就用httpd_is_multipart函数判断一下当前表单是普通表单还是文件上传表单,如果是文件上传表单(函数返回1)则报错。
httpd_is_multipart函数的实现如下:

/* 根据http header判断当前表单是否为文件上传表单 */static int httpd_is_multipart(const char *http_request, int http_request_len){  char value[100];  char *s = "multipart/form-data";    httpd_get_header(http_request, http_request_len, "Content-Type", value, sizeof(value));  return (strncasecmp(value, s, strlen(s)) == 0);}/* 在http header中找出指定名称属性的值 */static int httpd_get_header(const char *http_request, int http_request_len, const char *name, char *valuebuf, int bufsize){  const char *endptr;  int linelen, namelen, valuelen;    namelen = strlen(name);  while (http_request_len != 0)  {    endptr = lwip_strnstr(http_request, "/r/n", http_request_len);    if (endptr != NULL)    {      linelen = endptr - http_request;      endptr += 2;    }    else    {      linelen = http_request_len;      endptr = http_request + http_request_len;    }        if (strncasecmp(http_request, name, namelen) == 0 && http_request[namelen] == ':')    {      http_request += namelen + 1;      linelen -= namelen + 1;      while (*http_request == ' ')      {        http_request++;        linelen--;      }            valuelen = linelen;      if (valuelen > bufsize - 1)        valuelen = bufsize - 1;      memcpy(valuebuf, http_request, valuelen);      valuebuf[valuelen] = '/0';      return linelen;    }        http_request_len -= endptr - http_request;    http_request = endptr;  }    valuebuf[0] = '/0';  return -1;}

其实就是看http header里面的Content-Type属性的值是否以multipart/form-data开头,如果是的话那就是文件上传表单。

httpd_post_receive_data函数是接收post表单内容的函数,函数把接收到的表单内容p通过pbuf_copy_partial函数复制到state->content缓冲区里面,注意防止缓冲区溢出。pbuf_copy_partial函数就是把struct pbuf *链表里面所有的payload内容复制到一个数组中。

httpd_post_finished函数是在接收完所有表单内容后调用的,在函数里面用httpd_post_parse函数解析表单内容,把state->content里面存的表单控件名和控件值分离出来,存到struct httpd_post_state结构体的params和values里面。
httpd_post_parse函数的代码如下:

/* 从普通表单内容中分离出控件名称和控件内容 */static int httpd_post_parse(struct httpd_post_state *state){  char *p;  int i, count;    if (state == NULL || state->param_count != 0 || state->params != NULL || state->values != NULL)    return -1;  else if (state->multipart || state->content_pos != state->content_len)    return -1;    p = state->content;  count = 0;  while (p != NULL && *p != '/0')  {    count++;    p = strchr(p, '&');    if (p != NULL)    {      *p = '/0';      p++;    }  }    if (count > 0)  {    state->params = (char **)mem_malloc(2 * count * sizeof(char *));    if (state->params == NULL)      return -1;    state->values = state->params + count;        p = state->content;    for (i = 0; i < count; i++)    {      state->params[i] = p;      state->values[i] = strchr(p, '=');      p += strlen(p) + 1;            if (state->values[i] != NULL)      {        *state->values[i] = '/0';        state->values[i]++;      }            urldecode(state->params[i]);      if (state->values[i] != NULL)        urldecode(state->values[i]);    }  }    state->param_count = count;  return count;}

post请求到这里就处理结束了,如果在httpd_post_finished函数中没有对response_uri字符数组赋值的话,最终浏览器显示的页面为404错误页面,提示找不到网页。如果对response_uri赋了值,那么就显示response_uri字符串指定的网页。

HTML网页的名称为form_test.html,放到lwip-2.1.3/apps/http/fs文件夹下并用makefsdata.exe程序打包。网页的内容如下:

<!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 .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 action="" method="post" name="form1">  <h1>表单测试</h1>  <p>    <label class="left-block" for="textfield">文本框1:</label>    <input type="text" name="textfield" id="textfield" class="textfield" value="">  </p>  <p>    <label class="left-block" for="textfield2">文本框2:</label>    <input type="text" name="textfield2" id="textfield2" class="textfield" value="">  </p>  <p>    <label class="left-block" for="textfield3">文本框3:</label>    <input type="text" name="textfield3" id="textfield3" class="textfield" value="">  </p>  <p>    <span class="left-block">单选框:</span>    <input name="radio" type="radio" id="radio" value="gpio" checked="checked"><label for="radio">GPIO</label>    <input type="radio" name="radio" id="radio2" value="adc"><label for="radio2">ADC</label>    <input type="radio" name="radio" id="radio3" value="dma"><label for="radio3">DMA</label>  </p>  <p>    <span class="left-block">复选框:</span>    <input type="checkbox" name="checkbox" id="checkbox"><label for="checkbox">FSMC</label>    <input type="checkbox" name="checkbox2" id="checkbox2"><label for="checkbox2">SDIO</label>    <input type="checkbox" name="checkbox3" id="checkbox3"><label for="checkbox3">USB</label>  </p>  <p>    <label class="left-block" for="select">下拉菜单框:</label>    <select name="select" id="select">      <option value="f103c8">STM32F103C8</option>      <option value="f107vc">STM32F107VC</option>      <option value="h743vi">STM32H743VI</option>      <option value="l476rg">STM32L476RG</option>    </select>  </p>  <p>    <label class="left-block" for="fileField">文件框:</label>    <input type="file" name="fileField" id="fileField" class="textfield">  </p>  <p>    <label class="left-block" for="textarea">内容框:</label>    <textarea name="textarea" id="textarea" rows="10"></textarea>  </p>  <p>    <span class="left-block"></span>    <input type="submit" value="提交">  </p></form></body></html>

我们来看看程序的运行结果:

点击提交按钮提交表单后,浏览器的网址没变,但是提示404错误(找不到网页),这是httpd_post_finished函数没有对response_uri字符数组赋值造成的,是正常现象
在串口输出中我们可以看到提交的表单内容。其中包括三个文本框输入的内容,还有单选框选择的项目,还有勾选了的复选框。没有勾选的复选框不会出现在表单内容中。
表单内容还有下拉菜单框选择的项目,以及文件选择框选择的文件名(不含文件路径和文件内容),最后是多行文本框输入的内容(换行也正确显示了)。

IE6下的运行结果:
注意看,httpd_post_receive_data收到的字节数比Content-Length多两个字节,要注意防止malloc缓冲区溢出。

 

POST参数传递到SSI动态页面

(本节例程名称:post_test2)
POST请求的处理到httpd_post_finished函数就结束了,之后显示的页面由response_uri字符串的内容决定。如果我们想把struct httpd_post_state里面存的params和values的内容传递过去,并显示到网页上,该怎么传过去呢?
我们可以把state指针的地址用snprintf函数通过0x%p打印到response_uri字符串上。比如当state=0x20001234时,response_uri="/success.ssi?state=0x20001234",不释放state所占用的内存,仍然保留在httpd_post_list链表中,但要把connection成员置为空指针NULL。
在lwipopts.h中,把CGI(新式)、SSI和FILE_STATE功能都打开。

#define MEM_SIZE 25600 // lwip的mem_malloc函数使用的堆内存的大小// 配置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_MULTIPART 1#define LWIP_HTTPD_SSI_RAW 1#define LWIP_HTTPD_SUPPORT_POST 1

httpd_post_finished函数执行结束后,lwip会去打开/success.ssi文件,并执行fs_state_init函数,我们在其中创建一个fs_state结构体。
之后,lwip调用httpd_cgi_handler函数解析URL里面的state参数,解析出来后我们就拿到了原来的struct httpd_post_state(下面简称post_state)结构体指针。在httpd_cgi_handler函数里面我们将fs_state和post_state结构体绑定在一起。
然后,lwip会调用SSI的回调函数test_ssi_handler,传入的connection_state参数就是fs_state,通过fs_state可以拿到post_state(就是最开始httpd_post_finished里面的那个state指针),就可以在SSI标签上显示post表单数据了。
网页内容生成完毕后,lwip会调用fs_state_free函数,我们可以在这里面释放掉fs_state和post_state结构体,并把post_state从httpd_post_list链表中移除。

值得注意的是,我们在httpd_post_finished函数里面把state打印到response_uri上后,由于可能会发生tcp_err错误或者出现mem_malloc失败的情况,lwip有可能不会去打开response_uri指定的文件,这将导致内存泄露。所以我们需要一定的超时机制,如果链表里面的post_state结构体在connection置为空指针后超过5秒钟还没有和fs_state结构体绑定,那就认为超时了,直接强制释放post_state结构体。这项检测我们可以放到httpd_post_begin里面进行,每当有新客户端连接的时候都要检查一下整个链表是否有节点超时。
另外,由于用户可以直接在浏览器里面输入success.ssi的网址访问,比如http://stm32f103ze/success.ssi?state=0x12345678这样的网址,其中state是一个无效的指针。为了防止STM32单片机出现HardFault错误,我们从URL取出state指针值后,一定要去链表上搜索一遍,看看这个指针是不是真的在链表上。如果不在链表上,那就是一个无效指针,不予处理。

整个过程还是比较复杂的,大概是这样的一条路径:POST->FILE_STATE(init)->CGI->SSI->FILE_STATE(free)。
让我们来看看代码吧,先看一下新添加的success.ssi动态网页,里面包含了filename和content这两个SSI标签。注意content这个标签是直接放在网页上的,没有放到多行文本框里面,所以待会儿在用htmlspecialchars的时候一定要记得nbsp参数要设置为1,把所有的空格都要替换为&nbsp;。

<!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></head><body><h1>表单提交成功</h1><hr><p><b>您选择的文件是: </b><!--#filename--></p><p>  <b>您在多行文本框中输入的内容为: </b><br>  <!--#content--></p></body></html>

新定义了struct httpd_fs_state结构体,原来的struct httpd_post_state结构体新增加了fs_state和post_finish_time成员,这两个成员分别记录的是绑定的httpd_fs_state对象和post请求处理完成的时间。
新增了httpd_post_is_valid_state函数,用于判断httpd_post_state指针是否有效,也就是说能不能在httpd_post_list链表中找到指定的httpd_post_state指针。

struct httpd_fs_state{  struct httpd_post_state *post_state;  char *filename;  char *content;};struct httpd_post_state{  struct httpd_post_state *next; // 链表的下一个节点  struct httpd_fs_state *fs_state;  void *connection; // http连接标识  char *content; // 表单内容  int content_len; // 表单长度  int content_pos; // 已接收的表单内容的长度  int multipart; // 是否为文件上传表单  char **params; // 表单控件名列表  char **values; // 表单控件值表  int param_count; // 表单控件个数  uint32_t post_finish_time; // post请求处理完成的时间};/* 判断state是否在链表中 */static int httpd_post_is_valid_state(struct httpd_post_state *state){  struct httpd_post_state *p;    for (p = httpd_post_list; p != NULL; p = p->next)  {    if (p == state)      return 1;  }  return 0;}

当post请求处理结束时,我们在httpd_post_finished函数中把state->connection置为NULL,把state指针的地址用snprintf函数打印到response_uri字符串上,通过URL参数传递给后续要显示的页面。state对象暂不释放,也暂不从链表上移除。后续要显示的页面是success.ssi。
如果内存充足,且网络没有出错(如tcp_err),lwip就会去打开response_uri字符串指定的success.ssi网页,调用fs_state_init函数。我们在fs_state_init函数中建立一个空的fs_state对象。这个时候由于URL参数还没开始解析,我们是不知道刚才state(后面改称为post_state)指针的地址的,所以只能建立一个空的fs_state对象放在那里。fs_state和post_state对象在网页内容生成完毕后在fs_state_free函数中一起释放。

#define STRPTR(s) (((s) != NULL) ? (s) : "(null)")/* 结束处理http post请求*/void httpd_post_finished(void *connection, char *response_uri, u16_t response_uri_len){  int i;  struct httpd_post_state *state;    printf("[httpd_post_finished] connection=0x%p/n", connection);  state = httpd_post_find_state(connection);  if (state != NULL)  {    httpd_post_parse(state);        printf("param_count=%d/n", state->param_count);    for (i = 0; i < state->param_count; i++)      printf("[Param] name=%s, value=%s/n", state->params[i], STRPTR(state->values[i]));        state->connection = NULL; // 与connection脱离关系    state->post_finish_time = sys_now(); // 记录当前时间    snprintf(response_uri, response_uri_len, "/success.ssi?state=0x%p", state); // 将state指针通过url参数传递给ssi动态页面    printf("response_uri=%s/n", response_uri);  }}void *fs_state_init(struct fs_file *file, const char *name){  struct httpd_fs_state *fs_state = NULL;    if (strcmp(name, "/success.ssi") == 0)  {    fs_state = mem_malloc(sizeof(struct httpd_fs_state));    if (fs_state != NULL)    {      printf("%s: new fs_state(0x%p)/n", __func__, fs_state);      memset(fs_state, 0, sizeof(struct httpd_fs_state));    }    else      printf("%s: mem_malloc() failed/n", __func__);  }  return fs_state;}void fs_state_free(struct fs_file *file, void *state){  struct httpd_fs_state *fs_state = state;    if (fs_state != NULL)  {    if (fs_state->post_state != NULL)    {      printf("%s: delete post_state(0x%p)/n", __func__, fs_state->post_state);      httpd_post_delete_state(fs_state->post_state);      fs_state->post_state = NULL;    }        printf("%s: delete fs_state(0x%p)/n", __func__, fs_state);    if (fs_state->filename != NULL)    {      mem_free(fs_state->filename);      fs_state->filename = NULL;    }    if (fs_state->content != NULL)    {      mem_free(fs_state->content);      fs_state->content = NULL;    }    mem_free(fs_state);  }}

接下来lwip开始解析URL参数,并调用httpd_cgi_handler函数。在httpd_cgi_handler函数中接收到success.ssi页面的state值后,就可以得到struct httpd_post_state *指针了,这正是刚才httpd_post_finished函数通过URL参数传递的state对象。
得到指针后先判断一下是否在httpd_post_list链表中,如果在链表中,说明post_state是一个有效的指针。如果不在链表上,那就是一个无效的指针。
post_state判定为有效指针后,就把这个post_state对象和刚才在fs_state_init里面创建的fs_state对象绑定。
后面lwip在替换SSI标签内容的时候,会调用test_ssi_handler函数,从connection_state参数中取出fs_state对象,再找到绑定的post_state对象,就可以得到post表单提交的数据了。注意在显示的时候htmlspecialchars的nbsp参数要设为1,这样才能正确显示空格和tab字符。

fs_state_init里面用mem_malloc创建fs_state对象有可能失败。失败了的话,lwip还是会调用httpd_cgi_handler、test_ssi_handler和fs_state_free函数。在httpd_cgi_handler函数里面解析state参数的时候,如果发现fs_state对象没有创建成功,就应该直接删除post_state对象。在test_ssi_handler函数里面判断到fs_state为空,也不会执行任何操作。

void httpd_cgi_handler(struct fs_file *file, const char *uri, int iNumParams, char **pcParam, char **pcValue, void *connection_state){  char *p;  int i, j;  struct httpd_fs_state *fs_state = connection_state;  struct httpd_post_state *post_state;  uintptr_t ptr;    if (strcmp(uri, "/success.ssi") != 0)    return; // 这里只用判断uri是否正确, 不用判断fs_state是否为NULL, fs_state=NULL的情况在后面的代码中处理    for (i = 0; i < iNumParams; i++)  {    if (strcmp(pcParam[i], "state") == 0)    {      ptr = strtol(pcValue[i], NULL, 16);      post_state = (struct httpd_post_state *)ptr;      if (httpd_post_is_valid_state(post_state))      {        printf("%s: valid post state 0x%p from URL parameter/n", __func__, post_state);        if (fs_state != NULL)        {          fs_state->post_state = post_state;          post_state->fs_state = fs_state;                    for (j = 0; j < post_state->param_count; j++)          {            // 在网页上直接显示, 必须要把空格转成&nbsp;, 所以htmlspecialchars的参数nbsp要设为1            // 如果是在多行文本框内显示的话, 一定不能把空格转成&nbsp;(否则表单提交后会出错), 参数nbsp要设为0            if (strcmp(post_state->params[j], "fileField") == 0)              fs_state->filename = htmlspecialchars(post_state->values[j], 1);            else if (strcmp(post_state->params[j], "textarea") == 0)            {              p = htmlspecialchars(post_state->values[j], 1);              if (p != NULL)              {                fs_state->content = nl2br(p);                mem_free(p);              }            }          }        }        else        {          // 刚才在fs_state_init函数中mem_malloc分配内存失败, fs_state对象没有创建成功          // fs_state为NULL, 删除post_state对象          printf("%s: delete post_state(0x%p)/n", __func__, post_state);          httpd_post_delete_state(post_state);        }      }      else      {        // URL参数传入的是无效的state指针        printf("%s: invalid post state 0x%p from URL parameter/n", __func__, post_state);      }      break;    }  }}static u16_t test_ssi_handler(const char *ssi_tag_name, char *pcInsert, int iInsertLen, u16_t current_tag_part, u16_t *next_tag_part, void *connection_state){  struct httpd_fs_state *fs_state = connection_state;  u16_t curr, len;    if (fs_state != NULL && fs_state->post_state != NULL)  {    if (strcmp(ssi_tag_name, "filename") == 0)    {      if (fs_state->filename != NULL)      {        strlcpy(pcInsert, fs_state->filename, iInsertLen);        return strlen(pcInsert);      }    }    else if (strcmp(ssi_tag_name, "content") == 0)    {      if (fs_state->content != NULL)      {        len = strlen(fs_state->content);        curr = len - current_tag_part;        if (curr > iInsertLen - 1)        {          curr = iInsertLen - 1;          *next_tag_part = current_tag_part + curr;        }        memcpy(pcInsert, fs_state->content + current_tag_part, curr);        pcInsert[curr] = '/0';        return curr;      }    }  }  return HTTPD_SSI_TAG_UNKNOWN;}

如果含有state指针的response_uri字符串交给lwip后,lwip因为某些原因没有去打开response_uri字符串指定的success.ssi网页,为了避免内存泄露,我们需要再写一个httpd_post_cleanup函数,搜索httpd_post_list链表上所有5秒内没有和fs_state对象完成绑定的节点,将这些节点强制删除。我们选择在httpd_post_begin里面调用httpd_post_cleanup函数,每次有新post请求到来的时候都清理一下链表。

/* 清除已处理完post请求却没有打开SSI网页的post_state结构体 */static void httpd_post_cleanup(void){  struct httpd_post_state *p, *q;  uint32_t now;    now = sys_now();  p = httpd_post_list;  while (p != NULL)  {    q = p->next;    if (p->connection == NULL && p->fs_state == NULL && now - p->post_finish_time >= 5000)    {      printf("%s: delete post_state(0x%p)/n", __func__, p);      httpd_post_delete_state(p);    }    p = q;  }}/* 开始处理http post请求*/err_t httpd_post_begin(void *connection, const char *uri, const char *http_request, u16_t http_request_len, int content_len, char *response_uri, u16_t response_uri_len, u8_t *post_auto_wnd){  struct httpd_post_state *state;    printf("[httpd_post_begin] connection=0x%p, uri=%s/n", connection, uri);  httpd_post_cleanup();  ...}

程序运行结果:

可以在多行文本框里面提交一段很长的文本,只要lwip的内存(MEM_SIZE)够大就能显示成功。换行符、空格和tab字符也能正确显示。

文件上传类型POST表单的解析

(本节例程名称:post_test3)
普通POST表单只会提交文件框里面所选文件的文件名,不会上传文件的内容。如果要想上传文件内容的话就得使用文件上传类型的POST表单,在<form>标签上添加enctype="multipart/form-data"属性。这种文件上传类型的POST表单提交后的内容格式和普通表单完全不一样,需要单独处理。

我们先来看一下文件上传表单提交后的http header内容。

HTTP/1.1Accept: */*Referer: http://stm32f103ze/form_test.htmlAccept-Language: zh-cnUser-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; InfoPath.3; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727; .NET CLR 1.1.4322)Content-Type: multipart/form-data; boundary=---------------------------7e729f1bf0a7aAccept-Encoding: gzip, deflateHost: stm32f103zeContent-Length: 39415Connection: Keep-AliveCache-Control: no-cache

其中Content-Type以multipart/form-data开头,后面还有一个boundary字符串,叫做分界符字符串。这个分界符字符串非常重要,分界符字符串是表单内容中各个控件内容的分界符。
Content-Length是整个表单内容(包括文件内容)的总长度。lwip调用httpd_post_begin函数时传入的content_len参数就等于http header中Content-Length属性的数值。

我们再来看一下表单内容的格式。

可以看到,每个表单控件都是用分界符字符串隔开的。整个表单以分界符字符串开始,结束也是靠的分界符字符串,只不过结束时多了两个横杠。
分界符字符串后面是控件属性区,里面存放的是控件名和文件属性。对于普通表单控件(如文本框、单选框、复选框、下拉菜单框等等),控件属性区只有name字符串,表示控件名。对于文件框控件,控件属性区除了name字符串外,还有filename字符串和Content-Type属性。filename字符串表示文件框选择的文件名,有的浏览器提供了完整的文件路径和文件名,有的浏览器就只提供了文件名。Content-Type属性表示的是文件的类型,比如image/pjpeg是jpg图片文件,这个数据也是由浏览器提供的。
name和filename的值是用双引号括起来的,所以值里面是不允许有双引号出现的。如果用户上传了名叫".txt的文件(Windows系统不允许文件名里面有双引号,但Linux系统允许),那么其中的双引号会被强制转换为%22这三个字符,除了双引号字符外,其他所有的字符都不会转换。所以,上传".txt和%22.txt这两个文件后,得到的文件名都是%22.txt。由于程序在收到%22这三个字符后无法区分原始内容是"还是%22,所以我们选择不对%22进行解码操作。文件名里面是允许有分号出现的,分号也不会被转换,在程序里面我们不能直接用strtok_r或explode函数按分号把字符串拆分成数组,只能直接用for循环遍历字符串,双引号内的分号必须忽略。

控件属性区完了之后再隔一个空行就是控件内容区了。普通表单控件的控件内容区存放的是控件里面填写的内容,文件框控件的控件内容区存放的是文件的内容。控件内容区是以原文形式存储的,是没有进行urlencode编码的,这和普通表单内容的格式很不一样。控件内容区里面任意二进制内容都可以出现,包括/r/n换行符甚至/0符,所以程序里面有一个很重要的任务就是正确区分某个/r/n到底是不是控件或文件里面的内容。一般来说,如果读到的/r/n后面恰好是分界符字符串,那么这个/r/n仅仅是表单的换行符,并不是控件或文件里面的内容。如果读到的/r/n后面不是分界符字符串,那么这个/r/n就属于控件或文件内容。

lwip2.1版本相比2.0版本新增了一个名叫pbuf_free_header的函数,这个函数非常有用,可以只释放pbuf的前面一部分,这给长文本的分行解析带来了很大的方便。比如某一次TCP接收,收到的是第一行、第二行的全部内容和第三行的一部分内容,那么我们在解析完前面两行后,调用pbuf_free_header释放前面两行所占用的空间,未接收完全的第三行暂时无法解析,就仍然留在pbuf里面。这样我们就可以实现一边接收TCP数据,一边一行一行地解析。不用把整个内容接收完了之后才来慢慢解析。pbuf_free_header函数的原型如下:
struct pbuf *pbuf_free_header(struct pbuf *q, u16_t size);
不过要特别注意的是,pbuf_free_header函数有可能会改变q的地址。假设q是由三段内存组成的:0~9、10~14、15~19,共20字节。我们如果释放前面12个字节的话,那么第一块内存就会被完全释放掉,第二块内存只释放了一半,q将变为指向第二块内存,由函数的返回值返回。如果size参数大于或等于q的总大小的话,那么q就会被完全释放,函数返回NULL,这就相当于是调用了pbuf_free函数了。

我们来看看完整的文件上传代码,大约一共有900行:

#include <ff.h>#include <lwip/apps/httpd.h>#include <lwip/def.h>#include <lwip/mem.h>#include <string.h>#include <time.h>#include "strutil.h"#include "test.h"struct httpd_post_multipart_state{  struct pbuf *p; // 未处理的数据  int reading_content; // 当前是否正在处理表单控件内容或文件内容  uint64_t content_len; // 已读取的控件内容或文件内容的长度  char boundary[70]; // 边界字符串  int boundary_len; // 边界字符串的长度  int is_file; // 当前是在读取文件内容还是控件内容 (0:控件内容, 其他值:文件内容)  char *filename; // 当前正在读取的文件的文件名  FIL fil; // 打开的文件  int crlf; // 控件内容或文件内容中是否还有未保存的/r/n  char *strbuf; // 存放解析出来的所有字符串  int strbuf_used; // 字符串缓冲区的已用空间  int strbuf_capacity; // 字符串缓冲区的容量};struct httpd_post_file{  char *name; // 文件名  char *path; // 保存路径  uint64_t size; // 文件大小  char *mimetype; // 文件类型};struct httpd_post_state{  struct httpd_post_state *next; // 链表的下一个节点  void *connection; // http连接标识  char *content; // 表单内容  int content_len; // 表单长度  int content_pos; // 已接收的表单内容的长度  struct httpd_post_multipart_state *multipart; // 文件上传状态  char **params; // 表单控件名列表  char **values; // 表单控件值表  struct httpd_post_file *files; // 文件列表  int param_count; // 表单控件个数};static struct httpd_post_state *httpd_post_create_state(void *connection, int content_len, int multipart);static struct httpd_post_state *httpd_post_find_state(void *connection);static void httpd_post_delete_state(struct httpd_post_state *state);static int httpd_get_header(const char *http_request, int http_request_len, const char *name, char *valuebuf, int bufsize);static int httpd_is_multipart(const char *http_request, int http_request_len);static int httpd_post_parse(struct httpd_post_state *state);static int httpd_post_parse_multipart(struct httpd_post_state *state);static err_t httpd_post_process_multipart(struct httpd_post_state *state, struct pbuf *p);static err_t httpd_post_process_multipart_line(struct httpd_post_state *state, int linelen);static int httpd_post_generate_file_path(const char *srcname, char *buffer, int bufsize);static err_t httpd_post_store_multipart_string(struct httpd_post_multipart_state *multipart, char flag, const char *str, int len, char **strout);static struct httpd_post_state *httpd_post_list; // 保存http post请求数据的链表/* 为新http post请求创建链表节点 */static struct httpd_post_state *httpd_post_create_state(void *connection, int content_len, int multipart){  struct httpd_post_state *state, *p;    LWIP_ASSERT("connection != NULL", connection != NULL);  LWIP_ASSERT("connection is new", httpd_post_find_state(connection) == NULL);  LWIP_ASSERT("content_len >= 0", content_len >= 0);    if (!multipart)    state = mem_malloc(sizeof(struct httpd_post_state) + content_len + 1);  else    state = mem_malloc(sizeof(struct httpd_post_state) + sizeof(struct httpd_post_multipart_state));  if (state == NULL)  {    printf("%s: mem_malloc() failed/n", __func__);    return NULL;  }  memset(state, 0, sizeof(struct httpd_post_state));  state->connection = connection; // http连接标识  if (!multipart)  {    state->content = (char *)(state + 1); // 指向结构体后面content_len+1字节的内存空间, 用于保存收到的表单内容    state->content[content_len] = '/0'; // 字符串结束符  }  else  {    state->multipart = (struct httpd_post_multipart_state *)(state + 1);    memset(state->multipart, 0, sizeof(struct httpd_post_multipart_state));  }  state->content_len = content_len; // 表单内容长度    // 将新分配的节点添加到链表末尾  if (httpd_post_list != NULL)  {    // 找到尾节点    for (p = httpd_post_list; p->next != NULL; p = p->next);    // 将state挂到尾节点的后面, 成为新的尾节点    p->next = state;  }  else    httpd_post_list = state; // 链表为空, 直接赋值, 成为第一个节点  return state;}/* 根据http连接标识找到链表节点 */static struct httpd_post_state *httpd_post_find_state(void *connection){  struct httpd_post_state *p;    LWIP_ASSERT("connection != NULL", connection != NULL);    for (p = httpd_post_list; p != NULL; p = p->next)  {    if (p->connection == connection)      break;  }  return p;}/* 从链表中删除节点 */static void httpd_post_delete_state(struct httpd_post_state *state){  struct httpd_post_state *p;    LWIP_ASSERT("state != NULL", state != NULL);    if (httpd_post_list != state)  {    // 找到当前节点的前一个节点    for (p = httpd_post_list; p != NULL && p->next != state; p = p->next)    LWIP_ASSERT("p != NULL", p != NULL);    // 从链表中移除    p->next = state->next;  }  else    httpd_post_list = state->next;    // 释放节点所占用的内存空间  state->next = NULL;  state->connection = NULL;  state->content = NULL;  if (state->multipart != NULL)  {    LWIP_ASSERT("state->multipart->p == NULL", state->multipart->p == NULL);    LWIP_ASSERT("file has been closed", state->multipart->is_file == 0);    if (state->multipart->strbuf != NULL)    {      mem_free(state->multipart->strbuf);      state->multipart->filename = NULL;      state->multipart->strbuf = NULL;    }    state->multipart = NULL;  }  if (state->params != NULL)  {    mem_free(state->params);    state->params = NULL;    state->values = NULL;    state->files = NULL;  }  mem_free(state);}/* 在http header中找出指定名称属性的值 */static int httpd_get_header(const char *http_request, int http_request_len, const char *name, char *valuebuf, int bufsize){  const char *endptr;  int linelen, namelen, valuelen;    namelen = strlen(name);  while (http_request_len != 0)  {    endptr = lwip_strnstr(http_request, "/r/n", http_request_len);    if (endptr != NULL)    {      linelen = endptr - http_request;      endptr += 2;    }    else    {      linelen = http_request_len;      endptr = http_request + http_request_len;    }        if (strncasecmp(http_request, name, namelen) == 0 && http_request[namelen] == ':')    {      http_request += namelen + 1;      linelen -= namelen + 1;      while (*http_request == ' ')      {        http_request++;        linelen--;      }            valuelen = linelen;      if (valuelen > bufsize - 1)        valuelen = bufsize - 1;      memcpy(valuebuf, http_request, valuelen);      valuebuf[valuelen] = '/0';      return linelen;    }        http_request_len -= endptr - http_request;    http_request = endptr;  }    valuebuf[0] = '/0';  return -1;}/* 根据http header判断当前表单是否为文件上传表单 */static int httpd_is_multipart(const char *http_request, int http_request_len){  char value[100];  char *s = "multipart/form-data";    httpd_get_header(http_request, http_request_len, "Content-Type", value, sizeof(value));  return (strncasecmp(value, s, strlen(s)) == 0);}/* 从普通表单内容中分离出控件名称和控件内容 */static int httpd_post_parse(struct httpd_post_state *state){  char *p;  int i, count;    if (state == NULL || state->param_count != 0 || state->params != NULL || state->values != NULL)    return -1;  else if (state->multipart != NULL || state->content_pos != state->content_len)    return -1;    p = state->content;  count = 0;  while (p != NULL && *p != '/0')  {    count++;    p = strchr(p, '&');    if (p != NULL)    {      *p = '/0';      p++;    }  }    if (count > 0)  {    state->params = (char **)mem_malloc(2 * count * sizeof(char *));    if (state->params == NULL)    {      printf("%s: mem_malloc() failed/n", __func__);      return -1;    }    state->values = state->params + count;        p = state->content;    for (i = 0; i < count; i++)    {      state->params[i] = p;      state->values[i] = strchr(p, '=');      p += strlen(p) + 1;            if (state->values[i] != NULL)      {        *state->values[i] = '/0';        state->values[i]++;      }            urldecode(state->params[i]);      if (state->values[i] != NULL)        urldecode(state->values[i]);    }  }    state->param_count = count;  return count;}/* 分离出文件上传表单的控件名称和控件内容 */static int httpd_post_parse_multipart(struct httpd_post_state *state){  char *p;  int i, count, memsize;  void *mem;  FRESULT fr;  if (state == NULL || state->param_count != 0 || state->params != NULL || state->values != NULL)    return -1;  else if (state->multipart == NULL)    return -1;    if (state->multipart->is_file != 0)  {    // 关闭文件    if (state->multipart->is_file == 2)    {      printf("%s: close file. size=%llu/n", __func__, state->multipart->content_len);      f_close(&state->multipart->fil);    }    state->multipart->is_file = 0;  }  if (state->content_pos == state->content_len)    printf("%s: fully processed/n", __func__);  else  {    printf("%s: partly processed (%d/%d)/n", __func__, state->content_pos, state->content_len);    goto err;  }  count = 0;  for (p = state->multipart->strbuf; p - state->multipart->strbuf < state->multipart->strbuf_used; p += strlen(p) + 1)  {    if (*p == 'n')      count++;  }  if (count > 0)  {    memsize = 2 * count * sizeof(char *) + count * sizeof(struct httpd_post_file);    mem = mem_malloc(memsize);    if (mem == NULL)    {      printf("%s: mem_malloc() failed/n", __func__);      goto err;    }    memset(mem, 0, memsize);    state->params = (char **)mem;    state->values = state->params + count;    state->files = (struct httpd_post_file *)(state->values + count);        i = -1;    for (p = state->multipart->strbuf; p - state->multipart->strbuf < state->multipart->strbuf_used; p += strlen(p) + 1)    {      if (*p == 'n')      {        i++;        state->params[i] = p + 1;      }      else if (i >= 0)      {        switch (*p)        {          case 'f':            state->files[i].name = p + 1;            // 继续往下执行          case 'c':            state->values[i] = p + 1;            break;          case 'p':            state->files[i].path = p + 1;            break;          case 's':            state->files[i].size = strtoull(p + 1, NULL, 10);            break;          case 't':            state->files[i].mimetype = p + 1;            break;        }      }    }  }  state->param_count = count;  return count;  err:  // 出错时删除所有上传的文件  for (p = state->multipart->strbuf; p - state->multipart->strbuf < state->multipart->strbuf_used; p += strlen(p) + 1)  {    if (*p == 'p')    {      printf("%s: delete uploaded file %s/n", __func__, p + 1);      fr = f_unlink(p + 1);      if (fr != FR_OK)        printf("%s: f_unlink() failed. fr=%d/n", __func__, fr);    }  }  return -1;}/* 接收并处理文件上传表单的数据 */static err_t httpd_post_process_multipart(struct httpd_post_state *state, struct pbuf *p){  err_t err;  int linelen;  int no_more_data = (p == NULL); // 当前是否为最后一次处理  uint16_t pos;  if (!no_more_data)  {    // 将新收到的数据包和前面没有处理的数据包拼在一起    if (state->multipart->p == NULL)      state->multipart->p = p;    else      pbuf_cat(state->multipart->p, p);  }  while ((p = state->multipart->p) != NULL && (pos = pbuf_strstr(p, "/r/n")) != 0xffff)  {    // 收到完整的一行就立即处理    linelen = pos + 2;    err = httpd_post_process_multipart_line(state, linelen); // 每调用一次这个函数, 变量p就必须重新赋值一次    if (err != ERR_OK)      return err;  }    // (1) 读取表单控件内容或文件内容时, 一行的长度有可能很长  //     当pbuf堆积的数据量超过一定大小后就要立即处理, 避免占用太多内存  // (2) 如果已经接收完所有的数据(no_more_data=1), 那么就要立即处理所有未处理的数据  if (p != NULL && ((state->multipart->reading_content && p->tot_len >= 500) || no_more_data))  {    if (pbuf_get_at(p, p->tot_len - 1) == '/r' && !no_more_data)    {      // 数据块以/r结尾且还没接收完所有数据(no_more_data=0)的话, 暂不处理最后一个/r (因为不知道后面会不会是/n)      // 前面的数据倒是可以处理, 因为已经确定前面没有/r/n了, 只可能存在单独的/r或/n字符, 单独的/r或/n字符是没有用的      linelen = p->tot_len - 1;    }    else      linelen = p->tot_len; // 处理整个数据块    err = httpd_post_process_multipart_line(state, linelen);    p = state->multipart->p; // 一定要记得重新给p赋值    if (err != ERR_OK)      return err;  }  return ERR_OK;}/* 处理文件上传表单数据中的一行内容 */static err_t httpd_post_process_multipart_line(struct httpd_post_state *state, int linelen){  char buffer[100]; // 设置一个固定的缓冲区, 以免连续的/r/n换行导致内存一直不停地分配又释放, 浪费时间                    // 但是这个缓冲区又不能太大, 不能超过单片机的栈的大小  char str[70];  char *line, *p, *q, *r;  err_t err = ERR_OK;  int len, quoted, ret;  int line_complete = 0;  FRESULT fr;  UINT bw;  if (linelen + 1 <= sizeof(buffer))    line = buffer;  else  {    // 固定缓冲区放不下了才新分配内存    line = mem_malloc(linelen + 1);    if (line == NULL)    {      printf("%s: mem_malloc() failed/n", __func__);      return ERR_MEM;    }  }  pbuf_copy_partial(state->multipart->p, line, linelen, 0);  line[linelen] = '/0';  state->multipart->p = pbuf_free_header(state->multipart->p, linelen);  state->content_pos += linelen;  if (linelen >= 2 && strcmp(line + linelen - 2, "/r/n") == 0)    line_complete = 1;  if (state->multipart->boundary_len == 0)  {    if (line[0] == '-' && line[1] == '-')    {      // 读取边界字符串的内容      printf("%s: first boundary/n", __func__);      if (!line_complete)      {        err = ERR_ARG;        printf("%s: incomplete boundary string/n", __func__);        goto end;      }      else if (sizeof(state->multipart->boundary) < linelen + 1)      {        err = ERR_MEM;        printf("%s: boundary string is too long/n", __func__);        goto end;      }      strlcpy(state->multipart->boundary, line, sizeof(state->multipart->boundary));      state->multipart->boundary_len = linelen - 2;    }  }  else if (strncasecmp(line, state->multipart->boundary, state->multipart->boundary_len) == 0)  {    // 遇到边界字符串就说明上一个控件的内容已经读完了    printf("%s: boundary/n", __func__);    if (!line_complete)    {      err = ERR_ARG;      printf("%s: incomplete boundary string/n", __func__);      goto end;    }    state->multipart->reading_content = 0;    if (state->multipart->is_file != 0)    {      // 关闭文件      if (state->multipart->is_file == 2)      {        printf("%s: close file. size=%llu/n", __func__, state->multipart->content_len);        f_close(&state->multipart->fil);      }      state->multipart->is_file = 0;            snprintf(str, sizeof(str), "%llu", state->multipart->content_len);      err = httpd_post_store_multipart_string(state->multipart, 's', str, -1, NULL); // 记录文件大小      if (err != ERR_OK)        goto end;    }  }  else if (!state->multipart->reading_content)  {    if (!line_complete)    {      err = ERR_ARG;      printf("%s: incomplete field information/n", __func__);      goto end;    }    line[linelen - 2] = '/0';    if (strncasecmp(line, "Content-", 8) == 0)    {      quoted = 0;      for (p = line; p != NULL; p = r)      {        for (r = p; *r != '/0'; r++)        {          if (*r == '"')            quoted = !quoted;          else if (*r == ';' && !quoted)          {            *r++ = '/0';            break;          }        }        if (*r == '/0')          r = NULL;                q = strchr(p, '=');        if (q == NULL)          q = strchr(p, ':');        if (q != NULL)        {          *q = '/0';          q++;          trim(p);          trim(q);          if (*q == '"')          {            len = strlen(q);            if (q[len - 1] == '"')            {              q[len - 1] = '/0';              q++;            }          }          if (strcmp(p, "name") == 0)            err = httpd_post_store_multipart_string(state->multipart, 'n', q, -1, NULL);          else if (strcmp(p, "filename") == 0)          {            state->multipart->is_file = 1;            err = httpd_post_store_multipart_string(state->multipart, 'f', q, -1, &state->multipart->filename);          }          else if (strcasecmp(p, "Content-Type") == 0)            err = httpd_post_store_multipart_string(state->multipart, 't', q, -1, NULL);          else            err = ERR_OK;          if (err != ERR_OK)            goto end;        }      }    }    else if (line[0] == '/0')    {      // 遇到空行说明要开始读取控件的内容了      printf("%s: empty line/n", __func__);      state->multipart->reading_content = 1;      state->multipart->content_len = 0;      state->multipart->crlf = 0;      if (state->multipart->is_file == 0)      {        err = httpd_post_store_multipart_string(state->multipart, 'c', "", 0, NULL);        if (err != ERR_OK)          goto end;      }      else if (state->multipart->is_file == 1 && state->multipart->filename[0] != '/0')      {        // 打开文件        ret = httpd_post_generate_file_path(state->multipart->filename, str, sizeof(str));        if (ret != -1)        {          err = httpd_post_store_multipart_string(state->multipart, 'p', str, -1, NULL);          if (err != ERR_OK)            goto end;          printf("%s: open file %s/n", __func__, str);          fr = f_open(&state->multipart->fil, str, FA_CREATE_ALWAYS | FA_WRITE);          if (fr == FR_OK)            state->multipart->is_file = 2; // 文件打开成功          else            printf("%s: f_open() failed. fr=%d/n", __func__, fr); // 文件打开失败        }      }    }  }  else  {    if (state->multipart->crlf)    {      printf("%s: read 2 bytes of content/n", __func__);      state->multipart->content_len += 2;      if (state->multipart->is_file == 0)      {        err = httpd_post_store_multipart_string(state->multipart, 0, "/r/n", 2, NULL);        if (err != ERR_OK)          goto end;      }      else if (state->multipart->is_file == 2)      {        // 写文件        fr = f_write(&state->multipart->fil, "/r/n", 2, &bw);        if (bw != 2)        {          printf("%s: f_write() failed. fr=%d, len=2, bw=%u/n", __func__, fr, bw);          err = ERR_MEM;          goto end;        }      }    }    if (line_complete)    {      state->multipart->crlf = 1;      len = linelen - 2;    }    else    {      state->multipart->crlf = 0;      len = linelen;    }        if (len > 0)    {      printf("%s: read %d byte(s) of content/n", __func__, len);      state->multipart->content_len += len;      if (state->multipart->is_file == 0)      {        err = httpd_post_store_multipart_string(state->multipart, 0, line, len, NULL);        if (err != ERR_OK)          goto end;      }      else if (state->multipart->is_file == 2)      {        // 写文件        fr = f_write(&state->multipart->fil, line, len, &bw);        if (bw != len)        {          printf("%s: f_write() failed. fr=%d, len=%d, bw=%u/n", __func__, fr, len, bw);          err = ERR_MEM;          goto end;        }      }    }  }end:  if (line != buffer)    mem_free(line);  return err;}/* 为上传的文件选择一个保存路径 */static int httpd_post_generate_file_path(const char *srcname, char *buffer, int bufsize){  char datestr[9];  char ext[10] = {0};  char *p;  int i;  struct tm tm;  time_t t;  FRESULT fr;  time(&t);  localtime_r(&t, &tm);  strftime(datestr, sizeof(datestr), "%Y%m%d", &tm);  if (srcname != NULL)  {    p = strrchr(srcname, '.');    if (p != NULL)    {      for (i = 0; i < sizeof(ext) - 1 && p[i] != '/0'; i++)        ext[i] = tolower(p[i]);      ext[i] = '/0';    }  }  for (i = 0; i < 10000; i++)  {    snprintf(buffer, bufsize, "C://fileupload//%s_%04d%s", datestr, i, ext);    fr = f_stat(buffer, NULL);    if (fr == FR_NO_FILE)      break; // 文件名可用    else if (fr == FR_NOT_ENABLED || fr == FR_NO_PATH)    {      printf("%s: f_stat() failed. fr=%d/n", __func__, fr);      return -1;    }  }  if (i == 10000)  {    // 所有文件名都不可用    printf("%s: file %s already exists/n", __func__, buffer);    return -1;  }  return i;}/* 在字符串缓冲区中保存一个字符串 */// flag!=0: 新增一个字符串, 标志为flag// flag=0: 在之前的字符串后面追加内容// 此函数会将str里面的所有'/0'字符存储成'?'static err_t httpd_post_store_multipart_string(struct httpd_post_multipart_state *multipart, char flag, const char *str, int len, char **strout){  char *mem, *p;  int i, memsize, size;  // 计算字符串的长度  if (len < 0)    len = strlen(str);  if (strout != NULL)    *strout = NULL;  // 计算所需的存储空间  if (flag != 0)    size = len + 2; // flag+字符串+结束符  else  {    LWIP_ASSERT("appending to an existing string", multipart->strbuf_used != 0);    size = len; // 需要追加的字符串  }  if (multipart->strbuf_used + size > multipart->strbuf_capacity)  {    // 缓冲区不够的话, 就再开辟一块更大的空间    memsize = multipart->strbuf_capacity + size + 300;    mem = mem_malloc(memsize);    if (mem == NULL)    {      printf("%s: mem_malloc() failed/n", __func__);      return ERR_MEM;    }    if (multipart->strbuf != NULL)    {      // 将旧缓冲区存放的内容复制到新缓冲区, 并删除旧缓冲区      memcpy(mem, multipart->strbuf, multipart->strbuf_used);      mem_free(multipart->strbuf);    }    // 替换成新缓冲区    multipart->strbuf = mem;    multipart->strbuf_capacity = memsize;  }  // 将字符串放入缓冲区  if (flag != 0)  {    p = multipart->strbuf + multipart->strbuf_used;    *p++ = flag;  }  else    p = multipart->strbuf + multipart->strbuf_used - 1; // 取代之前的'/0'字符  if (strout != NULL)    *strout = p;  for (i = 0; i < len; i++)  {    if (str[i] != '/0')      p[i] = str[i];    else      p[i] = '?';  }  p[len] = '/0';  multipart->strbuf_used += size;  return ERR_OK;}/* 开始处理http post请求*/err_t httpd_post_begin(void *connection, const char *uri, const char *http_request, u16_t http_request_len, int content_len, char *response_uri, u16_t response_uri_len, u8_t *post_auto_wnd){  int multipart;  struct httpd_post_state *state;    printf("[httpd_post_begin] connection=0x%p, uri=%s/n", connection, uri);  printf("%.*s/n", http_request_len, http_request);  if (strcmp(uri, "/form_test.html") != 0 && strcmp(uri, "/upload_test.html") != 0)  {    //strlcpy(response_uri, "/bad_request.html", response_uri_len);    return ERR_ARG;  }    multipart = httpd_is_multipart(http_request, http_request_len);  state = httpd_post_create_state(connection, content_len, multipart);  if (state == NULL)  {    //strlcpy(response_uri, "/out_of_memory.html", response_uri_len);    return ERR_MEM;  }  return ERR_OK;}/* 接收表单数据 */err_t httpd_post_receive_data(void *connection, struct pbuf *p){  err_t err;  struct httpd_post_state *state;    printf("[httpd_post_receive_data] connection=0x%p, payload=0x%p, len=%d/n", connection, p->payload, p->tot_len);  state = httpd_post_find_state(connection);  if (state != NULL)  {    if (state->multipart == NULL)    {      pbuf_copy_partial(p, state->content + state->content_pos, p->tot_len, 0);      state->content_pos += p->tot_len;      pbuf_free(p);    }    else    {      err = httpd_post_process_multipart(state, p);      if (err != ERR_OK)        return err;    }  }  return ERR_OK;}/* 结束处理http post请求*/void httpd_post_finished(void *connection, char *response_uri, u16_t response_uri_len){  int i;  struct httpd_post_state *state;    printf("[httpd_post_finished] connection=0x%p/n", connection);  state = httpd_post_find_state(connection);  if (state != NULL)  {    if (state->multipart == NULL)      httpd_post_parse(state);    else    {      httpd_post_process_multipart(state, NULL);      httpd_post_parse_multipart(state);    }        printf("param_count=%d/n", state->param_count);    for (i = 0; i < state->param_count; i++)    {      if (state->files != NULL && state->files[i].name != NULL)        printf("[File] name=%s, filename=%s, path=%s, size=%llu, mimetype=%s/n", state->params[i], state->files[i].name, STRPTR(state->files[i].path), state->files[i].size, STRPTR(state->files[i].mimetype));      else        printf("[Param] name=%s, value=%s/n", state->params[i], STRPTR(state->values[i]));    }        httpd_post_delete_state(state);    //strlcpy(response_uri, "/success.html", response_uri_len);  }}void test_init(void){  FRESULT fr;    fr = f_stat("C://fileupload", NULL);  if (fr == FR_NO_FILE)  {    // 文件夹不存在, 创建文件夹    fr = f_mkdir("C://fileupload");    if (fr == FR_OK)      printf("%s: created fileupload folder/n", __func__);    else      printf("%s: failed to create fileupload folder. fr=%d/n", __func__, fr);  }  else if (fr == FR_NOT_ENABLED)  {    // 这种情况一般是因为fatfs没有初始化    printf("%s: disk has not been initialized/n", __func__);  }  else if (fr != FR_OK)    printf("%s: f_stat() failed. fr=%d/n", __func__, fr);}

分界字符串取的是表单内容的首行,而不是http header里面Content-Type中的boundary字符串,主要是因为这两者前导横杠的个数不同。http header Content-Type的boundary字符串有27个前导横杠,而表单内容里面的每个分界字符串都是29个前导横杠,多了两个横杠。在和表单里面其他的分界字符串作比较时,如果取的是表单内容首行的分界字符串,就不用特别处理横杠的个数,直接用strcmp函数比较就可以了,更加便捷。

程序运行结果:

文件上传完成后,浏览器显示404错误页面是因为httpd_post_finished函数没有对response_uri字符数组赋值。在串口里面可以看到各个表单控件的名称和内容,文件框控件还可以看到文件名、文件保存路径、文件大小和文件类型。串口显示中文乱码是Tera Term软件本身的bug,不是我们程序的问题。

 

上传的文件统一保存到C:/fileupload目录中,以当前日期加数字命名。
在fatfs里面,要启用C盘盘符访问,需要在ff13c/ffconf.h里面定义:
#define FF_STR_VOLUME_ID    1   // 允许使用盘符
#define FF_VOLUME_STRS        "C"
路径中既可以使用正斜杠/,也可以使用反斜杠/。在C语言字符串里面,反斜杠必须双写://。
ffconf.h里面的FF_FS_LOCK选项也很重要,这个值最好设置为大于0的数,表示启用文件锁并指定可以同时打开的文件个数,同一个文件不能同时多次打开。不然的话,如果多个函数同时打开同一个文件的话,整个文件系统很可能就会被破坏掉!这个选项在HTTP和FTP程序里面是很有用的。

lwip自带的http服务器默认最大只能上传95MB的文件,这是因为httpd.c里面将HTTP_HDR_CONTENT_LEN_DIGIT_MAX_LEN定义成了10。10减去2(/r/n的长度)后是8,Content-Length最大只允许为8位数,99999999字节差不多就是95.37MB左右。
如果HTTP_HDR_CONTENT_LEN_DIGIT_MAX_LEN=11的话,那么最大可以上传999999999字节,也就是953.67MB的文件。
如果HTTP_HDR_CONTENT_LEN_DIGIT_MAX_LEN=12,最大只能上传2147483647字节,即2047.999999MB的文件,这是因为content_len变量和atoi函数返回值的类型为int。如果想要上传更大的文件的话,httpd.c就得大改了,要把所有的跟content_len变量有关的变量类型由int全部改成int64_t。

下一篇:lwip-2.1.3自带的httpd网页服务器使用教程(五)使用COOKIE实现用户登录

也许您对下面的内容还感兴趣: