目录
参考示例
前言
1、此次实现的web服务器是使用了rtthread的webnet软件包来实现的。WebNet 软件包是 RT-Thread 自主研发的,基于 HTTP 协议的 Web 服务器实现,它不仅提供设备(HTTP Seerver)与 HTTP Client 通讯的基本功能,而且支持多种模块功能扩展,满足开发者对嵌入式设备服务器的功能需求。要将WebNet软件包用起来,基础的网络通信功能肯定是需要的,同时还需要能对 静态页面 进行存储、上传 等功能,所以WebNet的使用还需要文件系统相关的组件和网络通信相关的组件的支持,通过这些组件和软件包可以快速搭建好一个在STM32开发web服务器的环境。在搭建好环境后,先使用HBuilder(HTML5的Web开发IDE)制作好你的网页,然后将这些网页使用tftp工具上传到/webnet目录下,最后使用webnet软件包提供的SSI、CGI等功能模块实现web服务器与浏览器之间的交互
2、使用的硬件为 正点原子的阿波罗STM32F429开发板
3、在ENV中选中的组件或软件包,如果开启了包管理器自动更新或者手动使用 pkgs --update 命令,就能自动将选择的软件包更新到BSP中;然后再使用 scons --target=xxx 命令编译BSP时,选择的软件包相关源代码就会被自动添加进工程中并进行编译
一、需使用的组件与软件包及其ENV配置
1、文件系统相关组件与软件包
1.1、DFS 框架
DFS 框架 是 RT-Thread 提供的虚拟文件系统组件,全称为 Device File System,即设备虚拟文件系统。DFS 框架为应用程序提供统一的 POSIX 文件和目录操作接口,如 read、write、poll/select 等。DFS 框架支持多种类型的文件系统,如 FatFS、RomFS、DevFS 等,并提供普通文件、设备文件、网络文件描述符的管理。
1.2、fal 软件包
fal 全称为 Flash Abstraction Layer,即 Flash 抽象层,是对 Flash 及基于 Flash 的分区进行管理、操作的抽象层,对上层统一了 Flash 及 分区操作的 API。并提供了将分区创建成 MTD 设备的 API
1.3、SFUD 组件
SFUD 是一款开源的串行 SPI Flash 通用驱动库。现有市面的大部分串行 Flash,用户只需要提供 SPI 或 QSPI 的读写接口,SFUD 就可以识别并驱动。同时 RT-Thread 提供了 FAL 针对 SFUD 的驱动移植,可以使两个组件无缝连接
2、网络通信相关组件和软件包
2.1、SAL组件
SAL 套接字抽象层,通过它 RT-Thread 系统能够适配下层不同的网络协议栈,并提供给上层统一的网络编程接口,方便不同协议栈的接入。套接字抽象层为上层应用层提供接口有:accept、connect、send、recv 等。具有如下特点:
- 抽象、统一多种网络协议栈接口;
- 提供 Socket 层面的 TLS 加密传输特性;
- 支持标准 BSD Socket API;
- 统一的 FD 管理,便于使用 read/write poll/select 来操作网络功能;
2.2、netdev组件
netdev 网卡层,主要作用是解决多网卡情况设备网络连接和网络管理相关问题,通过 netdev 网卡层用户可以统一管理各个网卡信息和网络连接状态,并且可以使用统一的网卡调试命令接口
2.3、协议栈组件
协议栈层包括几种常用的 TCP/IP 协议栈,例如嵌入式开发中常用的轻型 TCP/IP 协议栈 lwIP 以及 RT-Thread 自主研发的 AT Socket 网络功能实现等。这些协议栈或网络功能实现直接和硬件接触,完成数据从网络层到传输层的转化。这里使用的是lwip
2.4、netutils工具集软件包
netutils软件包中汇集了 RT-Thread 可用的全部网络小工具集合,这里主要使用TFTP小工具,TFTP (Trivial File Transfer Protocol, 简单文件传输协议)是 TCP/IP 协议族中的一个用来在客户机与服务器之间进行简单文件传输的协议,提供不复杂、开销不大的文件传输服务,端口号为 69 ,比传统的 FTP 协议要轻量级很多,适用于小型的嵌入式产品上。在板卡上开启TFTP Server后,就可以在PC上使用TFTP Client软件将HTML网页文件上传到板卡的SPI FLASH中。
2.5、webnet软件包
官网有很详细的介绍,WebNet 软件包功能特点:
- 支持 HTTP 1.0/1.1
- 支持 CGI 功能
- 支持 ASP 变量替换功能
- 支持 AUTH 基本认证功能
- 支持 INDEX 目录文件显示功能
- 支持 ALIAS 别名访问功能
- 支持 SSI 文件嵌入功能
- 支持文件上传功能
- 支持预压缩功能
- 支持缓存功能
- 支持断点续传功能
浏览器访问设备 IP 地址不显示页面信息
- 原因:设置的根目录地址错误。
- 解决方法:确定设置的根目录地址(/webnet)和设备文件系统上创建的目录地址一致,确定根目录下有页面文件。也就是说必须先在块设备上初始化文件系统,且在文件系统中有 /webnet 这个文件夹,同时页面文件也已经上传到了跟目录下。
二、添加驱动和初始化代码
1、SPI FLASH驱动
1.1、在spi_flash_init.c中添加如下内容,注册softspi1总线,注册softspi10设备并挂载到softspi1总线上;使能SFUD驱动W25Q64块设备
-
#include <rtthread.h>
-
#include "spi_flash.h"
-
#include "spi_flash_sfud.h"
-
#include "drv_soft_spi.h"
-
#if defined(BSP_USING_SPI_FLASH)
-
static int rt_hw_spi_flash_init(void)
-
{
-
__HAL_RCC_GPIOG_CLK_ENABLE();
-
rt_soft_spi_device_attach("softspi1", "softspi10", GPIOG, GPIO_PIN_10);
-
if (RT_NULL == rt_sfud_flash_probe("W25Q64", "softspi10"))
-
{
-
return -RT_ERROR;
-
}
-
return RT_EOK;
-
}
-
INIT_COMPONENT_EXPORT(rt_hw_spi_flash_init);
-
#endif
1.2、在ENV中开启模拟SPI,开启BSP_USING_SOFT_SPI和BSP_USING_SOFT_SPI1宏定义,这样在scons构建工程时,drv_soft_spi.c 就能自动添加进工程中
2、网卡驱动
2.1、网卡驱动部分rtthread已经在drv_eth.c/h中写好了,唯一要改的就是在phy_reset.c中添加PHY网卡的复位,添加如下内容
-
#define ETH_RESET_IO GET_PIN(H, 3) //PHY RESET PIN
-
/* phy reset */
-
void phy_reset(void)
-
{
-
rt_pin_write(ETH_RESET_IO, PIN_HIGH);
-
rt_thread_mdelay(100);
-
rt_pin_write(ETH_RESET_IO, PIN_LOW);
-
rt_thread_mdelay(100);
-
}
-
int phy_init(void)
-
{
-
rt_pin_mode(ETH_RESET_IO, PIN_MODE_OUTPUT);
-
rt_pin_write(ETH_RESET_IO, PIN_LOW);
-
return RT_EOK;
-
}
-
INIT_BOARD_EXPORT(phy_init);
2.2、在ENV中选中网卡驱动,开启 BSP_USING_ETH 和 PHY_USING_LAN8720A宏定义,这样在scons构建工程时,drv_eth.c 和 phy_reset.c 就能自动添加进工程中
3、FAL配置
3.1、在 fal_cfg.h中定义 flash 设备、flash 设备表、flash 分区表。flash设备表中,nor_flash0是使用了SFUD接口实现片外SPI FLASH操作的fal_flash设备,具体实现在FAL针对 SFUD 的移植文件fal_flash_sfud_port.c中。stm32_onchip_flash_xx 是直接操作单片机片内FLASH的fal_flash设备,具体实现在 drv_flash_f4.c中
-
/* flash device table */
-
#define FAL_FLASH_DEV_TABLE \
-
{ \
-
&stm32_onchip_flash_16k, \
-
&stm32_onchip_flash_64k, \
-
&stm32_onchip_flash_128k, \
-
&nor_flash0, \
-
}
-
/* ====================== Partition Configuration ========================== */
-
#ifdef FAL_PART_HAS_TABLE_CFG
-
/* partition table */
-
#define FAL_PART_TABLE \
-
{ \
-
{FAL_PART_MAGIC_WROD, "bl", "onchip_flash_16k", 0 , FLASH_SIZE_GRANULARITY_16K , 0}, \
-
{FAL_PART_MAGIC_WROD, "easyflash", "onchip_flash_64k", 0 , FLASH_SIZE_GRANULARITY_64K , 0}, \
-
{FAL_PART_MAGIC_WROD, "app", "onchip_flash_128k", 0 , FLASH_SIZE_GRANULARITY_128K, 0}, \
-
{FAL_PART_MAGIC_WROD, "fs", FAL_USING_NOR_FLASH_DEV_NAME, 0 , 4 * 1024 * 1024, 0}, \
-
{FAL_PART_MAGIC_WROD, "tgfx", FAL_USING_NOR_FLASH_DEV_NAME, 4 * 1024 * 1024 , 4 * 1024 * 1024, 0}, \
-
}
3.2、在 spi_flash_init.c 中调用fal_init()初始化该组件
-
#if defined(PKG_USING_FAL)
-
int fs_init(void)
-
{
-
/* partition initialized */
-
fal_init();
-
return 0;
-
}
-
INIT_COMPONENT_EXPORT(fs_init);
-
#endif
4、格式化块设备
4.1、将FAL的"fs"分区挂载到根目录下,用于存储静态网页,第一次挂载的时候可能会失败,因为该分区还没有文件系统,需要先在 "fs"分区创建elmFAT文件系统。此时可以在系统起来后,直接在shell中输入 mkfs -t elm fs 命令对“fs”分区进行格式化
-
#if defined(RT_USING_DFS_ELMFAT)
-
#define FS_PARTITION_NAME "fs"
-
int elm_fatfs_init(void)
-
{
-
/* partition initialized */
-
// elm_init();
-
//dfs_mkfs("elm", "fs"); /* 在fs块设备上创建elm文件系统*/
-
/* Create a block device on the "fs" partition of spi flash */
-
struct rt_device *flash_dev = fal_blk_device_create(FS_PARTITION_NAME);
-
if (flash_dev == NULL){
-
rt_kprintf("Can't create a block device on '%s' partition.\n", FS_PARTITION_NAME);
-
} else {
-
rt_kprintf("Create a block device on the %s partition of flash successful.\n", FS_PARTITION_NAME);
-
}
-
/* mount the file system from "fs" partition of spi flash. */
-
if (dfs_mount(FS_PARTITION_NAME, "/", "elm", 0, 0) == 0)
-
{
-
LOG_I("Filesystem initialized!");
-
}
-
else
-
{
-
LOG_E("Failed to initialize filesystem!");
-
LOG_D("You should create a filesystem on the block device first!");
-
}
-
return 0;
-
}
-
INIT_COMPONENT_EXPORT(elm_fatfs_init);
三、web服务器开发基础
1、HTTP简介
HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从服务器传输超文本到本地浏览器的传送协议。
1.1、工作原理
HTTP是一个基于TCP/IP通信协议来传递数据(HTML 文件, 图片文件, 查询结果等)HTTP协议工作于客户端-服务端架构上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。默认端口为80。HTTP使用统一资源标识符(Uniform Resource Identifiers, URI)来传输数据和建立连接。
HTTP三点注意事项:
- HTTP是无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
- HTTP是媒体独立的:这意味着,只要客户端和服务器知道如何处理的数据内容,任何类型的数据都可以通过HTTP发送。客户端以及服务器指定使用适合的MIME-type内容类型。
- HTTP是无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。
1.2、消息结构
客户端请求消息格式:由四个部分组成,分别是:请求行(request line)、请求头部(header)、空行、请求数据
示例:
-
GET /hello.txt HTTP/1.1
-
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
-
Host: www.example.com
-
Accept-Language: en, mi
-
xxxxxxxxxxxxxx
服务器响应消息格式:也由四个部分组成分别是:状态行、消息报头、空行、响应正文
2、HTML简介
超文本标记语言(HyperText Markup Language),是一种用于创建网页的标准标记语言。HTML 运行在浏览器上,由浏览器来解析。
2.1、HTML 网页结构
2.2、HTML 标签
HTML 标记标签通常被称为 HTML 标签 (HTML tag)。
- HTML 标签是由尖括号包围的关键词,比如 <html>
- HTML 标签通常是成对出现的,比如 <b> 和 </b>
- 标签对中的第一个标签是开始标签,第二个标签是结束标签
- 开始和结束标签也被称为开放标签和闭合标签
2.3、HTML 属性
- HTML 标签可以设置属性
- 属性可以在元素中添加附加信息
- 属性一般描述于开始标签
- 属性总是以名称/值对的形式出现,比如:name="value"
- 一个标签中可以同时设置多个属性,属性之间需要用空格隔开
<a href="http://www.runoob.com">这是一个链接</a> <h1>这是<font color="red" size="10">一个</font>网页</h1>
3、CSS简介
CSS 指层叠样式表 (Cascading Style Sheets),样式表定义如何显示 HTML 元素,就像 HTML 中的字体标签和颜色属性所起的作用那样。样式通常保存在外部的 .css 文件中。我们只需要编辑一个简单的 CSS 文档就可以改变所有页面的布局和外观。根据CSS样式在HTML中被引用的方式可分为以下三种类型:
内部样式表:包含含HTML文件的head标签中,在body中的所以标签的可以使用
<style type="text/css"> p{color:red;font-size:40px;}</style>
外部样式表:外部独立的.css文件中,通过在HTML文件中引用这个.css文件使用其样式
<link rel="stylesheet" type="text/css" href="css文件路径" />
内联样式:直接写在标签的开始标签中,只对该标签有效
<p style="color:red;font-size:40px;"> </p>
4、CGI技术简介
公共网关接口(Common Gateway Interface),是外部应用程序与web服务器之间的接口标准。CGI规范允许web服务器执行外部程序,并将它们的输出发送给web浏览器,CGI在物理上是一段程序,运行在服务器上,提供同客服端HTML页面的接口。绝大多数的CGI程序被用来解释处理来自表单的输入信息,并在服务器产生相应的处理,或经相应的信息反馈给浏览器,CGI程序使网页具有交互功能,比如通过web来处理浏览器提交的表单数据。如下图,一个表单在浏览被提交,发送给web被处理的过程:
1、登录网页
2、提交表单(Checkbox, Radio button, selection list etc)
3、CGI程序处理
4、返回处理结果
5、SSI简介
服务器侧包含(server side include),是一种类似于ASP的基于服务器的网页制作技术。是指将HTML内容发送到浏览器之前,先使用SSI指令将文本、图形、或变量数据信息包含到网页中,再发给浏览器解析。对于在多个文件中重复出现的文本或图形,使用SSI包含技术是一种简便的方法,只需将内容存入一个包含文件中即可,而不必将内容写入到所有的文件,通过一个非常简单的语句即可调用包含文件,此语句指示web服务器将内容插入到网页的位置。
实现SSI功能的原理也很简单,就是在发送网页文件的时候遍历整个文件,查找文件中的SSI指令,如果出现有<!--#include file="xxx" --> 指令,则将xxx文件的内容嵌入到网页中;如果出现有<!--#include virtual="xxx" --> 指令,则匹配注册好的xxx名称,如果有注册则执行其回调函数。想想如果发送每个网页的时候都这么去查找,效率肯定很低,而且很多网页并没有文件或变量需要嵌入,所以使用SSI指令的网页有特殊的扩展名,默认有 .stm、.shtm、.shtml,查找前会先匹配文件的扩展名,如果不是SSI默认的扩展名文件则不回配置SSI指令。如下图,浏览器请求一个.shtml扩展名的文件:
1、用户请求SSI页面(后缀名是 .shtm .shtml .ssi .xml 的特殊的html网页)
2、服务器根据页面SSI标签(<!--#include file="xxx" -->或<!--#include virtual="xxx" -->)动态插入数据。
3、服务器返回动态生成的页面
四、web服务器应用程序设计
1、网页制作
一个网页主要由三部分构成:结构、表现、行为。结构:用于描述页面的结构;表现:用于控制页面中元素的样式;行为:用于响应用户的操作。
此次设计的有如下几个网页,网页使用HBuilder编写
1.1、效果图
1.2、CGI类型与SSI标签
1、在需要提交表单的页面中,将表单的action属性填写成web服务器可识别的URL,表明向何处提交你的表单,这样wen服务器在收到表单数据后,就会去执行跟action匹配的自定义注册好的CGI执行函数。在rtthread的webnet中默认的CGI类型是/cgi-bin开头的,需要注意!!
-
<form action="cgi-bin/ethip" method="post" target="nm_iframe">
-
...
-
</form>
2、在需要动态嵌入变量或文件的.shtml页面中,需要通过注释的方式定义好SSI标签,这样web服务器在发送这个页面的时候就会去匹配页面中的SSI标签,然后去执行跟SSI标签名称匹配的自定义注册好的SSI回调函数。如下,实时获取传感器数据的HTML
-
<table>
-
<tr>
-
<th height="50" colspan="2"><div align="center"><span class="font1">传感器实时数据</span></div><hr /></th>
-
<tr>
-
<tr>
-
<td class="td_left">温度:</td>
-
<td class="td_right"><span class="font2"><!--#include virtual="sensor_temp" --> ℃</span></td>
-
</tr>
-
<tr>
-
<td class="td_left">湿度:</td>
-
<td class="td_right"><span class="font2"><!--#include virtual="sensor_humit" --> %RH</span></td>
-
</tr>
-
<tr>
-
<td class="td_left">太阳能电压:</td>
-
<td class="td_right"><span class="font2"><!--#include virtual="sensor_adc" --> mV</span></td>
-
</tr>
-
</table>
2、功能实现
2.1、自定义注册CGI执行函数
需要在启动web服务器之前注册好你的CGI执行函数,我这里注册的函数如下
以通过网页远程控制板卡为例,浏览器给web服务器发送的数据如下:
程序解析过程如下:
-
static void cgi_remotectl_handler(struct webnet_session* session)
-
{
-
/* defined the LED0 pin: PH2 */
-
#define LED0_PIN GET_PIN(H, 2)
-
/* defined the BEEP pin: PE3 */
-
#define BEEP_PIN GET_PIN(E, 3)
-
struct webnet_request* request;
-
const char* mimetype;
-
const char *led1,*beep,*relay;
-
char led1_value=0,beep_value=0,relay_value=0;
-
RT_ASSERT(session != RT_NULL);
-
/* get mimetype */
-
mimetype = mime_get_type(".html");
-
request = session->request;
-
/* set http header */
-
session->request->result_code = 200;
-
webnet_session_set_header(session, mimetype, 200, "Ok", -1);
-
/* 解析 */
-
led1 = webnet_request_get_query(request, "led1");
-
beep = webnet_request_get_query(request, "beep");
-
relay = webnet_request_get_query(request, "relay");
-
led1_value = atoi(led1);
-
beep_value = atoi(beep);
-
relay_value = atoi(relay);
-
if (led1_value) rt_pin_write(LED0_PIN, PIN_LOW);
-
else rt_pin_write(LED0_PIN, PIN_HIGH);
-
if (beep_value) rt_pin_write(BEEP_PIN, PIN_HIGH);
-
else rt_pin_write(BEEP_PIN, PIN_LOW);
-
}
2.2、自定义注册SSI执行函数
rtthread实现的webnet中只实现了在网页中嵌入文件的功能,即只识别 <!--#include file="xxx" --> 标签,需要修改下 wn_module_ssi.c 实现 <!--#include virtual="xxx" --> 标签的识别与处理。同理需要在启动web服务器之前注册好你的SSI执行函数,如上图所示。我这里以获取以太网IP地址为例,在浏览器请求 ethip.shtml 的时候,web服务器会执行注册好的回调函数
在回调函数中直接将网卡的IP地址发送给浏览器
-
static void ssi_eth_ipaddr_value_handler(struct webnet_session* session)
-
{
-
struct netdev *netdev = netdev_get_by_name("e0");
-
webnet_session_printf(session, "%s", inet_ntoa(netdev->ip_addr));
-
}
3、页面文件上传
开启RT-Thread 的设备虚拟文件系统后,需要将静态页面上传到文件系统中服务器的根目录下(这里根目录为 /webnet),需要依次执行下面操作:
1、使用 mkdir webnet 命令创建 WebNet 软件包根目录 /webnet,并使用 cd webnet 命令进入该目录;
2、使用 mkdir admin 、 mkdir upload 和 mkdir download命令创建 /webnet/admin 、/webnet/upload 和/webnet/download,用于 AUTH 功能、 upload 功能和download功能的测试
3、将设计的所有网页依次上传到设备 /webnet 目录中。(可以使用 TFTP 工具上传文件,具体操作方式参考 TFTP 使用说明)
4、启动webnet
设备启动,网络连接成功,创建目录和上传文件成功之后,就可以启动例程,测试 WebNet 软件功能来了,过程如下:在 Shell 命令行输入 webnet_test 命令即可启动 WebNet 服务器
-
msh /webnet>webnet_test
-
[D/wn.log] server initialize success.
-
[I/wn] RT-Thread webnet package (V2.0.0) initialize success.
5、解决的问题
1、在浏览器获取不到网页的问题
获取的网页需要写成绝对地址,比如获取/webnet/frame_table.html页面时,需要写成 /frame_table.html,不能只写成 frame_table.html
2、在提交完表单后会切换当前的页面,有时候其实是不需要切换的,还停留在当前页面。这时候可以巧妙运用 iframe,在页面中放置一个面积为0的iframe,使其跳转在iframe中完成,即实际跳转了,只是不可见
-
<iframe id="id_iframe" name="nm_iframe" style="display:none;"></iframe>
-
<form action="cgi-bin/ethip" method="post" target="nm_iframe">
3、动态实时刷新页面
<meta charset="utf-8" http-equiv="refresh" content="1">
4、登录的时候,先判断用户名和密码,不对的话打印提示信息
-
<script type="text/javascript">
-
function check(form)
-
{
-
var a = form.username.value;
-
var b = form.password.value;
-
if (a != "admin")
-
{
-
alert("User name error,Please input correct user name!");
-
}
-
else
-
{
-
if (b != "admin")
-
{
-
alert("Pass word error,Please input correct pass word!");
-
}
-
else
-
{
-
form.submit();
-
}
-
}
-
}
-
</script>
-
<input type="button" name="login" id="" style="position:absolute; left:48%;" value="登录" onclick="check(form)"/>