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

上一篇: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.1
Accept: 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.html
Accept-Language: zh-cn
User-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-urlencoded
Accept-Encoding: gzip, deflate
Host: stm32f103ze
Content-Length: 810
Connection: Keep-Alive
Cache-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.1
Accept: */*
Referer: http://stm32f103ze/form_test.html
Accept-Language: zh-cn
User-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=---------------------------7e729f1bf0a7a
Accept-Encoding: gzip, deflate
Host: stm32f103ze
Content-Length: 39415
Connection: Keep-Alive
Cache-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实现用户登录

  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
lwip(轻量级IP协议栈)是一个开源的TCP/IP协议栈,旨在用于嵌入式系统和实时应用。移植lwip 2.1.3到特定的嵌入式系统需要以下步骤: 1. 下载和解压缩lwip软件包:首先,从lwip官方网站上下载最新版本的lwip 2.1.3软件包。然后,将软件包解压缩到本地目录。 2. 配置lwip:进入lwip软件包所在的目录,找到lwipopts.h头文件。通过修改该头文件中的宏定义,根据嵌入式系统的需求配置lwip。可能需要设置的选项包括:IP地址、子网掩码、网关地址、最大数据包长度等。 3. 移植硬件驱动:lwip需要硬件驱动程序来与底层网络接口进行通信。嵌入式系统通常有自己的网络接口硬件,所以需要移植特定的硬件驱动程序。根据硬件接口和规范,实现网络驱动程序和相关函数。 4. 移植操作系统适配层(optional):如果嵌入式系统使用操作系统,如RTOS(实时操作系统),则需要移植操作系统适配层以支持lwip的多线程和并发操作。根据具体的操作系统规范,实现适配层函数和功能。 5. 编译和链接:使用适当的交叉编译工具链,将lwip源代码以及硬件驱动程序和适配层代码编译成目标平台的可执行文件。然后,将生成的目标文件链接到嵌入式系统的应用程序中。 6. 调试和测试:在嵌入式系统上运行编译和链接后的 lwip软件,并进行相应的调试和测试。确保lwip在特定的嵌入式环境下能够正常工作,并实现所期望的网络功能。 总之,移植lwip 2.1.3到嵌入式系统需要进行配置、移植硬件驱动程序、可能的操作系统适配层移植、编译和链接等步骤。通过这些步骤,lwip可以在嵌入式系统上实现TCP/IP网络功能,并提供轻量级的网络通信能力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

巨大八爪鱼

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

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

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

打赏作者

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

抵扣说明:

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

余额充值