2023年3月

iptables的使用

容器系列文章

容器系列视频

iptables介绍

iptables是基于内核提供的netfilter框架实现的,网络协议栈是分层的,在tcp ip网络模型里,tcp传输层下面的一层就是ip网络层,而netfilter就是工作在ip网络层,通过定义钩子函数,允许用户代码干预数据在协议栈中的过滤逻辑。

iptables 钩子函数位置

在进出ip路由前后,都定义了相关的钩子函数,可以在钩子函数内部加上过滤数据包的逻辑,但直接使用netfilter还是比较麻烦,需要写代码。

而iptables则是基于netfilter提供的功能,让使用者能用配置的方式替代之前直接写代码的操作。可以说它简化了netfilter的使用。

规则定义

在看iptables使用规则前,得先明白这5个钩子函数的触发规则。

钩子函数

PREROUTING
:
在进入 IP 路由之前触发,就意味着只要接收到的数据包,无论是否真的发往本机,也都会触发这个钩子。它一般是用于目标网络地址转换(Destination NAT,DNAT)。

INPUT
:
报文经过 IP 路由后,如果确定是发往本机的,将会触发这个钩子,它一般用于加工发往本地进程的数据包。

FORWARD
:
报文经过 IP 路由后,如果确定不是发往本机的,将会触发这个钩子,它一般用于处理转发到其他机器的数据包。

OUTPUT
:
从本机程序发出的数据包,在经过 IP 路由前,将会触发这个钩子,它一般用于加工本地进程的输出数据包。

POSTROUTIN
:
从本机网卡出去的数据包,无论是本机的程序所发出的,还是由本机转发给其他机器的,都会触发这个钩子,它一般是用于源网络地址转换(Source NAT,SNAT)

在使用iptables时,能够在特定的钩子函数上定义一条条规则,而为了更好的管理这些规则,iptables将这些规则按功能进行分类,这样相同目的的规则就形成了规则表。

规则表

我们来看看:

raw 表
:
用于去除数据包上的连接追踪机制(Connection Tracking)。

mangle 表
:
用于修改数据包的报文头信息,比如服务类型(Type Of Service,ToS)、生存周期(Time to Live,TTL)。

nat 表
:
用于修改数据包的源或者目的地址等信息,典型的应用是网络地址转换(Network Address Translation)。

filter 表
:
用于对数据包进行过滤,控制到达某条链上的数据包是继续放行、直接丢弃或拒绝(ACCEPT、DROP、REJECT),典型的应用是防火墙。

security 表
:
用于在数据包上应用SELinux,这张表并不常用。

nat和filter表用的很频繁,这也是今天分析的重点。

五张表能够在相应的钩子函数上设定规则,而如果碰到不同表在相同的钩子函数上设定规则,那么规则谁先谁后呢,这就需要知道表的优先级顺序。
表优先级:raw→mangle→nat→filter→security。

这里你要注意,在 iptables 中新增规则时,需要按照规则的意图指定要存入到哪张表中,如果没有指定,就默认会存入 filter 表。此外,每张表能够使用到的钩子函数也有所不同。

规则表对应的钩子函数范围

命令格式

有了这些基础之后,我们再来看看iptables的命令格式:

iptables -t nat 命令 规则链 规则

在使用iptables的时候,我们用-t 指定定义的规则是属于哪张规则表的,如果不指定,则默认是指filter表。 而命令则是说明需要对规则表所进行的操作,比如查看添加删除规则链。

iptables命令 -L 代表查看 -A 代表插入到尾部, -D 删除规则 -F 清空规则

规则链则是具体的定义按什么规则去匹配数据包,如筛选出源ip是10.1.0.1的数据包,或者目的端口是8080的数据包。 规则即是对匹配的数据包所做的操作,比如是丢弃还是接受。

这里我列几个比较常用的规则;

DROP:直接将数据包丢弃。

REJECT:给客户端返回 Connection Refused 或 Destination Unreachable 报文。

RETURN:跳出当前链,该链里后续的规则不再执行。

ACCEPT:同意数据包通过,继续执行后续的规则。

JUMP:跳转到其他用户自定义的链继续执行。

REDIRECT:在本机做端口映射。

MASQUERADE:地址伪装,自动用修改源或目标的 IP 地址来做 NAT。

来看一个实际的iptables的实际使用例子。

iptables -t nat -A POSTROUTING -s 192.168.10.0/24 -j MASQUERADE

这条命令就代表往nat规则表中添加一条规则,规则在POSTROUTING钩子函数处触发,规则是将源ip是192.168.10.0/24网段的数据包都做一次snat操作。 -s 指定源ip地址或ip网段

iptables -A INPUT -p tcp -s 192.168.10.0/24 -j DROP

上面这条命令没有用-t参数则说明默认是往filter规则表中添加,规则是在INPUT钩子函数处被触发,-p 指定匹配的协议,这里将源ip是192.168.10.0/24网段的tcp协议的数据包都丢掉了。-j 指定的就是规则链之后的动作。

除了添加内置的规则表中添加规则 ,iptables还允许用户自定义规则链表,这里将不再深入分析,本文目的仅是简单的了解下iptables的使用,以及能看懂iptables的输出即可,为后续分析容器网络环境做准备。

iptables 输出结果分析

知道iptables命令是如何使用之后,我们再来看看iptables是如何查看主机上的规则,以及如何对规则输出的结果进行分析。

iptables -nvL

这个命令能直接输出主机上的filter表的规则链,-n 代表不解析ip地址的域名,-v则是能输出更多的信息,-L则是查看命令了。

关于iptables 的命令参数有很多,更多详细的规则都可以通过man iptables去查看。

看看输出结果。

root@master:/home/parallels# iptables -nvL
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
  20M 4214M KUBE-NODEPORTS  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes health check service ports */
 293K   23M KUBE-EXTERNAL-SERVICES  all  --  *      *       0.0.0.0/0            0.0.0.0/0            ctstate NEW /* kubernetes externally-visible service portals */
  20M 4220M KUBE-FIREWALL  all  --  *      *       0.0.0.0/0            0.0.0.0/0

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 FLANNEL-FWD  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* flanneld forward */
    0     0 KUBE-FORWARD  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes forwarding rules */

.......
Chain KUBE-KUBELET-CANARY (0 references)
 pkts bytes target     prot opt in     out     source               destination

Chain KUBE-NODEPORTS (1 references)
 pkts bytes target     prot opt in     out     source               destination

输出的规则按不同规则链进行了归类,除了之前提到的iptables内置的5个规则链表,还有一些是自定义的规则链,自定义的规则链只能通过内置的规则链去进行跳转。

比如这里的第一行输出:

Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
  20M 4214M KUBE-NODEPORTS  all  --  *      *       0.0.0.0/0            0.0.0.0/0            /* kubernetes health check service ports */

则表示,匹配所有经过INPUT链的数据包(source 为0.0.0.0/0 表示任意源ip地址,destination为0.0.0.0/0 表示任意目的ip地址,prot 为all表示任意协议,in out皆为all 表示输入输出可以是任意网络设备,opt则是一些定义规则链时候的扩展参数,这里为空) 都将跳转到KUBE-NODEPORTS这条自定义规则链。

pkts 表示经过此规则的包数量,bytes则是经过此规则的包大小,都是累加值。

然后

Chain KUBE-NODEPORTS (1 references)
 pkts bytes target     prot opt in     out     source               destination

看到KUBE-NODEPORTS 没有定义规则,则回到上层规则链处,继续执行下一条规则。

通过上面的分析,应该能够看懂iptables的输出了,后续我将会结合iptables命令,看看容器环境下,对主机的iptables规则做了哪些改动,敬请期待。

本文发表于 2020 年 6 月 8 日,虽然时间较久远,但现在看起来仍然是非常有价值的一篇文章。

在这个全民 LLM 的狂欢里,想测测你拿到的预算够训一个多大的模型吗?本文会给你一个答案,至少给你一个计算公式。

在自然语言处理领域,有时候我们恍惚觉得大家是为了搏头条而在模型尺寸上不断进行军备竞赛。
1750 亿参数
无疑是一个很抓眼球数字!为什么不考虑高效地去训一个小一点的模型?其实,这是因为深度学习领域有一个挺惊人的缩放效应,那就是:
大神经网络计算效率更高
。这是以 OpenAI 为代表的团队在像
神经语言模型的缩放定律
这样的论文中探索出的结论。 本文的研究也基于这一现象,我们将其与 GPU 速度估计相结合,用于确保在进行语言模型实验时,我们能根据我们算力预算来设计最合适的模型尺寸 (剧透一下,这个大小比你想象的要大!)。我们将展示我们的方法是如何影响一个标准的语言建模基准的架构决策的:
我们在没有任何超参优化的前提下,仅使用了原论文 75% 的训练时间,复现了
Zhang 等人的 Transformer-XL 论文
中的 14 层模型的最佳结果

。我们还估计
来自同一篇论文的 18 层模型其实仅需要比原论文少一个数量级的步数就能达到相同的结果
。继续阅读之前想先玩玩我们的演示吗?只需单击
此处

1. 停止训练的最佳时间 (比你想象的要早)

我们先观察一些损失曲线 (loss curve)。我们使用的任务是在 Wikitext-103 上训练 Transformer-XL 语言模型,Wikitext-103 是一个标准的中等体量的测试基准。 GPT-2 在此等体量的数据集上表现不佳。随着训练的进行,我们来观察计算成本 (通过浮点运算数来衡量) 与模型性能 (通过验证集上的损失来衡量) 的联动关系。我们做点实验吧!在下图中,不同颜色的线段表示不同层数和大小的 Transformer-XL 模型运行 200000 步的数据,这些模型除了层数与大小外的所有其他超参数都相同。模型参数量范围从几千到一亿 (不含嵌入)。越大的模型在图中越靠右,因为它们每一步需要的计算量更大。本图是交互式的,你可以玩一玩!

体验地址: https://hf.co/calculator/


缩放定律
一文中的做法一样,我们的横轴为非嵌入浮点运算数 (non-embedding FLoating Point Operations, neFLOs),纵轴为验证集损失。对于给定的 neFLOs 预算,似乎存在一个任何模型都没法越过的性能边界,我们在图中用红色线段表示。在
缩放定律
一文中,它被称为计算边界 (compute frontier)。我们可以看到,在所有的实验中,几乎每个实验都能在经过初始若干步的损失迅速降低后达到或接近该计算边界,随后又在训练接近尾声时,因训练效率降低而偏离该计算边界。这个现象有其实际意义: 给定固定的浮点运算预算,为了达到最佳性能,你应该选择一个模型尺寸,使得在浮点运算预算见顶时正好达到计算边界,然后我们就可以在此时停止训练。此时离模型收敛所需的时间还很远,模型收敛还需要 10 倍左右的时间。事实上,如果此时你还有额外的钱可用于计算,你应该把大部分用到增大模型上,而只将一小部分用于增加训练步数。[
译者注: 这是因为性能边界本质上度量了每 neFLOs 带来的 loss 的降低是多少,到达计算边界后,后面的每 neFLOs 能带来的 loss 的降低变小,不划算了。我们应该转而去寻求增大模型所带来的接近计算边界的高回报,而不应该卷在增加训练步数带来的低回报上。
]


缩放定律
一文中,OpenAI 团队用幂律函数拟合了一个 GPT-2 训练的计算边界。这似乎也适用于我们的任务,我们也拟合了一个以预算为自变量,最适合该预算的模型参数量为因变量的幂律函数。如下图所示。

体验地址: https://hf.co/calculator/

由于好的模型的
neFLOs- 损失
曲线往往会与计算边界相切比较长时间,因此最终的拟合函数会存在一些噪声。然而,这恰恰也意味着基于该拟合函数的估计的容忍度会比较好,即使我们预测尺寸有点偏差,其结果仍然会非常接近最优值。我们发现,如果将算力预算乘以 10,最佳模型尺寸会随之乘以 7.41,而最佳训练步数仅乘以 1.35。将此规则外推到 Tranformer-XL 论文中的那个更大的 18 层最先进模型,我们发现
其最佳训练步数约为 25 万步
。即使这个数字由于模型尺寸的变化而变得不那么精确,它也比论文中所述的
收敛所需的 400 万步
小得多。以更大的模型、更少的训练步数为起点,在给定的 (庞大的) 预算下我们能训到更小的损失。

2. GPU 针对大而宽的模型进行了优化

我们现在有了一个将性能和最佳模型尺寸与 neFLOs 联系起来的规则。然而,neFLOs 有点难以具象化。我们能否将其转化为更直观的指标,如训练时间?其实,无论你是有时间上的限制还是财务上的限制,主要关注的都是 GPU 时间。为了在 neFLOs 和 GPU 时间之间建立联系,我们在谷歌云平台上用 4 种不同 GPU 实例以及各种不同大小的 Transformer-XL 模型进行了数万次的基准测试 (包括混合精度训练测试)。以下是我们的发现:

速度估计

每秒 neFLOs (即公式中的
speed
) 可以建模为由模型宽度 (每层神经元数) 、深度 (层数) 和 batch size 三个因子组成的多变量函数,这三个因子的重要性递减。在我们的实验中,观察到的最大预测误差为测量值的 15%。

宽度

GPU 针对宽 transfomer 模型的大型前馈层进行了优化。在我们所有的实验中,每秒 neFLOs 与模型宽度
呈 1.6 次方的幂律关系
,这意味着两倍宽的模型需要 4 倍的操作。然而执行这些操作的速度也提高了大约 3.16 倍,
几乎抵消了额外的计算成本

深度

每秒 neFLOs 也与深度正相关。我们目前发现的最佳关系是每秒 neFLOs 与

成正比。这与 transformer 模型必须串行地处理每一层的事实是一致的。从本质上讲,
层数更多的模型其实并不会更快,但它们似乎表现出更快,其原因主要是它们的均摊开销更小
。公式中的
常数
就代表这一开销,在我们的实验中该常数一直在 5 左右,这其实意味着 GPU 加载数据、嵌入和 softmax 这些操作的耗时大约相当于 5 个 transfomer 层的时间。

Batch size

Batch size 发挥的作用最小。
Batch size 较小时,其与速度呈正相关关系,但这个关系很快就饱和了
(甚至在 V100 和 P100 上 batch size 大于 64 后、在 K80 和 P4 batch size 大于 16 后,速度比小 batch size 时还有所降低)。因此,我们将其对速度的贡献建模为对数函数以简化计算,它是 3 个因子中最弱的。因此,最终我们所有实验都是在单 GPU 上用 batch size 64 运行出来的。这是大模型的另一个好处:
因为更大的 batch size 似乎没有多大帮助,如果你的模型太大而无法塞进 GPU,你可以只使用较小的 batch size 以及梯度累积技术。

2 的幂在 2020 年仍然很重要!

最后,一个令人惊讶的收获是
宽度或 batch size 设置为 2 的幂的话其最终性能会比设为其他值高
。有或没有 Tensor Core 的 GPU 都是如此。在像 V100 这样的 Tensor Core GPU 上,NVIDIA 建议张量形状设置为 8 的倍数; 然而,我们试验过将其不断加倍至 512 的倍数,性能还会继续提高。但是,在最终拟合时我们还是只选择拟合 2 的幂的数据,因为拟合所有数据点意味着拟合质量会变差,而且最终的拟合结果会对采用 2 的幂情况下的速度估计得过于乐观。但这不妨碍你去选择最快的形状参数。

最终,我们得到运行速度的估算公式如下:

例如,在未使用混合精度的 V100 GPU 上,k=2.21 × 10^7、a=1.66、b=5.92、c=1.33。不同的 GPU 具有不同的乘性系数,但结果很接近。

3. 语言建模任务演示: Wikitext-103

现在我们已经知道了模型尺寸和训练速度之间的关系,我们可以依此预测: 对于给定的 GPU 时间或预算,适合目标任务的最佳模型尺寸及其能达到的性能。

体验地址: https://hf.co/calculator/
体验地址: https://hf.co/calculator/

这里使用的价格是 Google 云平台 (Google Cloud Platform,GCP) 的价格。我们使用了 Peter Henderson 的
Experiment impact tracker
来估算能源消耗,并使用了
Electricity map
的荷兰数据 (Google 的欧洲服务器所在地) 来估算 CO2 排放量。尽管巨大的训练成本常常博得头条,但事实上,我们仍然有可能以 30 美元的价格在中等规模的数据集上复现最先进的结果!对于一个恰当优化过的训练方案而言,V100 已经算一个强大的武器了。

图中所示的数据的测例为在 Wikitext-103 上使用 batch size 60 以及单 GPU 训练一个 Transformer-XL 模型,模型的目标长度 (target length) 和记忆长度 (memory length) 为 150,测试基于 CMU 的 Transformer-XL
代码库
。为了充分利用 V100 的 Tensor Core 功能,我们在该 GPU 上把 batch size 设为 64,序列长度设为 152。在我们的
模型尺寸 - 速度
预测公式中,我们假设内部前馈层维度与嵌入和注意力维度相同,并且宽深比是恒定的。
Reformer
表明,这种设置有利于节省内存。虽然
缩放定律
一文表明: 形状不会显著影响 GPT-2 的性能。然而,对于大模型而言,我们还是发现具有更大前馈层的更浅的模型的性能会更好,因此我们在图中给出了两种候选的模型形状: 一个宽而浅,一个窄而深。

为了复现中型 Transformer-XL 预训练模型 (损失为 3.15) 的结果,我们调整了原模型的大小以增加的前馈维度并使之为 2 的高次幂,同时保持相同参数量。我们最终得到了一个 14 层的模型,隐藏层维度为 768 且前馈层维度为 1024。相比之下,原文中的模型是通过激进的超参数搜索搜得的 16 层模型,形状也很奇怪,隐藏层维度为 410 且前馈层维度为 2100。我们的实验表明,由于我们的形状是 2 的高次方,并且是一个更浅、更宽的模型,因此它在 NVIDIA RTX Titan 上每 batch 的速度比原模型提高了 20%。对于该模型,CMU 团队提供的脚本已经非常接近最佳停止时间。最终,我们获得了相同的性能,同时减少了
25% 的训练时间
。最重要的是,原模型使用超参数搜索得到了对它而言更优形状,而我们什么也没调,甚至连随机种子也是直接复用的他们手调的随机种子。由于我们使用了较小规模的训练来拟合缩放定律,并依此缩放定律计算所需的模型超参,因此节省参数搜索实际上可能是我们获得的另一个也是更大的一个收益。

4. 要点

  • 大模型效率惊人!
  • 训练至收敛一点也不高效。
  • 运行小规模基准测试能够帮助我们预测生产级模型的模型性能和最佳停止时间。
  • 优化模型形状以提高速度以及使用尺寸大、停止早的模型可帮助降低训练成本。


英文原文:
https://hf.co/calculator/

原文作者: Teven Le Scao

译者: Matrix Yao (姚伟峰),英特尔深度学习工程师,工作方向为 transformer-family 模型在各模态数据上的应用及大规模模型的训练推理。

审校、排版: zhongdongy (阿东)

初识Node与内置模块

概述:了解Node.js,熟悉内置模块:fs模块、path模块、http模块

初识Node.js

浏览器中的JavaScript运行环境

运行环境是指代码正常运行所需的必要环境

  • 对于Chrome浏览器运行环境=V8引擎+内置API(DOM、BOM等)
  • V8引擎负责解析和执行Js代码
  • 内置API是由
    运行环境
    提供的特殊接口,
    只能运行在所属的运行环境中

JavaScript能否做后端开发?

也许你听到的后端开发语言大多是:Java、Python、PHP等,但其实Js也能做后端开发,虽然性能不如他们,但对于前端开发人员十分友好。

如果我们把Js代码运行浏览器中,就可以进行前端开发;
如果运行到Node.js中,就可以实现后端开发

什么是Node.js?

Node.js
是一个基于Chrome V8引擎的
JavaScript运行环境

Node.js官网:
http://nodejs.org/zh-cn/

Node.js中的JavaScript运行环境

对于Node.js运行环境=V8引擎+内置API

  • 浏览器是Js的前端运行环境
  • Node.js是Js的后端运行环境
  • Node.js中无法调用DOM和BOM等
    浏览器内置API

Node.js可以做什么?

很多框架和工具,都基于Node的基础功能,学会Node.js,可以“
帮助前端程序员胜任更多工作
”:

  • 基于Express框架,快速构建Web应用
  • 基于Electron框架,构建跨平台桌面应用
  • 基于restify框架,构建API接口项目
  • 读写和操作数据库、创建使用的命令行工具辅助开发等

如何走Node.js这条路?

JavaScript基础语法——Node.js内置API模块(fs、path、http等)——第三方API模块(express、koa、egg、mysql等)

Node.js环境安装

官网下载安装

区分LST版本和Current版本:

  • LST:长期稳定版,适于追求稳定性的企业级项目
  • Current:新特性尝鲜版,适于热衷于尝试新特性的用户

查看已安装的版本号

//打开终端(Terminal)
ndoe -v

在Node.js中执行JavaScript代码

打开终端

输入node要执行的Js文件路径

F:\xxx\xxx\Node.js>node index.js

终端中的快捷键:

  • 方向键↑:快速定位上一次命令
  • Tab:自动补全文件名
  • esc:快速清空已输入的命令
  • cls:清空终端内容

fs文件系统模块

什么是fs模块?

fs模块是Node.js官方提供的用来
操作文件
的模块,它提供了一系列的方法和属性,用来满足用户对文件的操作需求。

fs.readFile()方法,读取文件内容

fs.writeFile()方法,写入内容

const fs=require('fs')

读取指定文件中的内容

  • 参数1:文件的存放路径
  • 参数2:读取时采用的编码格式(可选)
  • 参数3:回调函数,得到失败和成功的结果
fs.readFile(path[,options],callback)
const fs = require('fs');

fs.readFile('./files/1.txt', 'utf8', function (err, dataStr) {
    console.log(err)
    console.log("------------------")
    console.log(dataStr)
})

向指定的文件中写入内容

  • 参数1:文件的存放路径
  • 参数2:写入的内容
  • 参数3:读取时采用的编码格式(可选)
  • 参数4:回调函数,得到失败和成功的结果
fs.writeFile(file,data[,options],callback)
const fs = require('fs');

fs.writeFile('./files/1.txt', 'Hello World!', function (err) {
    console.log(err)
})

路径动态拼接错误问题

在使用fs模块操作文件时,如果提供的操作路径是以./或../开头的相对路径,很容易出现路径动态拼接错误问题。

原因:代码运行时,会以node命令时所处的目录,动态拼接被操作文件的完整路径。

解决:直接提供完整的绝对路径(移植性差,不利于维护)。

fs.writeFile(__dirname+'/files/2.txt', newStr, function (err) {
    if (err) {
        return console.log("文件写入失败!+", err.message)
    }
    console.log("文件写入成功!")
})

path路径模块

什么是path模块?

path模块是Node.js官方提供的用来
处理路径
的模块,它提供了一系列的方法和属性,用来满足用户对路径的处理需求。

path.join()方法,将多个路径片段拼接成一个完整的路径字符串

path.basename()方法,将路径中的文件名解析出来

const path=require('path')

路径拼接

path.join([...paths])
const path = require('path')

const pathStr = path.join('/a', '/b/c', '../', '/d', 'e') //../抵消一层
console.log(pathStr)//\a\b\d\e

获取路径中的文件名

  • 参数1:文件的存放路径
  • 参数2:文件扩展名
path.basename(path[,ext])
const path = require('path')

const fpath = './files/1.txt'
const fullname = path.basename(fpath)
console.log(fullname)//1.txt

const nameWithoutExt = path.basename(fpath, '.txt')
console.log(nameWithoutExt)//1

获取路径中的文件扩展名

path.extname(path)

http模块

什么是http模块?

在网络节点中,负责消费资源的电脑,叫做客户端;负责对外提供网络资源的电脑,叫做服务器。

http模块是Node.js官方提供的用来
创建Web服务器的模块
的模块,通过提供的http.createServer()方法,就能把一台普通的电脑,变成一台Web服务器,从而对外提供Wbe资源。

const http=require('http')

服务器和普通电脑的区别在于:服务器上安装了Web服务器软件,例如IIS、Apache等

再Node.js中,我们不需要使用IIS、Apache等第三方web服务器软件。因为我们可以基于Node.js的http模块,通过几行代码,手写一个服务器软件,从而对外提供web服务。

创建最基本的web服务器

  1. 导入http模块
  2. 创建web服务器实例
  3. 为服务器实例绑定reques时间,监听客户端请求
  4. 启动服务器
const http = require('http')
const server = http.createServer()

server.on('request', function (req, res) {
    console.log("Someone visit our web server")
})

server.listen(8082, function () {
    console.log("Server running at http://127.0.0.1:8082")
})

req请求对象

访问与客户端相关的数据或属性

const http = require('http')
const server = http.createServer()

server.on('request', req=>{
    const url=req.url//客户端请求的url地址
    const method=req.method//客户端请求的method类型
})

server.listen(8082, function () {
    console.log("Server running at http://127.0.0.1:8082")
})

res响应对象

访问与服务器相关的数据或属性

const http = require('http')
const server = http.createServer()

server.on('request', (req, res) => {
    const url = req.url//客户端请求的url地址
    const method = req.method//客户端请求的method类型
    //调用res.end向客户端响应内容
    res.end("Hello World!")
})

server.listen(8082, function () {
    console.log("Server running at http://127.0.0.1:8082")
})

解决中文乱码问题

res.setHeader('Content-Type','text/html; charset=utf-8')

SQL注入是常见的系统安全问题之一,用户通过特定方式向系统发送SQL脚本,可直接自定义操作系统数据库,如果系统没有对SQL注入进行拦截,那么用户甚至可以直接对数据库进行增删改查等操作。

XSS全称为Cross Site Script跨站点脚本攻击,和SQL注入类似,都是通过特定方式向系统发送攻击脚本,对系统进行控制和侵害。SQL注入主要以攻击数据库来达到攻击系统的目的,而XSS则是以恶意执行前端脚本来攻击系统。

项目框架中使用mybatis/mybatis-plus数据持久层框架,在使用过程中,已有规避SQL注入的规则和使用方法。但是在实际开发过程中,由于各种原因,开发人员对持久层框架的掌握水平不同,有些特殊业务情况必须从前台传入SQL脚本。这时就需要对系统进行加固,防止特殊情况下引起的系统风险。

在微服务架构下,我们考虑如何实现SQL注入/XSS攻击拦截时,肯定不会在每个微服务都实现一遍SQL注入/XSS攻击拦截。根据我们微服务系统的设计,所有的请求都会经过Gateway网关,所以在实现时就可以参照前面的日志拦截器来实现。在接收到一个请求时,通过拦截器解析请求参数,判断是否有SQL注入/XSS攻击参数,如果有,那么返回异常即可。

我们前面在对微服务Gateway进行自定义扩展时,增加了Gateway插件功能。我们会根据系统需求开发各种Gateway功能扩展插件,并且可以根据系统配置文件来启用/禁用这些插件。下面我们就将防止SQL注入/XSS攻击拦截器作为一个Gateway插件来开发和配置。

1、新增SqlInjectionFilter 过滤器和XssInjectionFilter过滤器,分别用于解析请求参数并对参数进行判断是否存在SQL注入/XSS攻脚本。此处有公共判断方法,通过配置文件来读取请求的过滤配置,因为不是多有的请求都会引发SQL注入和XSS攻击,如果无差别的全部拦截和请求,那么势必影响到系统的性能。
  • 判断SQL注入的拦截器
/**
 * 防sql注入
 * @author GitEgg
 */
@Log4j2
@AllArgsConstructor
public class SqlInjectionFilter implements GlobalFilter, Ordered {

......

        // 当返回参数为true时,解析请求参数和返回参数
        if (shouldSqlInjection(exchange))
        {
            MultiValueMap<String, String> queryParams = request.getQueryParams();
            boolean chkRetGetParams = SqlInjectionRuleUtils.mapRequestSqlKeyWordsCheck(queryParams);
    
            boolean chkRetJson = false;
            boolean chkRetFormData = false;
            
            HttpHeaders headers = request.getHeaders();
            MediaType contentType = headers.getContentType();
            long length = headers.getContentLength();

            if(length > 0 && null != contentType && (contentType.includes(MediaType.APPLICATION_JSON)
                    ||contentType.includes(MediaType.APPLICATION_JSON_UTF8))){
                chkRetJson = SqlInjectionRuleUtils.jsonRequestSqlKeyWordsCheck(gatewayContext.getRequestBody());
            }
            
            if(length > 0 && null != contentType  && contentType.includes(MediaType.APPLICATION_FORM_URLENCODED)){
                log.debug("[RequestLogFilter](Request)FormData:{}",gatewayContext.getFormData());
                chkRetFormData = SqlInjectionRuleUtils.mapRequestSqlKeyWordsCheck(gatewayContext.getFormData());
            }
            
            if (chkRetGetParams || chkRetJson || chkRetFormData)
            {
                return WebfluxResponseUtils.responseWrite(exchange, "参数中不允许存在sql关键字");
            }
            return chain.filter(exchange);
        }
        else {
            return chain.filter(exchange);
        }
    }

......

}
  • 判断XSS攻击的拦截器
/**
 * 防xss注入
 * @author GitEgg
 */
@Log4j2
@AllArgsConstructor
public class XssInjectionFilter implements GlobalFilter, Ordered {

 ......

        // 当返回参数为true时,记录请求参数和返回参数
        if (shouldXssInjection(exchange))
        {
            MultiValueMap<String, String> queryParams = request.getQueryParams();
            boolean chkRetGetParams = XssInjectionRuleUtils.mapRequestSqlKeyWordsCheck(queryParams);
    
            boolean chkRetJson = false;
            boolean chkRetFormData = false;
            
            HttpHeaders headers = request.getHeaders();
            MediaType contentType = headers.getContentType();
            long length = headers.getContentLength();

            if(length > 0 && null != contentType && (contentType.includes(MediaType.APPLICATION_JSON)
                    ||contentType.includes(MediaType.APPLICATION_JSON_UTF8))){
                chkRetJson = XssInjectionRuleUtils.jsonRequestSqlKeyWordsCheck(gatewayContext.getRequestBody());
            }
            
            if(length > 0 && null != contentType  && contentType.includes(MediaType.APPLICATION_FORM_URLENCODED)){
                log.debug("[RequestLogFilter](Request)FormData:{}",gatewayContext.getFormData());
                chkRetFormData = XssInjectionRuleUtils.mapRequestSqlKeyWordsCheck(gatewayContext.getFormData());
            }
            
            if (chkRetGetParams || chkRetJson || chkRetFormData)
            {
                return WebfluxResponseUtils.responseWrite(exchange, "参数中不允许存在XSS注入关键字");
            }
            return chain.filter(exchange);
        }
        else {
            return chain.filter(exchange);
        }
    }

......

}
2、新增SqlInjectionRuleUtils工具类和XssInjectionRuleUtils工具类,通过正则表达式,用于判断参数是否属于SQL注入/XSS攻击脚本。
  • 通过正则表达式对参数进行是否有SQL注入风险的判断
/**
 * 防sql注入工具类
 * @author GitEgg
 */
@Slf4j
public class SqlInjectionRuleUtils {
    
    /**
     * SQL的正则表达式
     */
    private static String badStrReg = "\\b(and|or)\\b.{1,6}?(=|>|<|\\bin\\b|\\blike\\b)|\\/\\*.+?\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT|UPDATE.+?SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE).+?FROM|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";
    
    /**
     * SQL的正则表达式
     */
    private static Pattern sqlPattern = Pattern.compile(badStrReg, Pattern.CASE_INSENSITIVE);
    
    
    /**
     * sql注入校验 map
     *
     * @param map
     * @return
     */
    public static boolean mapRequestSqlKeyWordsCheck(MultiValueMap<String, String> map) {
        //对post请求参数值进行sql注入检验
        return map.entrySet().stream().parallel().anyMatch(entry -> {
            //这里需要将参数转换为小写来处理
            String lowerValue = Optional.ofNullable(entry.getValue())
                    .map(Object::toString)
                    .map(String::toLowerCase)
                    .orElse("");
            if (sqlPattern.matcher(lowerValue).find()) {
                log.error("参数[{}]中包含不允许sql的关键词", lowerValue);
                return true;
            }
            return false;
        });
    }
    
    
    /**
     *  sql注入校验 json
     *
     * @param value
     * @return
     */
    public static boolean jsonRequestSqlKeyWordsCheck(String value) {
        if (JSONUtil.isJsonObj(value)) {
            JSONObject json = JSONUtil.parseObj(value);
            Map<String, Object> map = json;
            //对post请求参数值进行sql注入检验
            return map.entrySet().stream().parallel().anyMatch(entry -> {
                //这里需要将参数转换为小写来处理
                String lowerValue = Optional.ofNullable(entry.getValue())
                        .map(Object::toString)
                        .map(String::toLowerCase)
                        .orElse("");
                if (sqlPattern.matcher(lowerValue).find()) {
                    log.error("参数[{}]中包含不允许sql的关键词", lowerValue);
                    return true;
                }
                return false;
            });
        } else {
            JSONArray json = JSONUtil.parseArray(value);
            List<Object> list = json;
            //对post请求参数值进行sql注入检验
            return list.stream().parallel().anyMatch(obj -> {
                //这里需要将参数转换为小写来处理
                String lowerValue = Optional.ofNullable(obj)
                        .map(Object::toString)
                        .map(String::toLowerCase)
                        .orElse("");
                if (sqlPattern.matcher(lowerValue).find()) {
                    log.error("参数[{}]中包含不允许sql的关键词", lowerValue);
                    return true;
                }
                return false;
            });
        }
    }
}

  • 通过正则表达式对参数进行是否有XSS攻击风险的判断
/**
 * XSS注入过滤工具类
 * @author GitEgg
 */
public class XssInjectionRuleUtils {
    
    private static final Pattern[] PATTERNS = {
            
            // Avoid anything in a <script> type of expression
            Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE),
            // Avoid anything in a src='...' type of expression
            Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            // Remove any lonesome </script> tag
            Pattern.compile("</script>", Pattern.CASE_INSENSITIVE),
            // Avoid anything in a <iframe> type of expression
            Pattern.compile("<iframe>(.*?)</iframe>", Pattern.CASE_INSENSITIVE),
            // Remove any lonesome <script ...> tag
            Pattern.compile("<script(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            // Remove any lonesome <img ...> tag
            Pattern.compile("<img(.*?)>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            // Avoid eval(...) expressions
            Pattern.compile("eval\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            // Avoid expression(...) expressions
            Pattern.compile("expression\\((.*?)\\)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
            // Avoid javascript:... expressions
            Pattern.compile("javascript:", Pattern.CASE_INSENSITIVE),
            // Avoid vbscript:... expressions
            Pattern.compile("vbscript:", Pattern.CASE_INSENSITIVE),
            // Avoid onload= expressions
            Pattern.compile("on(load|error|mouseover|submit|reset|focus|click)(.*?)=", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL)
    };
    
    public static String stripXSS(String value) {
        if (StringUtils.isEmpty(value)) {
            return value;
        }
        for (Pattern scriptPattern : PATTERNS) {
            value = scriptPattern.matcher(value).replaceAll("");
        }
        return value;
    }
    
    public static boolean hasStripXSS(String value) {
        if (!StringUtils.isEmpty(value)) {
            for (Pattern scriptPattern : PATTERNS) {
                if (scriptPattern.matcher(value).find() == true)
                {
                    return true;
                }
            }
        }
        return false;
    }
    
    /**
     * xss注入校验 map
     *
     * @param map
     * @return
     */
    public static boolean mapRequestSqlKeyWordsCheck(MultiValueMap<String, String> map) {
        //对post请求参数值进行sql注入检验
        return map.entrySet().stream().parallel().anyMatch(entry -> {
            //这里需要将参数转换为小写来处理
            String lowerValue = Optional.ofNullable(entry.getValue())
                    .map(Object::toString)
                    .map(String::toLowerCase)
                    .orElse("");
            if (hasStripXSS(lowerValue)) {
                return true;
            }
            return false;
        });
    }
    
    
    /**
     *  xss注入校验 json
     *
     * @param value
     * @return
     */
    public static boolean jsonRequestSqlKeyWordsCheck(String value) {
        if (JSONUtil.isJsonObj(value)) {
            JSONObject json = JSONUtil.parseObj(value);
            Map<String, Object> map = json;
            //对post请求参数值进行sql注入检验
            return map.entrySet().stream().parallel().anyMatch(entry -> {
                //这里需要将参数转换为小写来处理
                String lowerValue = Optional.ofNullable(entry.getValue())
                        .map(Object::toString)
                        .map(String::toLowerCase)
                        .orElse("");
                if (hasStripXSS(lowerValue)) {
                    return true;
                }
                return false;
            });
        } else {
            JSONArray json = JSONUtil.parseArray(value);
            List<Object> list = json;
            //对post请求参数值进行sql注入检验
            return list.stream().parallel().anyMatch(obj -> {
                //这里需要将参数转换为小写来处理
                String lowerValue = Optional.ofNullable(obj)
                        .map(Object::toString)
                        .map(String::toLowerCase)
                        .orElse("");
                if (hasStripXSS(lowerValue)) {
                    return true;
                }
                return false;
            });
        }
    }
    
}
3、在GatewayRequestContextFilter 中新增判断那些请求需要解析参数。因为出于性能等方面的考虑,网关并不是对所有的参数都进行解析,只有在需要记录日志、防止SQL注入/XSS攻击时才会进行解析。
    /**
     * check should read request data whether or not
     * @return boolean
     */
    private boolean shouldReadRequestData(ServerWebExchange exchange){
        if(gatewayPluginProperties.getLogRequest().getRequestLog()
                && GatewayLogTypeEnum.ALL.getType().equals(gatewayPluginProperties.getLogRequest().getLogType())){
            log.debug("[GatewayContext]Properties Set Read All Request Data");
            return true;
        }

        boolean serviceFlag = false;
        boolean pathFlag = false;
        boolean lbFlag = false;

        List<String> readRequestDataServiceIdList = gatewayPluginProperties.getLogRequest().getServiceIdList();

        List<String> readRequestDataPathList = gatewayPluginProperties.getLogRequest().getPathList();

        if(!CollectionUtils.isEmpty(readRequestDataPathList)
                && (GatewayLogTypeEnum.PATH.getType().equals(gatewayPluginProperties.getLogRequest().getLogType())
                    || GatewayLogTypeEnum.CONFIGURE.getType().equals(gatewayPluginProperties.getLogRequest().getLogType()))){
            String requestPath = exchange.getRequest().getPath().pathWithinApplication().value();
            for(String path : readRequestDataPathList){
                if(ANT_PATH_MATCHER.match(path,requestPath)){
                    log.debug("[GatewayContext]Properties Set Read Specific Request Data With Request Path:{},Math Pattern:{}", requestPath, path);
                    pathFlag =  true;
                    break;
                }
            }
        }

        Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
        URI routeUri = route.getUri();
        if(!"lb".equalsIgnoreCase(routeUri.getScheme())){
            lbFlag = true;
        }

        String routeServiceId = routeUri.getHost().toLowerCase();
        if(!CollectionUtils.isEmpty(readRequestDataServiceIdList)
                && (GatewayLogTypeEnum.SERVICE.getType().equals(gatewayPluginProperties.getLogRequest().getLogType())
                || GatewayLogTypeEnum.CONFIGURE.getType().equals(gatewayPluginProperties.getLogRequest().getLogType()))){
            if(readRequestDataServiceIdList.contains(routeServiceId)){
                log.debug("[GatewayContext]Properties Set Read Specific Request Data With ServiceId:{}",routeServiceId);
                serviceFlag =  true;
            }
        }

        if (GatewayLogTypeEnum.CONFIGURE.getType().equals(gatewayPluginProperties.getLogRequest().getLogType())
                && serviceFlag && pathFlag && !lbFlag)
        {
            return true;
        }
        else if (GatewayLogTypeEnum.SERVICE.getType().equals(gatewayPluginProperties.getLogRequest().getLogType())
                && serviceFlag && !lbFlag)
        {
            return true;
        }
        else if (GatewayLogTypeEnum.PATH.getType().equals(gatewayPluginProperties.getLogRequest().getLogType())
                && pathFlag)
        {
            return true;
        }

        return false;
    }
4、在GatewayPluginProperties中新增配置,用于读取Gateway过滤器的系统配置,判断开启新增的防止SQL注入/XSS攻击插件。
@Slf4j
@Getter
@Setter
@ToString
public class GatewayPluginProperties implements InitializingBean {

    public static final String GATEWAY_PLUGIN_PROPERTIES_PREFIX = "spring.cloud.gateway.plugin.config";
    
    public static final String GATEWAY_PLUGIN_PROPERTIES_PREFIX_LOG_REQUEST = GATEWAY_PLUGIN_PROPERTIES_PREFIX + ".logRequest";
    
    public static final String GATEWAY_PLUGIN_PROPERTIES_PREFIX_SQL_INJECTION = GATEWAY_PLUGIN_PROPERTIES_PREFIX + ".sqlInjection";
    
    public static final String GATEWAY_PLUGIN_PROPERTIES_PREFIX_XSS_INJECTION = GATEWAY_PLUGIN_PROPERTIES_PREFIX + ".xssInjection";
    
    /**
     * Enable Or Disable
     */
    private Boolean enable = false;
    
    /**
     * LogProperties
     */
    private LogProperties logRequest;
    
    /**
     * SqlInjectionProperties
     */
    private SqlInjectionProperties sqlInjection;
    
    /**
     * XssInjectionProperties
     */
    private XssInjectionProperties xssInjection;

    @Override
    public void afterPropertiesSet() {
        log.info("Gateway plugin logRequest enable:", logRequest.enable);
        log.info("Gateway plugin sqlInjection enable:", sqlInjection.enable);
        log.info("Gateway plugin xssInjection enable:", xssInjection.enable);
    }
    
    /**
     * 日志记录相关配置
     */
    @Getter
    @Setter
    @ToString
    public static class LogProperties implements InitializingBean{
    
        /**
         * Enable Or Disable Log Request Detail
         */
        private Boolean enable = false;
    
        /**
         * Enable Or Disable Read Request Data
         */
        private Boolean requestLog = false;
    
        /**
         * Enable Or Disable Read Response Data
         */
        private Boolean responseLog = false;
    
        /**
         * logType
         * all: 所有日志
         * configure:serviceId和pathList交集
         * serviceId: 只记录serviceId配置列表
         * pathList:只记录pathList配置列表
         */
        private String logType = "all";
    
        /**
         * Enable Read Request Data When use discover route by serviceId
         */
        private List<String> serviceIdList = Collections.emptyList();
    
        /**
         * Enable Read Request Data by specific path
         */
        private List<String> pathList = Collections.emptyList();
    
        @Override
        public void afterPropertiesSet() throws Exception {
            if(!CollectionUtils.isEmpty(serviceIdList)){
                serviceIdList = serviceIdList.stream().map(String::toLowerCase).collect(Collectors.toList());
            }
            if(!CollectionUtils.isEmpty(pathList)){
                pathList = pathList.stream().map(String::toLowerCase).collect(Collectors.toList());
            }
        }
    }
    
    /**
     * sql注入拦截相关配置
     */
    @Getter
    @Setter
    @ToString
    public static class SqlInjectionProperties implements InitializingBean{
        
        /**
         * Enable Or Disable
         */
        private Boolean enable = false;
        
        /**
         * Enable Read Request Data When use discover route by serviceId
         */
        private List<String> serviceIdList = Collections.emptyList();
        
        /**
         * Enable Read Request Data by specific path
         */
        private List<String> pathList = Collections.emptyList();
        
        @Override
        public void afterPropertiesSet() {
            if(!CollectionUtils.isEmpty(serviceIdList)){
                serviceIdList = serviceIdList.stream().map(String::toLowerCase).collect(Collectors.toList());
            }
            if(!CollectionUtils.isEmpty(pathList)){
                pathList = pathList.stream().map(String::toLowerCase).collect(Collectors.toList());
            }
        }
    }
    
    /**
     * xss注入拦截相关配置
     */
    @Getter
    @Setter
    @ToString
    public static class XssInjectionProperties implements InitializingBean{
        
        /**
         * Enable Or Disable
         */
        private Boolean enable = false;
        
        /**
         * Enable Read Request Data When use discover route by serviceId
         */
        private List<String> serviceIdList = Collections.emptyList();
        
        /**
         * Enable Read Request Data by specific path
         */
        private List<String> pathList = Collections.emptyList();
        
        @Override
        public void afterPropertiesSet() {
            if(!CollectionUtils.isEmpty(serviceIdList)){
                serviceIdList = serviceIdList.stream().map(String::toLowerCase).collect(Collectors.toList());
            }
            if(!CollectionUtils.isEmpty(pathList)){
                pathList = pathList.stream().map(String::toLowerCase).collect(Collectors.toList());
            }
        }
    }
}

5、GatewayPluginConfig 配置过滤器在启动时动态判断是否启用某些Gateway插件。在这里我们将新增的SQL注入拦截插件和XSS攻击拦截插件配置进来,可以根据配置文件动态判断是否启用SQL注入拦截插件和XSS攻击拦截插件。

@Slf4j
@Configuration
@ConditionalOnProperty(prefix = GatewayPluginProperties.GATEWAY_PLUGIN_PROPERTIES_PREFIX, value = { "enable"}, havingValue = "true")
public class GatewayPluginConfig {
    
    /**
     * Gateway插件是否生效
     * @return
     */
    @Bean
    @ConditionalOnMissingBean(GatewayPluginProperties.class)
    @ConfigurationProperties(GatewayPluginProperties.GATEWAY_PLUGIN_PROPERTIES_PREFIX)
    public GatewayPluginProperties gatewayPluginProperties(){
        return new GatewayPluginProperties();
    }
    
......
    
    /**
     * sql注入拦截插件
     * @return
     */
    @Bean
    @ConditionalOnMissingBean(SqlInjectionFilter.class)
    @ConditionalOnProperty(prefix = GatewayPluginProperties.GATEWAY_PLUGIN_PROPERTIES_PREFIX, value = { "sqlInjection.enable" },havingValue = "true")
    public SqlInjectionFilter sqlInjectionFilter(@Autowired GatewayPluginProperties gatewayPluginProperties){
        SqlInjectionFilter sqlInjectionFilter = new SqlInjectionFilter(gatewayPluginProperties);
        log.debug("Load SQL Injection Filter Config Bean");
        return sqlInjectionFilter;
    }

    /**
     * xss注入拦截插件
     * @return
     */
    @Bean
    @ConditionalOnMissingBean(XssInjectionFilter.class)
    @ConditionalOnProperty(prefix = GatewayPluginProperties.GATEWAY_PLUGIN_PROPERTIES_PREFIX, value = { "xssInjection.enable" },havingValue = "true")
    public XssInjectionFilter xssInjectionFilter(@Autowired GatewayPluginProperties gatewayPluginProperties){
        XssInjectionFilter xssInjectionFilter = new XssInjectionFilter(gatewayPluginProperties);
        log.debug("Load XSS Injection Filter Config Bean");
        return xssInjectionFilter;
    }
......
}

在日常开发过程中很多业务需求都不会从前端传入SQL脚本和XSS脚本,所以很多的开发框架都不去识别前端参数是否有安全风险,然后直接禁止掉所有前端传入的的脚本,这样就强制规避了SQL注入和XSS攻击的风险。但是在某些特殊业务情况下,尤其是传统行业系统,需要通过前端配置执行脚本,然后由业务系统去执行这些配置的脚本,这个时候就需要通过配置来进行识别和判断,然后对容易引起安全性风险的脚本进转换和配置,在保证业务正常运行的情况下完成脚本配置。

源码地址:

Gitee:
https://gitee.com/wmz1930/GitEgg

GitHub:
https://github.com/wmz1930/GitEgg

javaEE Web(Tomcat)深度理解 和 Servlet的本质

在这里插入图片描述

每博一文案

我所有的进步,只为更接近你。
上天没有给予人们公平的人生,有人拥有出奇的才能,便有人只能不辞辛苦的攀登阶梯,我默默地守望着一缕光芒,小心翼翼,如掬如束,可若长久凝望,一点会让自己,也受烫灼。
平凡的人生或是悲惨的际遇,可能还不是最糟糕的,我害怕的是,如果始终懦弱不堪,会让哪怕是一份小小的心意,也永远无法传达。
竭尽力量的小勇敢,小心愿,一点会被许多人笑话的吧,那么,会被一个人了解吗,逝水比喻时光荏苒,十年了,我一直在努力的改变自己,夕语,你呢,你变成什么样子了?
不经意抬头看到的躲闪目光,低头侧目时不断加速的心跳,还有慌张间来不及带走的心思,以及追寻一个背影时的迟疑步伐。
过去的千丝万缕才刚浮动显现,穿过遮掩长久的黑暗,未来我们再相见时,希望所有眼泪,都能消失无形吧,曾经那个丢掉记忆逃跑的男孩,再见了。
上一次离开,我是那样决绝,我以为自己不会再回来了,再次相见时,本以为时隔多年,命运会再次垂青于我,可是兜兜转转,像是上天给我开了一个玩笑,错的一直是我。
我渴望的爱情如同虚幻泡沫,我想要挽回的友情,也没有了机会,此时此刻,我已经失去了留下来的力量。
所有成熟的大人,都曾是受伤的小孩,那个小孩可能会长大,但永远不会离开,我一直都在你的世界,看着你一点点改变,只是你,常常遗忘过去的自己。
今后我所有的牵挂 所有的向往 都和你一样 
                                                —————— 《我是江小白》
                                                —————— 来源《https://www.bilibili.com/read/cv16491270》

@


说明:
该博客是在基于安装了
Tomcat
的前提下操作的。 详细安装 Tomcat 的步骤大家可以移步至:
(Tomcat 的安装以及其中配置环境原理的详细说明_tomcat要在什么环境下运行_ChinaRainbowSea的博客-CSDN博客

1. 实现一个最基本的web应用(这个web应用中没有java小程序)

我们首先编写一个关于静态的网页的页面:一个简单的 百度链接:如下

<!--index.html文件-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>实现一个最基本的web应用(这个web应用中没有java小程序) </title>
</head>
<body style="text-align:center">
    <h3>百度链接</h3>
    <a href="https://www.baidu.com/">进入百度</a>
</body>
</html>

具体步骤如下:

第一步:
找到我们安装
Tomcat
中的路径找到一个名为
webapps
的目录。

注意:
我们所有的 web的 app 都要放到该 webapps 目录下,(没有为什么,这是 Tomcat 服务器的要求,如果不放到这里,Tomcat 服务器是无法到你的应用的app,并运行的)。

如下图所示:

在这里插入图片描述


第二步:
在我们找到的这个
webapps
目录下创建一个子目录,名为
Mytest
的目录。

注意:
这个目录名 test 就是你这个运行 webapp的名字。

在这里插入图片描述

第三步:
将我们上面编写的 html 静态文件,添加到该 Mytest目录下。

在这里插入图片描述

第四步:
启动我们的
Tomcat
服务器,打开我们的浏览器在地址栏上输入:
http://127.0.0.1:8080/项目名/index.html
:具体如下:
http://127.0.0.1:8080/Mytest/index.html
,最后回车,就可以访问到我们编写的 index.html 这个静态网页了。

在这里插入图片描述

在这里插入图片描述


重点:

我们在浏览器上直接输入一个 URL,然后回车,这个操作和点击一个
超链接
是本质是一样的都是静态访问,既然都是一样的了。那我们完全可以使用超链接,访问我们在 Tomcat 服务器上编写的
index.html
资源文件了。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>访问我们部署到Tomcat中的index.html资源文件</title>
</head>
<body>
    <!--注意:我们目前前端上的路径都以“/”开始的,都是加项目名的。-->
    <a href="http://127.0.0.1:8080/Mytest/index.html">index.html</a>
</body>
</html>

在这里插入图片描述

在同一个部署到同一个项目下的,资源文件是可以相互访问的。例如:我们编写一个名为 index2.html 文件,通过 index.html 访问 index2.html 资源文件。

<!--index2.html 文件-->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>部署到Tomcat服务器中的index2.html资源文件</title>
</head>
<body style="text-align:center">
    <h1 >你好世界</h1>
    <!--部署到Tomcat同一个项目下的之间的访问,不需要添加:http://127.0.0.1:8080-->
    <!--注意:我们目前前端上的路径都以“/”开始的,都是加项目名的。-->
    <a href="/Mytest/index.html">访问我们部署到Tomcat中同一个项目中的的index.html资源文件</a>
</body>
</html>

同样放到我们安装 Tomcat 服务器的 webapps 中我们建立的项目 Mytest目录下

在这里插入图片描述

在这里插入图片描述

上述这种资源的访问:我们可以称之为是
静态资源
。所谓的静态资源就是,固定的,数据是不会更新的,从一开始就写死了的。

怎么能变成
动态资源
,显然是需要
连接数据库的

连接数据库需要用
JDBC
程序,也就是需要编写 Java程序连接数据库,数据库中右多少条记录的信息,页面会根据数据库的记录的信息,进行一个动态的显示。这种技术被称为
动态网页技术
。(动态网页技术并不是说页面中有
flash
动画。动态网页技术是说页面中的数据是动态的,是根据数据库中存储的数据进行一个时时动态更新变化的 )。这个操作在本博客的后半部分有详细说明。

2. 一个动态 Web 页面中所包含的 “角色” 和 “协议”

如下是一个简易的 Web 通信图

在这里插入图片描述

2.1 角色

根据上图分析:在
整个BS结构的系统当中
有哪些人参与进去了

  • 浏览器软件的开发团队
    :(浏览器软件太多了:谷歌浏览器,火狐浏览器,IE 浏览器...)
  • Web Serve的开发团队
    : Web Server 这个软件也是太多了:Tomcat,Jetty,WebLogic ,JBOSS,WebSphere...
  • DB Server的开发团队:
    DB Server 也是不少的:Oracle,MySQL...
  • Web app的开发团队:
    Web 应该程序是我们作为
    Java web 程序员
    开发的。

2.2 协议

协议:
所谓的协议就是一种规范,大家都要遵守的规范。不过不遵守规范的话。相互之间是无法通信交流的。就不如说:两个人之间交流:一个人说英语,一个人说中文,这两个人之间使用的语言不同,也就是遵守的规范不同,两个人就无法交流了。各自都听不明白。如果两个人都遵守同一个规范,都是说中文,或者英文,相互之间就可以明白各自在说什么了,相互交流了。

同理:
浏览器前端 与 服务器后端之间想要相互交流数据信息,也是要遵守规范。服务器后端 与 数据库也是一样的。

在这里插入图片描述

  • Brower(浏览器) 与 WebServer
    之间有一套传输协议:
    HTTP
    (超文本传输协议)

  • Web Server 与 webapp
    之间有一套规范:
    JavaEE规范比如 Servlet规范



  • Webapp 和 DB Server
    之间有一套规范:JDBC 规范,关于 JDBC 的详细内容,大家可以移步至: