这个页面的 最新开发版本 可能比这个发布的 0.4.0 版本新。

WebNet

介绍

WebNet 是基于 HTTP 1.0/1.1 协议的 Web 服务器实现,它不仅提供设备与 HTTP Client 通讯的基本功能, 而且支持多种模块功能扩展,且资源占用少、可裁剪性强。

对于 WebNet 的使用,可以选择是否使用文件系统。如果不使用文件系统,需将文件嵌入到代码中使用,请参考 文件嵌入到代码中。

WebNet 功能特点如下:

工作原理

WebNet 主要用于在嵌入式设备上实现 HTTP 服务器,主要工作原理基于 HTTP 协议实现。

HTTP 协议定义了客户端如何从服务器请求数据,以及服务器如何把数据传送给客户端的方式。 HTTP 协议采用了 请求/响应模型 。 客户端向服务器发送一个请求报文,请求报文包含请求的方法、 URL、协议版本、请求头部和请求数据。服务器通过解析请求头部信息,执行相应的功能模块,并且给客户端发送响应数据, 响应数据的内容包括协议的版本、成功或者错误代码、服务器信息、响应头部和响应数据。

在 HTTP 协议的实际使用过程中,一般遵循以下流程:

  1. 客户端连接到服务器

    通常是通过 TCP 三次握手建立 TCP 连接,WebNet 中默认的连接端口号为 80。

  2. 客户端发送 HTTP 请求

    通过 TCP 套接字,客户端向 WebNet 发送一个请求报文,一个请求报文由请求行、请求头部、空行和请求数据四部分组成

  3. 服务器接收请求并解析数据信息

    服务器接收到客户端的请求后,开启解析请求信息,定位服务器上请求的资源文件,然后向客户端发送响应数据,由客户端读取。一个响应数据由状态行、响应头部、空行和响应数据四部分组成。

  4. 客户端和服务器断开连接

    若客户端和服务器之间连接模式为普通模式,则服务器主动关闭 TCP 连接,客户端被动关闭连接,释放 TCP 连接。若连接模式为 keepalive 模式,则该连接保持一段时间,在该时间内可以继续接收数据。

工作流程

下面以浏览器访问 WebNet 服务器根目录下主页面为例,介绍 WebNet 基本工作流程:

初始化 WebNet:

int webnet_init(int keep_alive);

WebNet 使用之前需要先初始化,初始化函数中创建了 WebNet 线程。该线程用于初始化开启的功能模块, 完成创建服务器监听套接字,并使用监听套接字等待客户端连接和数据交互。 如下图为线程函数主要操作:

/* WebNet 服务器线程处理 */
static void webnet_thread(void *parameter)
{
    ....
    /* 创建监听 */
    listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    ...
    /* 初始化开启的功能模块 */
    webnet_module_handle_event(NULL, WEBNET_EVENT_INIT);

    /* 等待连接请求,等待接收数据 */
    for (;;)
    {
        ...
        sock_fd = select(maxfdp1, &tempfds, &tempwrtfds, 0, 0);
        if (sock_fd == 0)
            continue;

        if (FD_ISSET(listenfd, &tempfds))
        {
            /* 处理新的连接请求 */
        }

        /* 处理接收或发送的数据 */
        webnet_sessions_handle_fds(&tempfds, &writeset);
    }
    ...
}

接收连接请求,创建连接会话:

WebNet 初始化线程创建成功之后,当有新的连接请求产生时,会创建一个连接会话结构体,结构体定义如下:

struct webnet_session
{
    struct webnet_session *next;                      // 会话结构体链表

    int socket;
    struct sockaddr_in cliaddr;
    struct webnet_request* request;                           // 会话的请求相关信息

    uint16_t buffer_length;
    uint16_t buffer_offset;
    uint8_t  buffer[WEBNET_SESSION_BUFSZ];     // 会话缓冲区数据,用于接收请求数据
    uint32_t  session_phase;                                          // 会话状态
    uint32_t  session_event_mask;
    const struct webnet_session_ops* session_ops; // 会话事件执行函数
    uint32_t user_data;
};

webnet_session 结构体用于存放当前建立的连接会话的部分信息,可用与当前会话连接的整个流程。 在进行 HTTP 数据交互之前,需要先创建并初始化该结构体,新会话的创建已经在 WebNet 线程中完成,如下所示:

struct webnet_session* accept_session;
accept_session = webnet_session_create(listenfd);
if (accept_session == NULL)
{
    /* 创建失败,关闭连接 */
}

接收 HTTP 请求数据,解析请求信息:

创建会话结构体成功之后,当连接会话接收到 HTTP 请求后,会对接收的 HTTP 请求进行处理,顺序地解析请求的类型、头部信息及附加参数。大致解析请求信息的流程如下所示:

/* 该函数用于解析当前会话连接的请求模式、头部信息和参数 */
static void _webnet_session_handle_read(struct webnet_session* session)
{
    /* 读取当前会话 HTTP 连接会话数据 */
    ....

    if (session->buffer_offset)
    {
        /* 解析 HTTP 请求模式(GET、POST 等)*/
        if (session->session_phase == WEB_PHASE_METHOD)
        {
            webnet_request_parse_method(...);
        }

        /* 解析 HTTP 请求头部信息 */
        if (session->session_phase == WEB_PHASE_HEADER)
        {
            webnet_request_parse_header(...);
        }

        /* 解析 HTTP URL 中附带请求参数 */
        if (session->session_phase == WEB_PHASE_QUERY)
        {
            webnet_request_parse_post(...);
        }
    }
}

判断请求的功能模块,执行对应的功能:

通过对请求模式和头部信息的解析,得到当前连接会话请求的基本信息,然后继续判断使用的功能模块的类型, 并且执行对应的模块,判断的大致流程如下:

/* 该函数为 WebNet 中用于判断和执行当前会话请求的功能,如日志功能、CGI 事件处理功能等 */
static int _webnet_module_system_uri_physical(struct webnet_session* session, int event)
{
    /* 如果开启 LOG 功能模块,使用 LOG 日志输出功能 */
#ifdef WEBNET_USING_LOG
    webnet_module_log(session, event);
#endif

    /* 如果开启 ALIAS 功能模块,判断当前请求是否是别名请求 */
#ifdef WEBNET_USING_ALIAS
    result = webnet_module_alias(session, event);
    if (result == WEBNET_MODULE_FINISHED) return result;
#endif

    /* 如果开启 AUTH 功能模块,判断当前请求是否需要执行基本认证操作 */
#ifdef WEBNET_USING_AUTH
    result = webnet_module_auth(session, event);
    if (result == WEBNET_MODULE_FINISHED) return result;
#endif

    /* 如果开启 CGI 功能模块,判断当前请求是否需要执行 CGI 操作 */
#ifdef WEBNET_USING_CGI
    result = webnet_module_cgi(session, event);
    if (result == WEBNET_MODULE_FINISHED) return result;
#endif
    ...
}

返回 HTTP 响应数据:

判断功能模块类型成功,并且正确执对应功能之后,WebNet 服务器会对当前会话连接的请求给予响应,如 CGI 功能执行之后, 在 CGI 执行函数中可以使用 webnet_session_printfwebnet_session_write 函数发送响应数据到客户端。

/* 该函数 CGI 功能执行函数,当浏览器访问当前 CGI 事件时,执行该函数返回响应头部信息和数据 */
static void cgi_hello_handler(struct webnet_session* session, void* handler_priv)
{
    /* 拼接需要发送的页面数据 */
    ....

    /* 发送响应头部信息 */
    webnet_session_set_header(session, mime_get_type(".html"), 200, "Ok", strlen(status));

    /* 发送响应数据 */
    webnet_session_write(session, (const uint8_t*)status, strlen(status));
}

关闭连接会话:

当前会话连接请求解析成功、功能模块执行完成、响应数据发送完成之后,会关闭当前连接会话,释放会话结构体, 完成整个 HTTP 数据数据交互过程,实现在浏览器上访问设备端提供的网页文件,或者完成上传、下载服务器上文件的操作。

使用方式

对于 WebNet 服务器的多种功能模块,有些功能模块在使用之前需要先设置对应配置参数,部分功能模块需要配合页面代码实现 功能, 接下来将介绍 WebNet 服务器不同功能模块的使用方式。

LOG 日志显示功能

开启之后可以显示会话请求的基本信息,比如连接设置的 IP 地址和端口号,HTTP 请求类型、访问地址等信息,建议调试代码时 开启。

AUTH 基本认证功能

Basic Authentication 基础认证功能可以按目录划分访问权限。需要在 WebNet 服务器初始化之前调用 webnet_auth_set 函数设置目录的用户名和密码(设置的格式为 用户名:密码),浏览器中访问该目录时需要输入正确的用户名和密码才能访问目录。相关函数定义如下:

/* 设置目录基本认证信息 */
void webnet_auth_set(const char* path, const char* username_password);

AUTH 基本认证功能示例代码如下:

void webnet_test(void)
{
    /* 设置 /admin 目录用户名为 admin 密码为 admin */
    webnet_auth_set("/admin", "admin:admin");
    webnet_init(0);
}

/admin 目录设置基本认证功能之后,在浏览器中访问 /admin 目录时,需要输入设置的用户名和密码才能访问。

CGI 功能

CGI 功能可以自定义事件的执行函数,当浏览器发送对应该事件的请求时,WebNet 服务器可以执行相应的操作。 需要在 WebNet 服务器初始化之前调用 webnet_cgi_register 函数注册 CGI 执行函数,相关函数定义如下:

/* 设置 CGI 事件根目录 */
void webnet_cgi_set_root(const char* root);
/* 设置 CGI 事件执行函数 */
void webnet_cgi_register(const char* name, void (*handler)(struct webnet_session* session, void* handler_priv), void* priv);

/* 发送头部信息到 WebNet 连接的客户端,用于 CGI 事件执行函数中 */
void webnet_session_set_header(struct webnet_session *session, const char* mimetype, int code, const char* status, int length);
/* 发送固定格式数据到 WebNet 连接的客户端,用于 CGI 事件执行函数中 */
void webnet_session_printf(struct webnet_session *session, const char* fmt, ...);
/* 发送数据到 WebNet 连接的客户端,用于 CGI 事件执行函数中 */
int  webnet_session_write(struct webnet_session *session, const uint8_t* data, size_t size);

CGI 功能使用的示例代码如下:

static void cgi_hello_handler(struct webnet_session* session, void* handler_priv)
{
    const char* hello = "Hello World\n";
    webnet_session_set_header(session, mime_get_type(".html"), 200, "Ok", strlen(status));
    webnet_session_write(session, hello, strlen(hello));
    webnet_session_printf(session, "%s", hello);
}
void webnet_test(void)
{
    /* 设置 CGI 事件执行函数*/
    webnet_cgi_register("hello", cgi_hello_handler, NULL);
    webnet_init(0);
}

对应的页面代码如下,浏览器上点击 hello world 按键将发送对应 CGI 请求给服务器。

<html>
<body>
    <hr>
    <h3> CGI Test</h3>
    WebNet CGI 功能可以让用户执行指定的函数,CGI测试:
    <br/><br/>
    <a href="/cgi-bin/hello">> hello world</a>
    <br/>
</body>
</html>

ASP 变量替换功能

ASP 变量替换功能,可以匹配网页代码中 <% %> 标记中包含的变量,替换成代码中注册的执行函数。 所以在 WebNet 初始化之前需要调用 webnet_asp_add_var 设置 ASP 变量替换函数,相关函数定义如下:

/* 设置 ASP 变量执行函数 */
void webnet_asp_add_var(const char* name, void (*handler)(struct webnet_session* session, void* handler_priv), void* priv);

ASP 功能示例代码如下:

static void asp_var_version(struct webnet_session* session, void* handler_priv)
{
    webnet_session_printf(session, "WM IoT SDK: %d.%d.%d\n", WM_VERSION, WM_SUBVERSION, WM_REVISION);
}

void webnet_test(void)
{
    /* 设置 ASP 变量执行函数*/
    webnet_asp_add_var("version", asp_var_version, NULL);
    webnet_init(0);
}

对应的页面代码如下(文件名为 version.asp),访问该页面代码将 ASP 替换显示 WM IoT SDK 最新版本信息:

<html>
<head>
    <title> ASP Test </title>
</head>
<body>
    <% version %>               /* ASP 变量替换成 WM IoT SDK 版本号显示 */
</body>
</html>

SSI 文件嵌套功能

WebNet 中支持嵌入文本文件到网页中,页面中需要有 <!–#include virtual=”/xxx”–> 或者 <!–#include file=”/xxx”–> 标记存在将替换成对应的文件内容,SSI 功能页面一般以 .shtml.stm 结尾,如下为示例页面代码(文件名为 index.shtml):

<html>
<head>
    <title> SSI Test </title>
</head>
<body>
    <h1> SSI Test</h1>
    <font size=\"+2\">The index.html page embedded in the following page</font>
    <hr>
    <!--#include virtual="/index.html" -->   /* 该页面这嵌入index.html 文件内容 */
</body>
</html>

INDEX 目录文件显示功能

WebNet 服务器初始化成功之后,直接在浏览器中输入设置 IP 地址和要访问的目录,可以列出当前目录下所有的文件。不使用文件系统时需访问 /admin 显示所有嵌入到代码的文件。

ALIAS 别名访问功能

ALIAS 别名访问功能可以给文件夹设置别名访问。需要在 WebNet 服务器初始化之前设置该文件夹的别名,如下代码所示, 调用 webnet_alias_set 函数设置 /test 目录的别名为 /admin,浏览器访问 /test 时会跳转访问到 /admin 目录:

void webnet_test(void)
{
    /* 设置 /test 目录的别名为 /admin */
    webnet_alias_set("/test", "/admin");
    webnet_init(0);
}

Upload 文件上传功能

Upload 文件上传功能用于上传本地文件到 WebNet 服务器指定目录中,上传文件之前需要创建并实现上传文件结构体,如下所示:

struct webnet_module_upload_entry
{
    const char* url;                                         /* 文件上传的目录名 */

    int (*upload_open) (struct webnet_session* session);     /* 打开文件 */
    int (*upload_close)(struct webnet_session* session);     /* 关闭文件 */
    int (*upload_write)(struct webnet_session* session, const void* data, size_t length);    /* 写数据到文件 */
    int (*upload_done) (struct webnet_session* session);     /* 下载完成 */
};

该结构体定义上传文件的目录文件和需要使用的事件回调函数,如:打开文件、关闭文件、写数据到文件等。

用户需要根据实际情况完成回调函数的实现,各回调函数中大致操作如下:

  • upload_open : 通过解析的文件名称,在指定的目录创建和打开文件;

  • upload_close:关闭文件;

  • upload_write:写数据到打开为文件中;

  • upload_done:文件上传成功之后,对浏览器返回响应数据。

在回调函数实现的过程中,可能用到的获取当前上传文件会话信息的函数定义如下:

/* 获取当前上传文件名称 */
const char* webnet_upload_get_filename(struct webnet_session* session);
/* 获取当前上传文件类型 */
const char* webnet_upload_get_content_type(struct webnet_session* session);
/* 获取当前上传文件打开之后的文件描述符 */
const void* webnet_upload_get_userdata(struct webnet_session* session);

具体实现方式可以参考例程 /webnet/wn_sample_upload.c 中各个函数的实现方式。

最后,在 WebNet 初始化之前需要调用 webnet_upload_add 函数设置上传文件的信息,如下代码所示:

static int upload_open (struct webnet_session* session)
{
    /* 打开或新建文件 */
}
static int upload_close (struct webnet_session* session)
{
    /* 关闭文件 */
}
static int upload_write (struct webnet_session* session)
{
    /* 写数据到文件 */
}
static int upload_done (struct webnet_session* session)
{
    /* 下载完成,返回响应数据 */
}
const struct webnet_module_upload_entry upload_entry_upload =
{
    "/upload",
    upload_open,
    upload_close,
    upload_write,
    upload_done
};

void webnet_test(void)
{
    /* 注册文件上传执行函数 */
    webnet_upload_add(&upload_entry_upload);
    webnet_init(0);
}

对应页面上传文件的代码如下:

<html>
<body>
    <h3>Upload File Test</h3>
    文件上传模块可以用于上传文件到指定的目录,这里上传到根目录下的 /upload 目录。
    <br/><br/>
    <form name="upload" method="POST" enctype="multipart/form-data" action="/upload">
    <input type="file" name="file1" >
    <input type="submit" name="submit" value="上传">
    </form>
    <br/>
    <a href="/upload/">点击查看上传文件的目录</a>
    <br/><br/>
</body>
</html>

提示

当不使用文件系统时,只可以在内存中临时保存1份上传的文件,所能保存的文件大小由配置 file in ram size 决定,默认为 10240 字节。

预压缩功能

WebNet 服务器预压缩功能,需要在服务器端提前压缩页面资源文件,生成以 .gz 后缀的压缩文件。以根目录下 index.html 页面为例,浏览器访问该页面文件时,如果存在名称为 index.html.gz 的压缩文件,将发送该压缩文件内容到浏览器,浏览器自动解析后显示原页面文件。

使用预压缩功能时,需要先将压缩后的 index.html.gz 文件上传到文件系统中。

支持缓存功能

WebNet 缓存机制可以判断浏览器请求文件是否修改,从而决定是否发送完整文件内容到浏览器。 WebNet 软件包缓冲机制分为如下三个级别:

  1. level 0:关闭缓存功能,即浏览器每次都会从 WebNet 完整的读取文件内容;

  2. level 1:开启缓存功能,WebNet 通过读取请求文件的最后修改的时间,如果和本地文件相同,则返回 304 通知浏览器文件并无更新,不会发送文件;

  3. level 2:开启缓存功能,在原来判断修改时间的基础上,添加缓存文件有效时间支持,操作有效时间浏览器可重新访问该文件。

提示

缓存机制需要支持文件系统才能使用。

支持断点续传功能

WebNet 服务器支持断点续传功能,即客户端在下载文件中途出现错误,再次下载时只需要提供给 WebNet 服务器前一次文件下载的偏移量,服务器将从指定文件偏移量发送文件内容给客户端,断点续传功能可以确保文件上传快速、准确,提高服务器运行效率。

API参考

为了方便用户使用,这里列出了常用的 API,并给出了相关的使用说明。

初始化函数

int webnet_init(int keep_alive);

用于初始化 WebNet 服务器,包括创建线程用于监听客户端连接事件、初始化开启的功能模块等功能;

参数

描述

返回

--

= 0

初始化成功

< 0

初始化失败

关闭 webnet

int wm_close_webnet_thread(void);

用于关闭 WebNet 服务器,包括创建的线程;

参数

描述

返回

--

关联文件信息

void wn_associated_file_information(char *file_name, const uint8_t *file_content, const uint32_t file_size);

用于不使用文件系统时关联嵌入的文件信息。其中文件名需和前端页面相对应。

参数

描述

file_name

自定义文件名(需以 / 开头)

file_content

文件的数据指针

file_size

文件大小

返回

--

设置监听套接字端口

void webnet_set_port(int port);

用于设置当前 WebNet 服务器监听端口号,WebNet 服务器默认监听端口号是 80,这也是 HTTP 协议默认端口号。使用默认端口号访问 URL 地址时可以不输入端口号直接访问,当使用非默认端口号时,需要在 URL 地址上指明端口号,如: http://host:80/index.html 。该函数只能 用于 WebNet 服务器初始化之前

参数

描述

port

设置的监听套接字端口

返回

--

获取监听套接字端口

int webnet_get_port(void);

用于获取当前 WebNet 服务器监听套接字端口号。

参数

描述

返回

--

>=0

监听套接字端口号

设置服务器根目录

void webnet_set_root(const char* webroot_path);

用于设置当前 WebNet 服务器根目录路径,使用文件系统时 WebNet 服务器默认根目录为 /webnet,浏览器和 WebNet 函数中使用或访问的路径都是基于根目录路径。当浏览器访问 http://host/index.html 时,会把文件系统中的 /webnet/index.html 返回给浏览器。

参数

描述

webroot_path

设置的根目录地址

返回

--

< 0

初始化失败

获取服务器根目录

const char* webnet_get_root(void);

用于获取当前 WebNet 服务器根目录地址。

参数

描述

返回

--

!= NULL

根目录地址

获取请求链接的类型

const char* mime_get_type(const char* url);

用于获取当前请求 URL 链接的类型,如:网页、图片、文本等。

参数

描述

url

请求链接的地址

返回

--

!= NULL

请求链接的类型

添加 ASP 变量处理方式

void webnet_asp_add_var(const char* name, void (*handler)(struct webnet_session* session, void* handler_priv), void* priv);

该函数用于添加一个 ASP 变量处理方式,当 ASP 文件中出现添加的 name 变量名时,会执行对应的 handler 操作。

参数

描述

name

ASP 变量名称

void (handler)(struct webnet_session session, void* handler_priv)

ASP 变量处理方式

priv

需要传递给handler的参数

返回

--

添加 CGI 事件处理方式

void webnet_cgi_register(const char* name, void (*handler)(struct webnet_session* session, void* handler_priv), void* priv);

该函数用于注册一个 CGI 事件处理方式,当浏览器请求带有 name 名称的 URL 时,会执行相应的 handle 操作。

参数

描述

name

CGI 事件名称

void (handler)(struct webnet_session session, void* handler_priv)

CGI 事件处理方式

priv

需要传递给handler的参数

返回

--

设置 CGI 事件根目录

void webnet_cgi_set_root(const char* root);

WebNet 服务器默认的 CGI 事件根目录为 /cgi-bin,当浏览器请求 http://host/cgi-bin/test 地址时, 会执行 test 名称对应的 CGI 事件处理函数。

该函数用于设置新的 CGI 事件根目录,设置成功之前的 CGI 根目录将不再起作用。

参数

描述

root

CGI 事件根目录

返回

--

设置基本认证信息

void webnet_auth_set(const char* path, const char* username_password);

用于设置目录访问时的基本认证信息,包括用户名和密码。

参数

描述

path

需要设置基本认证信息的目录

username_password

设置的用户名和密码,格式为 username:password

返回

--

设置目录别名

void webnet_alias_set(char* old_path, char* new_path);

用于设置目录的别名,设置成功之后可以使用目录别名访问该目录。

参数

描述

old_path

需要设置别名的目录

new_path

设置的目录别名,一般为服务器中存在的目录

返回

--

发送 HTTP 请求头部

void webnet_session_set_header(struct webnet_session* session, const char* mimetype, int code, const char* title, int length);

用于拼接并发送头部信息到连接的客户端,一般用于 ASP 变量处理函数和 CGI 事件处理函数中。

参数

描述

session

当前服务器连接的会话

mimetype

需要发送的响应文件类型(Content-Type),可以使用 mime_get_type 函数获取

code

发送的响应状态码,正常为 200

title

发送的响应状态类型,正常为 OK

length

需要发送的响应文件长度(Content-Length)

返回

--

发送 HTTP 响应数据

int webnet_session_write(struct webnet_session* session, const uint8_t* data, size_t size);

用于发送响应数据到客户端,一般用于 ASP 变量处理函数和 CGI 事件处理函数中。

参数

描述

session

当前服务器连接的会话

data

发送的数据指针

size

发送的数据长度

返回

--

发送 HTTP 固定格式响应数据

void webnet_session_printf(struct webnet_session* session, const char* fmt, ...);

用于发送固定格式的响应数据到客户端,一般用于 ASP 变量处理函数和 CGI 事件处理函数中。

参数

描述

session

当前服务器连接的会话

fmt

自定义的输入数据的表达式

输入的参数

返回

--

获取上传文件的名称

const char* webnet_upload_get_filename(struct webnet_session* session);

获取当前上传文件的名称,用于打开或创建文件。

参数

描述

session

当前服务器连接的会话

fmt

自定义的输入数据的表达式

输入的参数

返回

--

!= NULL

当前上传文件的名称

获取上传文件的类型

const char* webnet_upload_get_content_type(struct webnet_session* session);

获取当前上传文件的类型。

参数

描述

session

当前服务器连接的会话

返回

--

!= NULL

当前上传文件的类型

获获取上传文件参数

const char* webnet_upload_get_nameentry(struct webnet_session* session, const char* name);

获取注册的上传文件的分隔符(HTTP 请求 boundary 参数)。

参数

描述

session

当前服务器连接的会话

name

上传文件的目录路径

返回

--

!= NULL

当前上传文件的类型

获取上传文件打开的文件描述符

const void* webnet_upload_get_userdata(struct webnet_session* session);

获取当前上传文件打开之后生成的文件描述符,用于读写数据到文件中。

参数

描述

session

当前服务器连接的会话

返回

--

!= NULL

上传文件打开的文件描述符

常见问题

Q: 浏览器访问设备 IP 地址不显示页面信息。

A:

  • 原因:设置的根目录地址错误。

  • 解决方法:确定设置的根目录地址和设备文件系统上创建的目录地址一致,确定根目录下有页面文件。