QtWebApp的使用【在Qt中搭建HTTP服务器】(二)

932 阅读12分钟

这篇博客介绍了QtWebApp中,form表单、session、cookie的使用。

处理用户输入

Web应用程序通常处理用户输入。我们将制作一个登录表单,以了解它的使用方法。

创建一个新类 LoginController。同样,它源自HttpRequestHandler。

//logincontroller.h:

#ifndef LOGINCONTROLLER_H
#define LOGINCONTROLLER_H

#include "httprequesthandler.h"

using namespace stefanfrings;

class LoginController : public HttpRequestHandler {
    Q_OBJECT
public:
    LoginController(QObject* parent=0);
    void service(HttpRequest& request, HttpResponse& response);
};

#endif // LOGINCONTROLLER_H
logincontroller.cpp:
#include "logincontroller.h"

LoginController::LoginController(QObject* parent)
    :HttpRequestHandler(parent) {
    // empty
}

void LoginController::service(HttpRequest &request, HttpResponse &response) {
    QByteArray username=request.getParameter("username");
    QByteArray password=request.getParameter("password");

    qDebug("username=%s",username.constData());
    qDebug("password=%s",password.constData());

    response.setHeader("Content-Type", "text/html; charset=ISO-8859-1");
    response.write("<html><body>");

    if (username=="test" && password=="hello") {
        response.write("Yes, correct");
    }
    else {
        response.write("<form method='POST' action='/login'>");
        if (!username.isEmpty()) {
            response.write("No, that was wrong!<br><br>");
        }
        response.write("Please log in:<br>");
        response.write("Name:  <input type='text' name='username'><br>");
        response.write("Password: <input type='password' name='password'><br>");
        response.write("<input type='submit'>");
        response.write("</form");
    }

    response.write("</body></html>",true);
}

如上文所述,将此新控制器添加到请求映射器中。

//requestmapper.h中的新行:

#ifndef REQUESTMAPPER_H
#define REQUESTMAPPER_H

#include "httprequesthandler.h"
#include "helloworldcontroller.h"
#include "listdatacontroller.h"
#include "logincontroller.h"

class RequestMapper : public HttpRequestHandler {
    Q_OBJECT
public:
    RequestMapper(QObject* parent=0);
    void service(HttpRequest& request, HttpResponse& response);
private:
    HelloWorldController helloWorldController;
    ListDataController listDataController;
    LoginController loginController;
};

#endif // REQUESTMAPPER_H
//在requestmapper.cpp中添加三行:
#include "requestmapper.h"

RequestMapper::RequestMapper(QObject* parent)
    : HttpRequestHandler(parent) {
    // empty
}

void RequestMapper::service(HttpRequest& request, HttpResponse& response) {
    QByteArray path=request.getPath();
    qDebug("RequestMapper: path=%s",path.data());

    if (path=="/" || path=="/hello") {
        helloWorldController.service(request, response);
    }
    else if (path=="/list") {
        listDataController.service(request, response);
    }
    else if (path=="/login") {
        loginController.service(request, response);
    }
    else {
        response.setStatus(404,"Not found");
        response.write("The URL is wrong, no such document.");
    }

    qDebug("RequestMapper: finished request");
}

运行程序,并在浏览器中打开UR: http://localhost:8080/login。你将看到以下表格:

在这里插入图片描述
尝试使用错误的名称和密码登录。浏览器会显示错误消息“No, that was wrong!”,并提示重试。如果输入正确的凭据(用户名“ test”和密码“ hello”),则会收到成功消息。

源代码中没有太多需要解释的内容。HTML表单使用名称定义了两个输入字段"username" 和 “password”。控制器使用request.getParameter() 获取值。

当参数为空或传入的HTTP请求中没有此类参数时,Request.getParameter() 返回一个空的 QByteArray。后一种情况会发生在你刚打开 http://localhost:8080/login 的时候。表单字段仅在用户单击提交按钮时才从Web浏览器发送到Web服务器。

如果需要区分空白字段和缺失字段,则可以使用 request.getParameterMap() 来检查必需的参数是否在返回的映射中。

作为表单的替代形式,参数也可以作为URL的一部分进行传输。例如,可以通过打开URL:http://localhost:8080/login?username=test&password=hello 进行登录。

在这里插入图片描述

注意:在URL中使用某些特殊字符时,必须将它们编码为转义序列。例如,如果用户名是"Stefan Frings",就得写成 http://localhost:8080/login?username=Stefan%20Frings&password=hello。HttpRequest 类解码后会自动返回原始格式 “Stefan Frings”。

如果您需要将字符串编码为URL格式,则可以使用QUrl类。

Session 会话

下一步,我们会将有关当前用户的数据保存在某个地方,并在后续请求中使用该数据。我们将要存储的第一个数据是用户名和用户登录时间,这里需要使用到session(会话)。

什么是Session

Session 是另一种记录浏览器状态的机制。与Cookie不同的是,Cookie保存在浏览器中,Session保存在服务器中。用户使用浏览器访问服务器的时候,服务器把用户的信息以某种的形式记录在服务器,这就是Session。

如果说Cookie是检查用户身上的”通行证“来确认用户的身份,那么Session就是通过检查服务器上的”客户信息明细表“来确认用户的身份的。Session相当于在服务器中建立了一份“客户信息明细表”。

为什么要使用Session技术?

Session比Cookie使用方便,Session可以解决Cookie解决不了的事情(Session可以存储对象,Cookie只能存储字符串)。

回到我们的程序中,我们必须在配置文件webapp1.ini中添加一个新部分来控制会话存储类:

[sessions]
expirationTime=3600000
cookieName=sessionid
;cookieDomain=mydomain.com
cookiePath=/
cookieComment=Identifies the user
  • expirationTime定义了一个数值(毫秒为单位),超过这个时间,将从内存中删除未使用的会话。当用户在规定的时间后回到服务器,其会话将丢失,因此它必须再次登录。
  • Cookie名称可以是任何名称,但是一般用 “sessionid”。一些负载均衡器依赖于该名称,因此除非有充分的理由,否则最好不要更改它。
  • 每个Cookie始终绑定到一个域。例如,google.com生成的Cookie仅发送到该域中的服务器。如果将cookieDomain设置为空,或将其注释掉,则Web浏览器会自动设置该参数。你也可以指定另一个域名,但是,除非你知道自己在做什么,否则最好将其留空。
  • Cookie路径可用于将Cookie限制为您域的一部分。如果你将 cookiePath 改为:/my/very/cool/online/shop,那么浏览器仅将以该路径开头的页面的cookie发送到你的服务器。默认值为“ /”,这表示该Cookie对你域中的所有网站均有效。
  • cookieComment 是一些Web浏览器显示在Cookie管理屏幕里的文本。

我们需要一个整个程序都可以达到的 HttpSessionStore 类的实例,因此它是全局范围的。我们需要创建两个新文件,第一个global.h:

//global.h
#ifndef GLOBAL_H
#define GLOBAL_H

#include "httpsessionstore.h"

using namespace stefanfrings;

extern HttpSessionStore* sessionStore;

#endif // GLOBAL_H
//And global.cpp:
#include "global.h"

HttpSessionStore* sessionStore;

现在我们有了一个名称为"sessionStore"的全局静态指针,整个程序可以通过包含文件global.h访问到它。让我们加载新的配置设置并初始化sessionStore。

main.cpp中的更改:

#include "global.h"

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    QString configFileName=searchConfigFile();

    // Session store
    QSettings* sessionSettings=new QSettings(configFileName,QSettings::IniFormat,&app);
    sessionSettings->beginGroup("sessions");
    sessionStore=new HttpSessionStore(sessionSettings,&app);

    // HTTP server
    QSettings* listenerSettings=new QSettings(configFileName,QSettings::IniFormat,&app);
    listenerSettings->beginGroup("listener");
    new HttpListener(listenerSettings,new RequestMapper(&app),&app);

    return app.exec();
}

注意:现在,main.cpp两次加载配置文件。sessionSettings 对象挑选出 “sessions” 部分,而 listenerSettings 挑选出 "listener"部分。对于每个部分,都需要一个单独的实例QSettings来接收,否则可能会造成混淆。

现在我们已经为会话数据创建了存储,可以开始使用它了。添加到logincontroller.cpp:

#include <QTime>	//新增
#include "logincontroller.h"
#include "global.h"	//新增

LoginController::LoginController(QObject* parent)
    :HttpRequestHandler(parent) {
    // empty
}

void LoginController::service(HttpRequest &request, HttpResponse &response) {
    HttpSession session=sessionStore->getSession(request,response,true);	//新增
    QByteArray username=request.getParameter("username");
    QByteArray password=request.getParameter("password");

    qDebug("username=%s",username.constData());
    qDebug("password=%s",password.constData());

    response.setHeader("Content-Type", "text/html; charset=ISO-8859-1");
    response.write("<html><body>");

    if (session.contains("username")) {
        QByteArray username=session.get("username").toByteArray();	//新增
        QTime logintime=session.get("logintime").toTime();	//新增
        response.write("You are already logged in.<br>");	//新增
        response.write("Your name is: "+username+"<br>");	//新增
        response.write("You logged in at: "+logintime.toString("HH:mm:ss").toLatin1()+"<br>");	//新增
    }
    else {
        if (username=="test" and password=="hello") {
            response.write("Yes, correct");
            session.set("username",username);	//新增
            session.set("logintime",QTime::currentTime());	//新增
        }
        else {
            response.write("<form method='POST' action='/login'>");
            if (!username.isEmpty()) {
                response.write("No, that was wrong!<br><br>");
            }
            response.write("Please log in:<br>");
            response.write("Name:  <input type='text' name='username'><br>");
            response.write("Password: <input type='password' name='password'><br>");
            response.write("<input type='submit'>");
            response.write("</form");
        }
    }

    response.write("</body></html>",true);
}

注意:sessionStore->getSession() 的调用要先于第一次调用 response.write(),因为它会创建或刷新会话cookie。严格来说,它是HTTP标头,而且所有HTTP标头必须在HTML文档之前发送。

运行程序,并打开URL:http://localhost:8080/login。并输入用户名和密码。

现在,查看Web服务器的控制台窗口时,将会看到已创建一个cookie和一个具有唯一ID的会话,该ID是一个较长的随机十六进制数字。

在这里插入图片描述

从Chrome浏览器中可以看到当前网页的cookie:

这里sessionid的内容和控制台窗口中显示的是一样的。
在这里插入图片描述
这里可以看到该会话的有效时间是一个小时,也就是我们在配置文件中指定的3600000毫秒。

在这里插入图片描述

用户会话最初是空的,它只是存在并且具有唯一的ID号,没有其他的。确认成功登录后,服务器会将用户名和登录时间放入用户会话。我们可以将任何QVariant支持的对象放入会话中。将对象放入会话中时,需要为其赋予名称,以供以后访问。

现在再次打开URL:http://localhost:8080/login。然后你会看到:

在这里插入图片描述

因此,在成功验证用户名和密码后,服务器将使用该会话来记住该用户的数据。

会话存储区保留在内存中。重新启动Web服务器时,会话存储中的所有数据都会丢失。因此,仅将其用于一些临时数据。

cookie

作为会话存储的替代方法,您还可以在Cookie中存储少量数据。Cookies存储在Web浏览器的客户端,而不是服务器。Cookie只能存储8位文本,并且保证仅4千字节有效。此外,每个域的Cookie数量是有限的,因此请尽量节省地使用它们。

下面,让我们添加一个新的控制器类,其名称为 CookieController 并将其绑定到路径 “/cookie”。

//cookiecontroller.cpp(.h文件略,和之前几个控制类的写法无异)
#include "cookiecontroller.h"

CookieController::CookieController(QObject *parent)
    :HttpRequestHandler(parent){

}

void CookieController::service(HttpRequest &request, HttpResponse &response) {

    QByteArray cookie=request.getCookie("testcookie");
    if (!cookie.isEmpty()) {
        response.write("Found cookie with value: "+cookie,true);
    }
    else {
        HttpCookie cookie("testcookie","123456789",60);
        response.setCookie(cookie);
        response.write("A new cookie has been created.",true);
    }

}

requestmapper.cpp 作如下改动:

#include "requestmapper.h"


RequestMapper::RequestMapper(QObject* parent)
    : HttpRequestHandler(parent) {
    // empty
}

void RequestMapper::service(HttpRequest& request, HttpResponse& response) {
    QByteArray path=request.getPath();  //获取请求路径
    qDebug("RequestMapper: path=%s",path.data());

    //请求映射器的作用:根据不同的url调用不同的控制器
    if (path=="/" || path=="/hello") {
        helloWorldController.service(request, response);
    }
    else if (path=="/list") {
        listDataController.service(request, response);
    }
    else if(path=="/login") {
        loginController.service(request,response);
    }
    //新增
    else if(path=="/cookie") {
        cookieController.service(request,response);	//cookieController是成员变量
    }
    else {
        response.setStatus(404,"Not found");
        response.write("The URL is wrong, no such document.",true);
    }

    qDebug("RequestMapper: finished request");
}

运行程序,并在浏览器中打开URL:http://localhost:8080/cookie

在这里插入图片描述
再次加载同一页面:
在这里插入图片描述
Cookies存储在Web浏览器中,并随每个HTTP请求发送到Web服务器。除了会话数据外,如果重新启动服务器,cookie不会丢失,因为cookie存储在客户端

在Chrome浏览器中查看cookie:
在这里插入图片描述
有效时间仅有1分钟。

在这里插入图片描述
在这里还可以看到会话cookie仍然存在。现在,我们有两个用于本地主机的cookie。

如果你等待几分钟,然后再次加载 http://localhost:8080/cookie ,你将会看到testcookie过期,服务器会再创建一个新的cookie:
在这里插入图片描述
在这里插入图片描述
如果要防止cookie在用户处于活动状态时过期,则必须在每个请求上重新创建cookie。然后,浏览器针为每个请求计算新的到期时间。

HTTP重定向

有时,你可能想将浏览器重定向到另一个页面,就需要使用到重定向。重定向常用于需要用户登录的网站。如果用户未登录,则将其重定向到登录页面。当然,匿名用户必须可以访问登录页面本身。

requestmapper.cpp中的更改:

#include "global.h"

void RequestMapper::service(HttpRequest& request, HttpResponse& response) {
    QByteArray path=request.getPath();
    qDebug("RequestMapper: path=%s",path.data());

    QByteArray sessionId=sessionStore->getSessionId(request,response).getId();
    sessionId.clear();	//清除sessionId,看是否能实现重定向
    if (sessionId.isEmpty() && path!="/login") {
        qDebug("RequestMapper: redirect to login page");
        response.redirect("/login");
        return;
    }

    else if (path=="/login") {
        ...
    }

    else if (path=="/whatever") {
        ...
    }

    qDebug("RequestMapper: finished request");
}

运行程序,在浏览器上打开URL:http://localhost:8080/haha,将会看到页面跳转至:

在这里插入图片描述

国际化

为什么HTTP服务器始终使用 QByteArray 代替 QString?这有一个简单的原因:性能。

整个HTTP协议基于8位编码,因此不通过不必要的前后转换来浪费CPU时间。

但你当然可以使用Unicode。看一个例子:

void UnicodeController::service(HttpRequest& request, HttpResponse& response) {
    QString chinese=QString::fromUtf8("美丽的花朵需要重症监护");
    response.setHeader("Content-Type", "text/html; charset=UTF-8");
    response.write(chinese.toUtf8(),true);
}

这就是Google翻译提供的“beautiful flowers need intensive care”的中文翻译。

将QString转换为UTF-8的性能要不低于转换为Latin1的性能。因此,如果需要,你可以随意使用Unicode。但是不要忘记使用QString::fromUtf8。如果只写chinese=“美丽的花朵需要重症监护”,就只会得到乱码。

静态文件

如果QtWebapp无法传送存储在服务器上的文件夹中的静态文件,则它将是不完整的。StaticFileController 提供该功能。但是在使用它之前,需要在ini文件中进行一些其他配置设置:

[files]
path=../docroot
encoding=UTF-8
maxAge=90000
cacheTime=60000
cacheSize=1000000
maxCachedFileSize=65536
  • path设置指定静态文件存储的基本文件夹,它是相对于配置文件的。也可以用绝对路径名称,例如“/opt/server/docroot”或“C:/server/docroot”。
  • encoding参数仅用于 * .txt 和 * .html 文件,以告诉浏览器这些文件具有哪些编码。如果同时需要不同的编码,则必须创建多个StaticFileController,每个编码一个。

其他参数控制缓存。首先,你应该知道操作系统已经缓存了文件。但是Linux和Windows在处理小文件时都不能很好地执行。因此,建议使用应用程序内部缓存(但仅用于小文件)。

  • cacheTime 控制在内存中文件最多保留多少毫秒。值 = 0 表示只要有足够的空间,文件就会保留在内存中。
  • CacheSize 指定允许缓存占用多少内存。如果人们请求的文件不在缓存中,则会删除最旧的文件,以释放空间给新文件使用。
  • maxCachedFileSize 控制缓存中单个文件的最大大小。Web服务器应用程序不缓存较大的文件。但操作系统却能很好地缓存大文件。
  • maxAge 参数与 cacheTime 的作用几乎一致,但它可以控制Web浏览器(而非服务器)的缓存。

我们需要一个StaticFileController 实例的全局指针,使整个程序都可以访问它。首先添加到global.h:

#ifndef GLOBAL_H
#define GLOBAL_H

#include "httpsessionstore.h"
#include "staticfilefontroller.h"

using namespace stefanfrings;

extern HttpSessionStore* sessionStore;
extern StaticFileController* staticFileController;

#endif // GLOBAL_H

和global.cpp:

#include "global.h"

HttpSessionStore* sessionStore;
StaticFileController* staticFileController;

在main.cpp中,我们配置了 StaticFileController:

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    QString configFileName=searchConfigFile();

    // Session store
    QSettings* sessionSettings=new QSettings(configFileName,QSettings::IniFormat,&app);
    sessionSettings->beginGroup("sessions");
    sessionStore=new HttpSessionStore(sessionSettings,&app);

    // Static file controller
    QSettings* fileSettings=new QSettings(configFileName,QSettings::IniFormat,&app);
    fileSettings->beginGroup("files");
    staticFileController=new StaticFileController(fileSettings,&app);

    // HTTP server
    QSettings* listenerSettings=new QSettings(configFileName,QSettings::IniFormat,&app);
    listenerSettings->beginGroup("listener");
    new HttpListener(listenerSettings,new RequestMapper(&app),&app);

    return app.exec();
}

现在我们可以使用 staticFileController 了。

在requestmapper.cpp中:

#include "requestmapper.h"
#include "httpsession.h"
#include "global.h"

void RequestMapper::service(HttpRequest& request, HttpResponse& response) {
    QByteArray path=request.getPath();
    qDebug("RequestMapper: path=%s",path.data());

    if (path=="/" || path=="/hello") {
        helloWorldController.service(request, response);
    }
    else if (path=="/list") {
        listDataController.service(request, response);
    }
    else if (path=="/login") {
        loginController.service(request, response);
    }
    else if (path=="/cookie") {
        cookieTestController.service(request, response);
    }
    else if (path.startsWith("/files")) {
        staticFileController->service(request,response);
    }
    else {
        response.setStatus(404,"Not found");
        response.write("The URL is wrong, no such document.");
    }

    qDebug("RequestMapper: finished request");
}

现在创建文件夹MyFirstWebApp/docroot/files,然后创建一个名为 hello.html 的HTML文件:

<html>
    <body>
        Hello World!
    </body>
</html>

启动程序并打开 http://localhost:8080/files/hello.html。浏览器将收到该文件的内容。

您可以将其他文件(图像,css,javascript,…)添加到此文件夹,并根据需要创建更多子文件夹。

如果出现“找不到文件”错误,多半是因为配置文件中的路径写错了,调试消息将帮助您找出服务器真正尝试加载的文件,修改配置文件即可。

到目前为止,我们已经学完了如何使用 QtWebApp 的 HTTP Server 模块的所有对象。