跳到主要内容

Web后端杂记

阅读量: 101
阅读人次: 102

Nginx 必要知识

这里只会记录 Nginx 的必要知识点,在实际应用中,OpenResty 提供了更为友好和强大的功能,所以应当:

  • 要尽可能少地配置 nginx.conf
  • 避免使用 ifsetrewrite 等多个指令的配合
  • 能通过 Lua 代码解决的,就别用 Nginx 的配置、变量和模块来解决。

这样可以最大限度地提高可读性、可维护性和可扩展性。

Nginx 通过配置文件来控制自身行为,它的配置可以看作是一个简单的 DSL。Nginx 在进程启动的时候读取配置,并加载到内存中。如果修改了配置文件,需要你重启或者重载 Nginx,再次读取后才能生效

每个指令都有自己适用的上下文(Context),也就是 NGINX 配置文件中指令的作用域。

最上层的是 main,里面是和具体业务无关的一些指令,比如上面出现的 worker_processes、pid 和 error_log,都属于 main 这个上下文。另外,上下文是有层级关系的,比如 location 的上下文是 server,server 的上下文是 http,http 的上下文是 main。

指令不能运行在错误的上下文中,NGINX 在启动时会检测 nginx.conf 是否合法。比如我们把 listen 80; 从 server 上下文换到 main 上下文,然后启动 NGINX 服务,会看到类似这样的报错:

"listen" directive is not allowed here ......

NGINX 不仅可以处理 HTTP 请求 和 HTTPS 流量,还可以处理 UDP 和 TCP 流量。

location匹配规则

location 的匹配规则是仅匹配 URI,忽略参数,有下面三种大的情况:

  • 前缀字符串
    • 常规匹配
    • =:精确匹配
    • ^~:匹配上后则不再进行正则表达式匹配
  • 正则表达式
    • ~:大小写敏感的正则匹配
    • ~*:大小写不敏感
  • 用户内部跳转的命名 location
    • @

先看一下 Nginx 的配置文件:

server {
listen 80;
server_name location.ziyang.com;
error_log logs/error.log debug;
#root html/;
default_type text/plain;
merge_slashes off;

location ~ /Test1/$ {
return 200 'first regular expressions match!\n';
}
location ~* /Test1/(\w+)$ {
return 200 'longest regular expressions match!\n';
}
location ^~ /Test1/ {
return 200 'stop regular expressions match!\n';
}
location /Test1/Test2 {
return 200 'longest prefix string match!\n';
}
location /Test1 {
return 200 'prefix string match!\n';
}
location = /Test1 {
return 200 'exact match!\n';
}
}

访问下面几个 URL 会分别返回什么内容呢?

/Test1
/Test1/
/Test1/Test2
/Test1/Test2/
/test1/Test2

例如访问 /Test1 时,会有几个部分都匹配上:

  1. 常规前缀匹配:location /Test1
  2. 精确匹配:location = /Test1

访问 /Test1/ 时,也会有几个部分匹配上:

  1. location ~ /Test1/$
  2. location ^~ /Test1/

那么究竟会匹配哪一个呢?Nginx 其实是遵循一套规则的,如下图所示:

全部的前缀字符串是放置在一棵二叉树中的,Nginx 会分为两部分进行匹配:

  1. 先遍历所有的前缀字符串,选取最长的一个前缀字符串,如果这个字符串是 = 的精确匹配或 ^~ 的前缀匹配,会直接使用
  2. 如果第一步中没有匹配上 = 或 ^~,那么会先记住最长匹配的前缀字符串 location
  3. 按照 nginx.conf 文件中的配置依次匹配正则表达式
  4. 如果所有的正则表达式都没有匹配上,那么会使用最长匹配的前缀字符串

下面看下实际的响应是怎么样的:

curl location.ziyang.com/Test1
# exact match!

curl location.ziyang.com/Test1/
# stop regular expressions match!

curl location.ziyang.com/Test1/Test2
# longest regular expressions match!

curl location.ziyang.com/Test1/Test2/
# longest prefix string match!

curl location.ziyang.com/Test1/Test3
# stop regular expressions match!
  • /Test1 匹配 location = /Test1
  • /Test1/ 匹配 location ^~ /Test1/
  • /Test1/Test2 匹配 location ~* /Test1/(\w+)$
  • /Test1/Test2/ 匹配 location /Test1/Test2
  • /Test1/Test3 匹配 location ^~ /Test1/

这里面重点解释一下 /Test1/Test3 的匹配过程:

  1. 遍历所有可以匹配上的前缀字符串,总共有两个
    • ^~ /Test1/
    • /Test1
  2. 选取最长的前缀字符串 /Test1/,由于前面有 ^~ 禁止正则表达式匹配,因此直接使用 location ^~ /Test1/ 的规则
  3. 返回 stop regular expressions match!

OpenResty

OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

Nginx [engine x] 是一个 HTTP 和反向代理服务器、邮件代理服务器和通用 TCP/UDP 代理服务器。

安装

sudo apt-get -y install --no-install-recommends wget gnupg ca-certificates
wget -O - https://openresty.org/package/pubkey.gpg | sudo gpg --dearmor -o /usr/share/keyrings/openresty.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/openresty.gpg] http://openresty.org/package/ubuntu $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/openresty.list > /dev/null
sudo apt-get update
sudo apt-get -y install openresty

下图是 lua-nginx-module 的一张图片。其中,init_by_lua 只会在 Master 进程被创建时执行,init_worker_by_lua 只会在每个 Worker 进程被创建时执行。其他的 *_by_lua 指令则是由终端请求触发,会被反复执行。

所以在 init_by_lua 阶段,我们可以预先加载 Lua 模块和公共的只读数据,这样可以利用操作系统的 COW(copy on write)特性,来节省一些内存。

对于业务代码来说,其实大部分的操作都可以在 content_by_lua 里面完成,但我更推荐的做法,是根据不同的功能来进行拆分,比如下面这样:

  • set_by_lua:设置变量;
  • rewrite_by_lua:转发、重定向等;
  • access_by_lua:准入、权限等;
  • content_by_lua:生成返回内容;
  • header_filter_by_lua:应答头过滤处理;
  • body_filter_by_lua:应答体过滤处理;
  • log_by_lua:日志记录。

NanoMQ

NanoMQ是2021年1月发布的开源边缘计算项目,是面向物联网边缘计算场景的下一代轻量级、高性能MQTT消息代理。

Github仓库地址:https://github.com/emqx/nanomq

NanoMQ 与 NNG 合作。 依托NNG优秀的网络API设计,NanoMQ可以专注于MQTT代理性能和更多扩展功能。 目标是在边缘设备和MEC中提供更好的SMP支持和高性价比。 未来计划添加其他物联网协议,例如 ZMQ、NanoMSG 和 SP。

使用 MQTTX 测试 MQTT 服务。

NNG

NNG 是一个轻量级、无需中间件(Broker-less)的库,它提供了一个简单的 API 来解决常见重复的消息传递问题,例如发布/订阅、RPC 请求/回复或服务发现。该 API 使我们无需担心连接管理、重试和其他常见注意事项等细节,因此可以使我们专注于应用程序而不是通讯过程。

  • 可靠性:NNG 从一开始就专为生产使用而设计。考虑到每种错误情况,并且设计为避免崩溃,除非出现严重的开发人员错误。
  • 可扩展性:NNG 使用定制的异步 I/O 框架进行扩展以使用多个核心,并使用线程池来分散负载。
  • 可维护性:NNG 的架构采用模块化设计,即使不熟悉代码库的开发人员也可以轻松掌握。代码也有很好的文档记录。
  • 可扩展性:由于它避免了与文件描述符的绑定,并避免了令人困惑的互锁状态机,因此更容易向 NNG 添加新的协议和传输。该特性通过添加 TLS 和 ZeroTier 传输得到了证明。
  • 安全性:NNG 提供 TLS 1.2 和 ZeroTier 传输,支持强大且符合行业标准的身份验证和加密。此外,它还经过强化,能够抵御恶意攻击者,并在某些恶劣的互联网环境中使用做了特殊优化。

NNG提供了六种传输协议:

  1. Pair - 点对点通信。
  2. PubSub - 发布/订阅模式。
  3. ReqRep - 请求/回复模式。
  4. Push/Pull - 用于负载分配的模式,其中 Push 端发送消息,Pull 端接收消息。
  5. Survey - 调查模式,允许请求者询问多个应答者。
  6. Bus - 总线模式,允许多对多通信。

NNG提供了七种传输方式:

  1. Inproc - 进程内通信。
  2. IPC - 进程间通信。
  3. TCP - 基于 TCP 的网络通信。
  4. TLS over TCP - 基于 tsl 加密 的 tcp socket 通信。
  5. WebSocket - 基于 WebSocket 的通信。
  6. BSD Socket - 支持BSD Socket API。
  7. ZeroTier - 支持 zerotier 网络。
注意

在使用TLS时,应尽可能使用 nng_tls_config_* 而不是通过 nng_listener_set_* 使用 NNG_OPT_TLS_CA_FILENNG_OPT_TLS_CERT_KEY_FILE

nng_tls_config *config = nullptr;
nng_tls_config_alloc(&config, NNG_TLS_MODE_SERVER);
nng_tls_config_ca_file(config, "resources/ca-cert.pem");
nng_tls_config_cert_key_file(config, "resources/server-cert-key.pem", NULL);
nng_listener_set_ptr(listener, NNG_OPT_TLS_CONFIG, config);
nng_tls_config_free(config);

在使用 dialer 时,使用 :

nng_dialer_set_string(dialer, NNG_OPT_TLS_CA_FILE, "resources/ca-cert.pem");

无法与服务器正常进行TLS握手,但是使用:

nng_tls_config *config = nullptr;
nng_tls_config_alloc(&config, NNG_TLS_MODE_CLIENT);
nng_tls_config_ca_file(config, "resources/ca-cert.pem");
nng_dialer_set_ptr(dialer, NNG_OPT_TLS_CONFIG, config);
nng_tls_config_free(config);

是表现正常的。

C++ Web Toolkit

Wt, C++ Web Toolkit 提供了使用 C++ 构建 Web 应用程序的方法,它提供了 Qt 风格的 API,便于理解。

Wt 支持三种部署方式:httpd、FastCGIISAPI。这里更推荐使用 Wt 的内置实现 httpd 服务器,这样便于调试,同时也支持 WebSocket,理论上使用 WebSocket 能够使 Wt 的 WebUI 交互更流畅,相比于使用 AJAX 一直像 Wt 请求实时状态更新。

在使用 wt 时,需要注意的就是:--docroot--resources-dir--approot--deploy-path

  • --approot :该路径用于存放 Web 浏览器不需获取,但应用程序内部需要使用的文件。例如 消息资源包 (Message Resource Bundles,xml)、CSV 文件、数据库文件(例如 Wt::Dbo 的 SQLite 文件)...

    如果该参数没有指定,则默认为 Wt 应用程序的工作路径,即应用程序运行的当前目录。

    messageResourceBundle().use(appRoot() + "text");
    messageResourceBundle().use(appRoot() + "charts");

    auto sqlite3_ = std::make_unique<Wt::Dbo::backend::Sqlite3>(appRoot() + "planner.db");
  • --deploy-path:指定 Wt 应用程序部署在哪个路径,默认是 /

  • --docroot:指定 Web 静态文件 .html.css 等存放路径根目录。可以在指定的根目录后加上;,然后加上以,分隔的静态文件路径列表(即使它们位于部署路径内)。例如:--docroot=.;/favicon.ico,/resources,/style

  • --resources-dir:Wt内部使用的 resources 文件夹的路径。默认情况下,Wt 将在 --docrootresources 子文件夹中查找其资源。如果在该资源文件夹中未找到文件,则将检查此文件夹作为后备(fallback机制)。如果省略此选项,则 Wt 将不使用后备(fallback)资源文件夹。

提示

当 Wt 应用程序部署在以/结尾的路径(即文件夹路径)时,只有与请求的 URL 完全匹配才会路由到应用程序本身。此行为避免了在/部署应用程序,导致 Web 服务器将无法提供任何静态文件。同时,将为内部路径(Internal Path)生成以?_=起始的丑陋的 URL。

// 第二个参数不填则默认为 /
addEntryPoint(EntryPointType::Application,createApplication);

即我们以上面的方式部署我们的应用时,只有访问 http://localhost:8080/ 才能访问,如果这个时候我们调用 WApplication::setInternalPath()时,内部会生成形如 http://localhost:8080/?_=/custom/path 的URL路径。且访问http://localhost:8080/apphttp://localhost:8080/app/loginhttp://localhost:8080/custom/path 等地址时,是匹配不到我们部署的应用程序的,这个时候 Wt 会在 --docroot 查找是否有静态资源文件,如果没有则返回 404。

而我们部署在:

addEntryPoint(EntryPointType::Application,createApplication, "wt");

http://localhost:8080/wthttp://localhost:8080/wt/apphttp://localhost:8080/wt/**任何在 /wt 下的路径都能访问到我们部署的应用程序。内部路径跳转也是生成 http://localhost:8080/wt/custom/path 这样的标准 url。

所以,问题是为什么部署在 / 下,则表现和我们所期望的不一致呢?(即我们所设想的是部署在 / 下时,任何路径都应指向到我们部署的应用程序。)

问题的原因就在于开头所述,即:

信息

Wt 不止提供应用程序部署,同时也支持提供静态资源文件的托管。

如果 Wt 实现的如我们所设想的这样,那么当我们部署在 / 下时,访问任何路径都将指向我们的应用,它就彻底失去了托管静态 html 文件的能力。这也是部署在 / 的应用程序内部路径 Wt 会自动加上 ugly url ?_= 前缀的原因。

从 Wt 3.1.9 版本开始,支持通过在 --docroot 中显示列出其目录包含静态源的子文件夹(用法如前对 --docroot 介绍时所述),就可以在/部署时,不再使用 ?_= 的 ugly url 方式解决上述问题。 例如当应用程序部署在 / 时,当我们访问 /app/login ,Wt 会先检查 /docroot/app 是否为静态资源文件夹,如果不是,则指向到我们部署的应用程序。

编译示例程序

在使用 Wt 构建 Web UI 时,其提供的很多控件我们并不是太清楚其样式,以及如何使用。所以,能够运行其示例代码是很重要的一步。

进入其源码目录,使用 CMake 编译构建:

cmake -G Ninja -B build -S . -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX=$(pwd)/app \
-DENABLE_QT4=OFF -DENABLE_QT5=OFF -DENABLE_QT6=OFF -DINSTALL_EXAMPLES=ON \
-DBOOST_ROOT=/opt/Libraries/boost_1_86_0
cmake --build build --target all
cmake --install build

构建完成后,示例位于 /app/lib/Wt/examples 下。以 widgetgallery 为例,直接运行 /app/lib/Wt/examples/widgetgallery/widgetgallery 即可。

Wt::Dbo

Wt::Dbo 是一个 C++ ORM(对象关系映射)库。该库作为 Wt 的一部分,用于构建数据库驱动的 Web 应用程序,但也可以独立使用。

该库提供了基于类的数据库表视图,通过插入、更新和删除数据库记录,使数据库对象的对象层次结构与数据库自动同步。C++ 类映射到数据库表,类字段映射到数据表列,指针和指针集合映射到数据库关系。映射类的对象称为数据库对象 (dbo)。查询结果可以根据数据库对象、基本数据类型或这些基本数据类型的元组来定义。

Wt::Dbo 使用现代 C++ 方法来解决映射问题。映射完全用 C++ 代码定义,而不是依靠基于 XML 的描述或者使用晦涩难懂的宏来描述 C++ 类和字段应如何映射到表和列。

Wt::Dbo::Session 对象是一个长期存在的对象,可用于访问我们的数据库对象。通常会为应用程序会话的整个生命周期创建一个 Session 对象,每个用户创建一个。Wt::Dbo 命名空间下的类都不是线程安全的(连接池除外),并且 Wt::Dbo::Session 对象不会在会话之间共享。

Wt::Dbo::Session 对象会获得一个连接,可用于与数据库通信。Wt::Dbo::Session 对象仅在事务期间使用连接,因此实际上不需要专用连接。当计划进行多个并发会话时,使用连接池是有意义的,并且Wt::Dbo::Session 对象也可以使用对连接池的引用进行构造。

C++类的映射

#include <Wt/Wt::Dbo/Wt::Dbo.h>
#include <string>

enum class Role {
Visitor = 0,
Admin = 1,
Alien = 42
};

class User {
public:
std::string name;
std::string password;
Role role;
int karma;

template<class Action>
void persist(Action& a) {
Wt::Dbo::field(a, name, "name");
Wt::Dbo::field(a, password, "password");
Wt::Dbo::field(a, role, "role");
Wt::Dbo::field(a, karma, "karma");
}
};

模板成员函数 persist() 提供了 User 类的持久化定义,Wt::Dbo::field() 函数提供了类成员变量到数据表的映射。

Wt::Dbo 支持标准 C++ 类型的映射,例如 intstd::stringenum 类型(可以在 Wt::Dbo::sql_value_traits<T> 的文档中找到支持类型的完整列表)。可以通过特化 Wt::Dbo::sql_value_traits<T> 来添加对其他类型的支持。目前还支持内置 Wt 类型,例如 WDateWDateTimeWTimeWString,可以通过包含 <Wt/Dbo/WtSqlTraits.h> 来启用映射支持。

Wt::Dbo 定义了许多操作(Action),这些操作将使用其 persist() 方法应用于数据库对象,该方法依次应用于其所有成员。然后,这些操作将读取、更新或插入数据库对象、创建数据表或传播事务结果。

CSS

推荐一个前端CSS框架:Bulma。Bulma 是一个移动端优先得 CSS 框架,其实现主要使用了 CSS 的 Flexbox 布局。所以,这也给我们一个提示,要想在现在 PC Mobile 的 Web 时代,要想让页面自适应,就得数量掌握 CSS 的 Flexbox 布局。

Cookie和Session是什么呢? HTTP是一种无状态协议,而Cookie和Session可以弥补 HTTP 的无状态特性。

Cookie是客户端请求服务端时,由服务端创建并由客户端存储和管理的小文本文件。具体流程如下:

  • 客户端首次发起请求。
  • 服务端通过HTTP响应头里Set-Cookie字段返回Cookie信息。
  • 客户端再发起请求时会通过HTTP请求头里Cookie字段携带Cookie信息

Session是客户端请求服务端时服务端会为这次请求创建一个数据结构,这个结构可以通过内存、文件、数据库等方式保存。具体流程如下:

  • 客户端首次发起请求。
  • 服务端收到请求并自动为该客户端创建特定的Session并返回SessionID,用来标识该客户端。
  • 客户端通过服务端响应获取SessionID,并在后续请求携带SessionID。
  • 服务端根据收到的SessionID,在服务端找对应的Session,进而获取到客户端信息。

基于Cookie和Session的登录态方案

什么是登录态?

主流Web应用比如浏览器是基于http协议的,而http协议是 无状态 的。什么是 无状态?就是服务器不知道是谁发送了这个http请求,无法识别区分用户身份。

所以登录态就是服务端用来区分用户身份,同时对用户进行记录的技术方案。

那怎么实现用户的登录态呢?常见的实现流程如下:

  1. 客户端用户输入登录凭据(如账户和密码),发送登录请求。
  2. 服务端校验用户是否合法(如认证和鉴权),合法后返回登录态,不合法返回第1步。
  3. 合法后携带登录态访问用户数据。

常见使用Cookie和Session认证流程如下:

  1. 客户端向服务端发送认证信息(例如账号密码)
  2. 服务端根据客户端提供的认证信息执行验证逻辑,如果验证成功则生成Session并保存,同时通过响应头Set-Cookie字段返回对应的SessionID
  3. 客户端再次请求并在Cookie里携带SessionID。
  4. 服务端根据SessionID查找对应的Session,并根据业务逻辑返回相应的数据。

Cookie和Session认证优点:

  • Cookie由客户端管理,支持设定有效期、安全加密、防篡改、请求路径等属性。
  • Session由服务端管理,支持有效期,可以存储各类数据。

Cookie和Session认证缺点

  • Cookie只能存储字符串,有大小和数量限制,对移动APP端支持不好,同时有跨域限制(主域不同)。
  • Session存储在服务端,对服务端有性能开销,客户端量太大会影响性能。如果集中存储(如存储在Redis),会带来额外的部署维护成本。