一般情况下,当NaiveUI和TailwindCSS被同时引入到一个项目中时,会出现一些组件样式异常的问题,本篇文章用于记录如何解决NaiveUI和TailwindCSS样式冲突的问题。
首先用pnpm+vite创建一个空的vue项目
pnpm create vite
如果你没有安装pnpm和vite,可以通过这行命令安装:
npm i -g pnpm vite
引入NaiveUI组件库
pnpm add -D naive-ui vfonts
引入TailwindCSS
pnpm add -D tailwindcss postcss autoprefixernpx tailwindcss init -p
执行上面两个命令后,根目录下会出现:tailwind.config.cjs
这个文件,将这个文件中的内容修改如下:
/** @type {import('tailwindcss').Config} */module.exports = { content: [ "./index.html", "./src/**/*.{vue,js,ts,jsx,tsx}", ], theme: { extend: {}, }, plugins: [],}
将CSS引入到src/style.css
中
@tailwind base;@tailwind components;@tailwind utilities;
然后在App.vue
放点NButton
先试试效果:
<script setup lang="ts">import {NButton} from "naive-ui";</script><template> <n-button>Default</n-button> <n-button type="tertiary"> Tertiary </n-button> <n-button type="primary"> Primary </n-button> <n-button type="info"> Info </n-button> <n-button type="success"> Success </n-button> <n-button type="warning"> Warning </n-button> <n-button type="error"> Error </n-button></template><style scoped></style>
效果如下:
可以看到这里的按钮样式出现了问题
将src/main.ts
改成下面这样,原理是让naive-ui-style
在 app 挂载之前动态的插入 meta 标签,防止TailwindCSS造成样式覆盖:
import { createApp } from 'vue'import './style.css'import App from './App.vue'async function bootstrap() { const app = createApp(App) const meta = document.createElement('meta') meta.name = 'naive-ui-style' document.head.appendChild(meta) app.mount('#app')}bootstrap()
效果如图:
参考文章:NaiveUI文档-潜在的样式冲突
]]>Conda是一个非常方便的开源包管理系统,可以用于在各种操作系统上安装管理软件包和环境。
在我们玩Python时,经常会遇到的问题Python版本不兼容的问题,而venv
这样的工具,在不折腾的情况下,只能创建和当前宿主机Python环境版本号相同的Python环境,而利用Conda
,我们就可以随意创建和切换虚拟Python环境了。
关于Anaconda
和Conda
之间的关系:
Anaconda是一个用于数据科学、机器学习和高性能计算的开源发行版,它基于Python语言,提供了大量常用的数据科学、机器学习和高性能计算的工具和库。Conda是Anaconda的包管理器,它主要用于管理Anaconda中的程序包和环境。所以Anaconda是Conda的发行版,Conda是Anaconda的包管理器。
我们应该选择安装哪个?
如果你是科学计算、数据分析或机器学习方面的开发人员,那么可能需要安装Anaconda,因为它预装了大量的数据科学工具和库。而如果你只是想使用Conda作为包管理器和环境管理器,那么您可以安装Miniconda,它只包含Conda和必要的依赖项。
即Anaconda
是Conda
的发行版,而且内置了Conda
,因此推荐直接一步到位安装Anaconda
。
Anaconda
:官方网站
首先点击中间的Download
按钮下载安装包
安装过程很简单,直接无脑下一步就行了。
安装完毕后,打开一个新的终端窗口,输入conda --version
命令,能正常显示版本号即代表安装成功
可以直接执行官方给的安装脚本:
进入发行版下载页面,找到Linux安装相关的链接,复制这个链接。
在机器上执行Shell脚本即可开始安装
bash <(curl -s <安装脚本链接>)# 例如bash <(curl -s https://repo.anaconda.com/archive/Anaconda3-2022.10-Linux-x86_64.sh)
一些常用的conda命令包括:
通过使用这些命令,您可以在不影响其他环境的情况下安装、更新和管理软件包。
在配置环境前,如果想知道有哪些Python3环境是可用的,那么可用使用下面这个命令来查询
conda search "^python=3"
我们可以使用Conda
在同一个系统中,创建多个不同的Python环境。比如说,我想创建一个名为py37
的Python3.7环境还有一个名为py311
的Python3.11环境,那我们就可以用下面这个命令创建
conda create --name py37 python=3.7conda create --name py311 python=3.11
第一行命令创建了一个名为py37
的Python环境,该环境的Python版本为:3.7
第二行命令创建了一个名为py311
的Python环境,该环境的Python版本为:3.11
创建后,可以使用以下命令切换环境:
conda activate <env-name># 例如,切换到名为py37的Python环境conda activate py37
如果我们需要退出Conda
的虚拟环境,可以使用下面这个命令来退出:
conda deactivate
如果我们需要彻底删除一个Conda
虚拟环境,可以用下面这个命令删除(假设要删除名为py311
的虚拟环境):
conda remove --name py311 --all
看到这里,可能会有同学想:Conda
可以像Pyvenv
那样,将每个项目的虚拟环境的包放到当前项目下的vnev
目录下吗?
其实是可以的,conda可以像pyvenv那样将每个项目的虚拟环境的包放到当前项目下的venv目录下。
可以使用以下命令创建一个名为myenv的虚拟环境,并将其安装在当前项目的venv目录下:
conda create --prefix ./venv/myenv python=3.7
然后你可以使用以下命令激活该虚拟环境:
conda activate ./venv/myenv
Conda
环境配置的导出与导入这里演示如何将项目的anacoda信息写到项目中(例如python版本、项目所需依赖),从而方便项目的其他协作者配置完全相同的环境
首先,使用conda env export
命令,将当前的Conda
环境导出到environment.yml
文件中
conda env export > environment.yml
导出后,当我们(或同学)在一个新的机器上配置环境时就方便多了,直接使用下面这个命令来创建环境
conda env create -f environment.yml
注:如果你导出的
Conda
环境配置文件带prefix
配置,即Conda
环境指定了文件路径。它在重新配置时也许不会生效。此时你可以在创建环境的命令后面加上--prefix
,例如:conda env create -f environment.yml --prefix ./venv/py38
为什么我安装Anaconda后,终端前面都会默认带上
(base)
? 如何取消?
相关问题:StackOverflow
解决方案:
使用下面这个命令,将自动激活base
环境的功能取消掉
conda config --set auto_activate_base false
同理,哪一天想开启的话,将它设置会true
即可
使用NodeJS中发送邮件是很常见的需求,本篇文章记录一下如何在NodeJS中使用时下流行的NodeMailer库发送邮件。
官方文档:https://nodemailer.com/about/
请根据自己所使用的包管理工具来安装依赖包。
# NPMnpm install nodemailer# Yarnyarn add nodemailer
以下为官方示例代码(其中require
动态导入被我改成了import
静态导入)
这并不会真的发送邮件,只能用来测试邮件效果,在测试环境中也许很有用。邮件“发送”成功后控制台会输出一个网址,你可以在网页中预览这个邮件
//导入邮件依赖包import * as nodemailer from 'nodemailer';// async..await is not allowed in global scope, must use a wrapperasync function main() { // Generate test SMTP service account from ethereal.email // Only needed if you don't have a real mail account for testing let testAccount = await nodemailer.createTestAccount(); // create reusable transporter object using the default SMTP transport let transporter = nodemailer.createTransport({ host: "smtp.ethereal.email", port: 587, secure: false, // true for 465, false for other ports auth: { user: testAccount.user, // generated ethereal user pass: testAccount.pass, // generated ethereal password }, }); // send mail with defined transport object let info = await transporter.sendMail({ from: '"Fred Foo 👻" <foo@example.com>', // sender address to: "bar@example.com, baz@example.com", // list of receivers subject: "Hello ✔", // Subject line text: "Hello world?", // plain text body html: "<b>Hello world?</b>", // html body }); console.log("Message sent: %s", info.messageId); // Message sent: <b658f8ca-6296-ccf4-8306-87d57a0b4321@example.com> // Preview only available when sending through an Ethereal account console.log("Preview URL: %s", nodemailer.getTestMessageUrl(info)); // Preview URL: https://ethereal.email/message/WaQKMgKddxQDoou...}main().catch(console.error);
用真实的163邮箱的SMTP账号发送个邮件试试
import * as nodemailer from "nodemailer";export async function mailTest() { let transporter = nodemailer.createTransport({ pool: true, // 启用连接池,保持与邮件服务器的连接 host: "smtp.163.com", //邮件服务器SMTP地址 port: 465, //服务器SMTP端口 secure: true, // 465时true,其它端口填false auth: { user: "yours@163.com",//SMTP账号 pass: "password"//SMTP密钥 } }); // send mail with defined transport object let info = await transporter.sendMail({ from: "\"Node邮件测试\" <yours@163.com>", // sender address to: "a@icloud.com, b@icloud.com", // list of receivers subject: "NodeJS Mail Test ✔", // Subject line text: "Hello Node Mail", // plain text body html: "<b>Hello Node Mail</b>" // html body //这里也可换成text: "xxx" 用来发送简单的文字 }); console.log("邮件发送信息:",info);}
使用import * as xxx from xxx
来实现包的静态导入。
关于import xxx
和import * as xxx
的区别:https://stackoverflow.com/questions/31386631/difference-between-import-x-and-import-as-x-in-node-js-es6-babel
//导入邮件依赖包import * as nodemailer from 'nodemailer';
使用nodemailer
的createTransport()
方法来创建一个transporter
对象,后面可以使用这个对象来发送邮件
let transporter = nodemailer.createTransport({ pool: true, // 启用连接池,保持与邮件服务器的连接 host: "smtp.163.com", //邮件服务器SMTP地址 port: 465, //服务器SMTP端口 secure: true, // 465时true,其它端口填false auth: { user: "yours@163.com",//SMTP账号 pass: "password"//SMTP密钥 } });
transporter
的sendMail()
是一个异步方法,可以使用await
来开始同步等待:
// send mail with defined transport object let info = await transporter.sendMail({ from: "\"Node邮件测试\" <yours@163.com>", // sender address to: "a@icloud.com, b@icloud.com", // list of receivers subject: "NodeJS Mail Test ✔", // Subject line text: "Hello Node Mail", // plain text body html: "<b>Hello Node Mail</b>" // html body //这里也可换成text: "xxx" 用来发送简单的文本 });//打印发送信息console.log(info);/*{ accepted: [ 'xxx@icloud.com' ], rejected: [], envelopeTime: 75, messageTime: 86, messageSize: 644, response: '250 Mail OK queued as zwqz-smtp-mta-g6-1,xxxxxxxxxxxxxx-xxxxxxxxxx', envelope: { from: 'xxxxxx@163.com', to: [ 'xxxxxxx@icloud.com' ] }, messageId: '<xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx@163.com>'}*/
也可也使用.then
异步执行这个方法:
// 异步发送邮件 transporter.sendMail({ from: "\"Node邮件测试\" <l59y063@163.com>", // sender address to: "isolate_wall_0w@icloud.com", // list of receivers subject: "NodeJS Mail Test ✔", // Subject line text: "Hello Node Mail", // plain text body html: "<b>Hello Node Mail</b>" // html body }).then((info) => { console.log(info); })
具体同步还是异步视自己的需求而定。
更多详细信息请查阅:官方文档
更多相关教程:W3Schools-NodeJS-Email
]]>像React这样开发的大前端Web应用有一个通病,就是首屏加载时间较长,而且在加载过程中用户看到的都是白屏,此时如果你的服务器或者用户自己的带宽过小的话可能会让用户以为这个网站挂掉了,从而导致流量损失。下图是Twitter网站的加载页面,像Twitter这样弄一个Loading页面或者图片也许就能在页面加载缓慢时留住用户。
本文用来记录一下如何实现在大前端JS加载前呈现一个Loading页面。
实现方法很简单,直接在项目根目录的index.html
中编写加载页面就行了。
建议在id
为root
的根节点中编写加载页面
因为在根节点中编写的内容,会在React前端JS加载完毕并开始渲染时被自动清除,正好省去了手动清理Loading
的麻烦。
比如我把root
div
改成下面这样:
<div id="root"> <div id="loading"> <img src="https://img.hash070.top/i/63b2693d4f9c3.webp" width="72px" alt="logo"> <p style="text-align: center">Loading</p> </div> </div>
那么React项目首屏加载时的效果就是这样的:
React开始渲染时确实也会被自动移除,感觉还不错。
参考链接:https://stackoverflow.com/questions/40987309/react-display-loading-screen-while-dom-is-rendering
刚刚的只是放了一个图片,也许有同学觉得还是差点意思,那么也许我们可以整一个CSS动画,让Loading更好看一点?
首先把css
写到头部的style
块中:
<style> .loader { border: 6px solid #e4f2fb; border-radius: 50%; border-top: 6px solid #1d9aee; width: 32px; height: 32px; -webkit-animation: spin 0.4s linear infinite; /* Safari */ animation: spin 0.4s linear infinite; } /* Safari */ @-webkit-keyframes spin { 0% { -webkit-transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); } } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style>
然后在根div
里这样写
<div id="root"><div id="loading"> <img src="https://img.hash070.top/i/63b2693d4f9c3.webp" style="margin:0 auto;display: block" width="72px" alt="logo"> <div style="margin:20px auto" class="loader"></div> </div></div>
实现效果:
更多现成的CSS Loader:
]]>本文记录如何在Linux上配置和部署Node环境。
在Linux上部署NodeJS环境共有三种方法:
apt
、yum
(不推荐)nvm
是一个强大的开源Node管理工具,能帮助你快速安装部署和管理多个Node环境,使用较为简单,因此我推荐用这种方式部署。
官方仓库:https://github.com/nvm-sh/nvm
要安装nvm
只需在命令行中输入这两行命令:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bashsource ~/.bashrc# 检查版本号,如果有输入则表示安装成功nvm -v
安装完nvm
后就可以用nvm
安装NodeJS
了
可以用这个命令安装最新的Node
和NPM
# 安装最新的Nodenvm install node
也可以像这样指定版本号
nvm install 18.12.1
注:安装的第一个Node会成为默认Node
nvm ls
可以用nvm use
参数方便地切换node环境(如果你用nvm安装了多个node的话
nvm use 18.12.1
更多命令请查阅官方文档
在官网手动下载最新的Linux二进制,并解压与创建软链接,我第一次也是用这样的方法安装的,但是不知为何用npm i -g
安装的全局包不能在命令行中使用,因此无奈改用nvm
。
安装方法如下:
# 下载文件wget https://nodejs.org/dist/v18.12.1/node-v18.12.1-linux-x64.tar.xz# 解压并删除压缩包tar xf node-v18.12.1-linux-x64.tar.xz && rm node-v18.12.1-linux-x64.tar.xz# 创建文件夹mkdir -p /develop && mv node-v18.12.1-linux-x64 /develop/# 创建软链接ln -s /develop/node-v18.12.1-linux-x64/bin/npm /usr/local/bin/ln -s /develop/node-v18.12.1-linux-x64/bin/node /usr/local/bin/# 查看版本hroot@ub20:~# npm -v8.19.2root@ub20:~# node -vv18.12.1
如果你也遇到了同样的问题,可以像这样取消安装:
# 取消软链接unlink /develop/node-v18.12.1-linux-x64/bin/npmunlink /develop/node-v18.12.1-linux-x64/bin/node# 删除文件rm -rf /develop/node-v18.12.1-linux-x64
使用apt/yum
安装的node版本过低,基本没法用。
# Debian系sudo apt install nodejs# 红帽系sudo yum install nodejs
]]>
在Ubuntu系统中,do-release-upgrade
命令可以实现升级系统版本的功能,(这个命令我每次ssh登录到服务器时都会看到。。。
Welcome to Ubuntu 20.04.5 LTS (GNU/Linux 5.4.0-28-generic x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantageNew release '22.04.1 LTS' available.Run 'do-release-upgrade' to upgrade to it.
本来我是不大喜欢Ubuntu 22.04
的,因为我几乎每次我用apt
安装依赖时都会问我要重启哪些服务,光这点就让我掉不少好感。这次升级是因为我不小心执行autoremove
命令把给系统整炸了(我怎么也想不到这玩意破坏力居然这么大),因此想着也许可以用升级系统的方法补救一下。
本次我演示的是从Ubuntu 20.04
更新到Ubuntu 22.04
。
数据无价,执行这种操作前一定要备份一下数据,(虽然我觉得既然官方给的这种升级方式应该是不会轻易翻车的。
我的家里云
里的数据虽然都有经常备份,但要是升级把服务器升炸了的话手动恢复起来还是比较麻烦的,而快照还原起来就方便多了。
我觉得更新系统前应该让系统上的软件包是最新的(直觉
apt update&&apt upgrade -y
执行这个命令
do-release-upgrade
当我忐忑地在控制台敲下这个命令并按下回车后,直接给我返回了个这个:
root@ubuntu:~# do-release-upgrade-bash: do-release-upgrade: command not found
我。。。
do-release-upgrade
命令的问题解决方法:
sudo apt updatesudo apt install update-manager-core
相关问题链接:https://askubuntu.com/questions/1318223/do-release-upgrade-command-failing
继续更新
再次在命令行中敲下这个命令,终于开始正常工作了:
在开始升级之前,系统会告诉你当前正在通过SSH操作,并表示不推荐这样做,因为当系统升级失败后会难以恢复,最后问是否继续。
我当然是yes了,反正有快照,有VNC,啥都不怕。
下一个提示:第三方源将会被禁用,你可以在升级后再次启用
最后一次升级前的确认:
然后就会开始下载一堆东西。
在安装和配置新系统时,会时不时像下面这样问你关于新配置文件覆盖的问题,如果你不确定该怎么做,我认为你应该选择默认的N
,即保留原先配置,除非你确定覆盖某软件包的原配置文件没什么问题。
在所有软件包升级完成后会告诉你有90个软件包将被移除,我肯定是无脑YES了。(逃
最后系统升级完成,需要重启
重启后再次登录,好像没什么问题,升级很顺利。
进入系统执行一下apt更新命令,会告诉你这些Python包已经不需要了,可以用xxx命令移除
这时你可不要听了它的鬼话!千万不要手贱在命令行执行apt autoremove
命令,特别是当它跟你提到和Python有关的东西时,不要问我是怎么知道的,这玩意可能会把你系统里几乎所有的软件包都给扬了
react-i18next
是一个强大的基于i18next的React / React Native国际化框架。
使用react-i18next
可以轻松满足前端工程的国际化需求,本文记录如何在React项目中使用react-i18next
来实现前端国际化。
# NPM 命令npm i react-i18next i18next# Yarn 命令yarn add react-i18next i18next
在src/locales
下创建一个cn.json
和en.json
文件,分别表示项目的中文字符和英文字符。
例如:
cn.sjon
{ "title": "你好世界"}
en.json
{ "title": "Hello World"}
在当前文件夹下创建一个资源文件
resourse.ts
import en from './en.json';import cn from './cn.json';// import locale json files from src/localesexport const resources = { "en": { "translation": en }, "zh": { "translation": cn }}
再创建一个初始化i18n
设定的代码
import i18n from 'i18next';import { initReactI18next } from 'react-i18next';import { resources } from './resources';i18n // 将 i18n 实例传递给 react-i18next .use(initReactI18next) // 初始化 i18next // 所有配置选项: https://www.i18next.com/overview/configuration-options .init({ resources, fallbackLng: "en-US", lng: "en-US", debug: true, // interpolation: { // escapeValue: false, // not needed for react as it escapes by default // } });export default i18n;
import './locales/i18n'
import './App.css'import {useTranslation} from "react-i18next";//引入react-i18next的useTranslation钩子函数function App() { //创建函数别名 const {t} = useTranslation(); return ( <div className="App"> <h1>{t('title')}</h1> </div> )}export default App
注:
此时我的项目src
目录结构如下:
.../src$ tree.├── App.css├── App.tsx├── assets│ └── react.svg├── index.css├── locales│ ├── en-US.json│ ├── i18n.ts│ ├── resources.ts│ └── zh-CN.json├── main.tsx└── vite-env.d.ts2 directories, 10 files
完成多语言支持后,你会发现在浏览器开发工具
=>Sensors
=>Location
=>Locale
中更改语言后,没有像预期的那样自动调整输出语言。
这里我们需要使用另一个能检测浏览器的语言的依赖来解决这个问题。
i18next-browser-languagedetector
# NPM 安装npm i i18next-browser-languagedetector# Yarn 安装yarn add i18next-browser-languagedetector
新增导入LanguageDetector
插件,然后将该插件引入到i18n
中。
再把init()
函数中的lng
选项改为navigator.language
,这个值来自于浏览器的Locales
,表示浏览器当前正在使用的语言。
i18n.ts
import i18n from 'i18next';import { initReactI18next } from 'react-i18next';import LanguageDetector from 'i18next-browser-languagedetector';import { resources } from './resources';i18n // 将 i18n 实例传递给 react-i18next .use(initReactI18next) // 引入languagedetector语言检测器 .use(LanguageDetector) // 初始化 i18next // 所有配置选项: https://www.i18next.com/overview/configuration-options .init({ resources, fallbackLng: "en-US", lng: navigator.language, debug: true, // interpolation: { // escapeValue: false, // not needed for react as it escapes by default // } });export default i18n;
前端不仅应该可以自动识别并显示语言,也应该给用户手动选择语言的选项。
在代码中实现的方法:
// 在类式组件中const { t, i18n } = this.props;i18n.changeLanguage("en"); // 手动切换到英文// 在函数式组件中使用const { t, i18n } = useTranslation();i18n.changeLanguage("zh"); // 手动切换到中文
中文:
英文:
]]>Win11在连接到远程桌面时,无法保存密码,提示 “Windows Defender Credential Guard不允许使用已保存的凭据”。
计算机配置
-> 管理模板
-> 系统
-> Device Guard
,将其设为已禁用
。git tag
Git 打标签命令
-a <annotate>
Tag的名称-m <msg>
Tag的的详情描述/注释-s
签名Tag-d
删除Tag# 最简单的Tag创建方法## 创建一个名为`v1.0`的Taggit tag v1.0## 创建一个名为`v1.0`的Tag,并写上该Tag的注释git tag -a v1.0 -m "Did something"## 创建一个名为`v1.0`的Tag,写上注释并签名git tag -s v1.0 -m "Fix Some Bugs"
查看当前仓库的所有Tag
git tag
查看某个Tag的信息:
git show v1.0
# 删除名为`v1.0`的Taggit tag -d v1.0
# 推送本地的`v1.0`Tag到远程仓库origingit push origin v1.0
# 删除远程仓库origin中的v1.0Taggit push -d origin v1.0
]]>
Flameshot是一款优秀的开源截图工具,能够完美替代Ubuntu原装的截图工具,轻松实现截图编辑功能。
本片文章用于记录一下在Ubuntu桌面版如何安装Flameshot以及之后如何手动配置一下截图快捷键。
项目地址:https://github.com/flameshot-org/flameshot
安装方法有三种,从以下方法中任选一种都行:
sudo snap install flameshot
sudo apt install flameshot
安装完之后想用一个快捷键(比如设置按下Print键后就开始截屏操作)
快捷键是在Ubuntu的系统设置里面弄的,在Keyboard
->Keyobard Shrotcuts
->Cutsom Shrotcuts
添加一个自定义快捷键就好了。
要注意的是,这里的Command
填写时最好使用which flameshot
命令确认一下flameshot的位置,例如我就是用Snap安装的,所以安装位置是这里,如果你使用了其他方法安装,则FlamesShot的位置可能和我的不一样。
其实这里直接填写 flameshot gui
应该也是可以的,我这里填二进制文件的绝对路径可能是因为CronTab
用多了然后习惯性地填入了绝对路径(CronTab
默认不认环境变量)
SMTP是常见的邮件发送协议,我们可以很方便地使用SMTP协议将邮件快速安全地发送出去,之前在研究如何使用Gmali的SMTP服务在WordPress发邮件时,搜到好多文章说要到Google Developers Console
开启GmailAPI服务之类的,简直离谱,因为想要使用Gmail的SMTP,只需要在自己的账户上设置一个“应用程序密码”就ok了,本文就来记录一下如何设置。
一、打开谷歌账号安全设置
谷歌账号网址: https://myaccount.google.com/security
二、点击安全性
菜单后,进入应用专用密码
三、生成邮件应用专用密码
如下图所示,第一栏的应用类型选邮件
,应用设备随意选,不影响使用。
点击生成
按钮后,系统会将新生成的密钥展示一次,通过密钥可以用来使用Gmail邮件的SMTP、IMAP之类的服务了,请将它保存在安全的地方。
实测该密钥可以通过IMAP和SMTP协议与Gmail服务器通信,某邮件客户端的测试成功截图:
如果您通过 SSL 或 TLS 连接网络,则可以使用 smtp.gmail.com 作为服务器,向组织内外的任何人员发送邮件。
选用此选项时,您必须通过 Gmail 或 Google Workspace 帐号和密码进行身份验证。
通过 OAuth,应用和设备无需您的用户名或密码就能访问您的 Google 帐号数据。不过,较旧款的扫描仪或打印机可能不支持 OAuth。在这种情况下,请先执行以下步骤,再设置设备:
发送上限 | 每天 2,000 封邮件。 有关详情,请参阅电子邮件发送上限 |
---|---|
反垃圾邮件过滤器 | 可疑的电子邮件可能会被过滤或拒绝。 |
SMTP 服务的完全限定域名 | smtp.gmail.com |
配置选项 | 端口:465(需要 SSL) 端口:587(需要 TLS)允许动态 IP 有关详情,请参阅 SSL 连接概览 |
身份验证要求 | 必须提供完整的 Gmail 或 Google Workspace 电子邮件地址(<您的用户名>@solarmora.com)才能进行身份验证。 |
在设备或应用中,输入 smtp.gmail.com 作为服务器地址。
在 端口 字段中,输入以下数字之一:
如要进行身份验证,请输入完整的 Google Workspace 或 Gmail 地址(例如:<您的用户名>@solarmora.com))和密码。在设备或应用中使用帐号前,请务必先登录该帐号。
BBR是谷歌研发的TCP阻塞控制算法,它主要致力于:在有一定丢包率的网络链路上充分利用带宽和降低网络链路上的 buffer 占用率,从而降低延迟。总之,开启BBR能够大幅提升服务器的TCP吞吐率,而且从Linux Kernel 4.9 开始已经默认编译了 TCP BBR 模块,由于Ubuntu 20.04 默认的内核就是 5.4 版本的内核,并,所以可以直接通过几行命令开启BBR,本篇文章用于记录一下如何开启BBR。
将下面这一块代码全复制到终端里就OK了。
# 修改系统变量echo net.core.default_qdisc=fq >> /etc/sysctl.confecho net.ipv4.tcp_congestion_control=bbr >> /etc/sysctl.conf# 使修改生效sysctl -p# 测试sysctl net.ipv4.tcp_available_congestion_control
如果执行完上述命令后,输出如下,则表示BBR开启成功了。
net.ipv4.tcp_available_congestion_control = reno cubic bbr
如果还不放心的话还可以使用lsmod
命令查看BBR是否真的载入了系统中。
Linux lsmod(英文全拼:list modules)命令用于显示已载入系统的模块。 执行lsmod 指令,会列出所有已载入系统的模块。
lsmod | grep bbr
如果上述命令有找到tcp_bbr
,则证明BBR阻塞控制算法模块确实已成功载入系统:
tcp_bbr 20480 83
]]>
注解是放在Java源码的类、方法、字段、参数前的一种特殊“注释”
注释会被编译器直接忽略,注解则可以被编译器打包进入class文件,因此,注解是一种用作标注的“元数据”。
从JVM的角度看,注解本身对代码逻辑没有任何影响,如何使用注解完全由工具决定。
可能有的同学在这里就有点迷惑了,那SpringBoot中的自动装配注解(@AutoWired
),REST接口注解是怎么生效的呢?稍安勿躁,往下面看看就知道了。
Java的注解可以分为三类:
SOURCE
)第一类是由编译器使用的注解,例如:
@Override
:让编译器检查该方法是否正确地实现了覆写;@SuppressWarnings
:告诉编译器忽略此处代码产生的警告。这类注解不会被编译进入.class
文件,它们在编译后就被编译器扔掉了。
.class
文件时使用的注解(CLASS
)第二类是由工具处理.class
文件使用的注解,比如有些工具会在加载class的时候,对class做动态修改,实现一些特殊的功能。这类注解会被编译进入.class
文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。
RUNTIME
)第三类是在程序运行期能够读取的注解,它们在加载后一直存在于JVM中,这也是最常用的注解。例如,一个配置了@PostConstruct
的方法会在调用构造方法后自动被调用(这是Java代码读取该注解实现的功能,JVM并不会识别该注解)。
Java使用@interface
语法来定义注解,它的格式如下:
public @interface Exam { int full() default 100; String subject() default "English"; String difficulty() default "hard";}
注解的参数类似于无参数方法,建议使用default设置一个默认值。
有一些注解可以修饰其他注解,这些注解被称之为元注解(Meta Annotation
)。
在Java标准库中已经定义了一些元注解,通常情况下,我们只需要使用元注解,而不需要自己手动编写元注解。
@Target
是最常用的元注解。使用@Target
可以定义这个注解(Annotation
)能够被应用于代码的哪些位置。
ElementType.TYPE
ElementType.FIELD
ElementType.METHOD
ElementType.CONSTRUCTOR
ElementType.PARAMETER
例如,我们要规定:注解@Exam
可用在代码中的方法(Method
)上。那么我们可以通过添加一个@Target(ElementType.METHOD)
到@Exam
注解上:
@Target(ElementType.METHOD)//规定这个注解可以用在代码中的方法上public @interface Exam { int full() default 100; String subject() default "English"; String difficulty() default "hard";}
那么如果我们想让上面的这个注解@Exam
既能用在方法(method
)上,又能用在字段(field
)上,那么我们可以把@Target注解参数变成数组:
@Target({ ElementType.METHOD, ElementType.FIELD})public @interface Exam { int full() default 100; String subject() default "English"; String difficulty() default "hard";}
ps:实际上@Target
定义的value
是ElementType[]
数组,只有一个元素时,可以省略数组的写法。
另一个重要的元注解@Retention
定义了Annotation
的生命周期:
RetentionPolicy.SOURCE
,在编译时即被丢弃RetentionPolicy.CLASS
,仅保存在Class文件中,不加载到JVM中RetentionPolicy.RUNTIME
,会被加载入JVM,并在运行时被程序读取如果@Retention
不存在,则该Annotation
默认为CLASS
。因为通常我们自定义的Annotation
都是RUNTIME
,所以,务必要加上@Retention(RetentionPolicy.RUNTIME)
这个元注解:
@Retention(RetentionPolicy.RUNTIME)public @interface Exam { int full() default 100; String subject() default "English"; String difficulty() default "hard";}
使用@Repeatable
这个元注解可以定义Annotation
是否可重复。这个注解应用不是特别广泛。
使用@Inherited
定义子类是否可继承父类定义的Annotation
。@Inherited
仅针对@Target(ElementType.TYPE)
类型的annotation
有效,并且仅针对class
的继承,对interface
的继承无效:
即如果一个类使用了一个带@Inherited
的注解,那么它的子类也将继承使用这个注解。
public @interface Exam { }
public @interface Exam { int full() default 100; String subject() default "English"; String difficulty() default "hard";}
@Target({ ElementType.METHOD, ElementType.FIELD})@Retention(RetentionPolicy.RUNTIME)public @interface Exam { int full() default 100; String subject() default "English"; String difficulty() default "hard";}
Java的注解本身对代码逻辑没有任何影响。根据@Retention
的配置:
SOURCE
类型的注解在编译期就被丢掉了;CLASS
类型的注解仅保存在class文件中,它们不会被加载进JVM;RUNTIME
类型的注解会被加载进JVM,并且在运行期可以被程序读取。如何使用注解完全由工具决定。SOURCE
类型的注解主要由编译器使用,因此我们一般只使用,不编写。
CLASS
类型的注解主要由底层工具库使用,涉及到class的加载,一般我们很少用到。
只有RUNTIME
类型的注解不但要使用,还经常需要编写。
所以一般情况下我们只需要学习RUNTIME
类型的注解就OK了。
因为注解定义后也是一种类(Class
),所有的注解都继承自java.lang.annotation.Annotation
,因此,读取注解,需要使用反射API。
Java提供的使用反射API读取Annotation
的方法包括:
Class
、Field
、Method
或Constructor
:Class.isAnnotationPresent(Class)
Field.isAnnotationPresent(Class)
Method.isAnnotationPresent(Class)
Constructor.isAnnotationPresent(Class)
例如:
// 判断@Report是否存在于Person类:Person.class.isAnnotationPresent(Report.class);
Class.getAnnotation(Class)
Field.getAnnotation(Class)
Method.getAnnotation(Class)
Constructor.getAnnotation(Class)
例如:
// 获取Person定义的@Report注解:Report report = Person.class.getAnnotation(Report.class);int type = report.type();String level = report.level();
使用反射API读取Annotation
有两种方法。方法一是先判断Annotation
是否存在,如果存在,就直接读取:
Class cls = Person.class;if (cls.isAnnotationPresent(Report.class)) { Report report = cls.getAnnotation(Report.class); ...}
第二种方法是直接读取Annotation
,如果Annotation
不存在,将返回null
:
Class cls = Person.class;Report report = cls.getAnnotation(Report.class);if (report != null) { ...}
读取方法、字段和构造方法的Annotation
和Class类似。但要读取方法参数的Annotation
就比较麻烦一点,因为方法参数本身可以看成一个数组,而每个参数又可以定义多个注解,所以,一次获取方法参数的所有注解就必须用一个二维数组来表示。例如,对于以下方法定义的注解:
public void hello(@NotNull @Range(max=5) String name, @NotNull String prefix) { }
要读取方法参数的注解,我们先用反射获取Method
实例,然后读取方法参数的所有注解:
// 获取Method实例:Method m = ...// 获取所有参数的Annotation:Annotation[][] annos = m.getParameterAnnotations();// 第一个参数(索引为0)的所有Annotation:Annotation[] annosOfName = annos[0];for (Annotation anno : annosOfName) { if (anno instanceof Range) { // @Range注解 Range r = (Range) anno; } if (anno instanceof NotNull) { // @NotNull注解 NotNull n = (NotNull) anno; }}
注解如何使用,完全由程序自己决定。例如,JUnit是一个测试框架,它会自动运行所有标记为@Test
的方法。
我们来看一个@Range
注解,我们希望用它来定义一个String
字段的规则:字段长度满足@Range
的参数定义:
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.FIELD)public @interface Range { int min() default 0; int max() default 255;}
在某个JavaBean中,我们可以使用该注解:
public class Person { @Range(min=1, max=20) public String name; @Range(max=10) public String city;}
但是,定义了注解,本身对程序逻辑没有任何影响。我们必须自己编写代码来使用注解。这里,我们编写一个Person
实例的检查方法,它可以检查Person
实例的String
字段长度是否满足@Range
的定义:
void check(Person person) throws IllegalArgumentException, ReflectiveOperationException { // 遍历所有Field(字段): for (Field field : person.getClass().getFields()) { // 获取所有Field(字段)中定义的@Range注解: Range range = field.getAnnotation(Range.class); // 如果@Range存在于Field字段中: if (range != null) { // 获取Field的值: Object value = field.get(person); // 如果值是String: if (value instanceof String) { String s = (String) value; // 判断值是否满足@Range的min/max: if (s.length() < range.min() || s.length() > range.max()) { throw new IllegalArgumentException("Invalid field: " + field.getName()); } } } }}
这样一来,我们通过@Range
注解,配合check()
方法,就可以完成Person
实例的检查。注意检查逻辑完全是我们自己编写的,JVM不会自动给注解添加任何额外的逻辑。
参考文章:廖雪峰官方网站-Java注解
]]>首先在Android Studio界面,双击Shift键盘,会出现一个搜索框。
在搜索框中输入VM options
,点击如下图所示的搜索结果。
在里面加上这一行配置
-Dfile.encoding=utf-8
然后重启Android Studio就OK了,可以看到中文已经能正确显示出来了。
附:
修改项目Gradle编码为UTF-8的方法:
在build.gradle
中加上下面几行配置。
compileJava.options.encoding = 'UTF-8'tasks.withType(JavaCompile) { options.encoding = 'UTF-8'}
]]>
在笔者写这篇文章时,距离Win11首发已经过去一年了,现在我正在用的主力操作系统就是Win11,开发和娱乐兼顾,方便的Android子系统和高性能的HyperV虚拟机,再加上Linux子系统,生产力确实爆表,仍在观望的小伙伴可以上车了(学校的机房里的新电脑都用上Win11了)。
但Win11的二级右键菜单真的是让人很烦,虽然我第一时间就用StartAllBack
这个软件改回去了,但最近把系统更新到Windows11 22H2
后这个软件直接就失效了,提示要升级。还好在网上找到了比较新的v3.5.5
版本,所以就打算写篇博客备份一下,顺便分享给大家。
软件已默认激活,但有条件的同学请支持正版。
先放下载链接:
下载并安装后点击左侧菜单栏的资源窗口栏目,勾选恢复经典样式的右键菜单就OK了。
效果如下:
这个软件还有许多其他强大的功能,比如自定义开始菜单栏样式,设置任务栏是否合并等,但我这里就不一一介绍了,感兴趣的同学就自己慢慢摸索吧。
]]>draw.io
是使用JavaScript编写的浏览器端的在线画图白板程序,使用体验还行,本文记录一下如何自托管一个draw.io
实例。
GitHub项目地址:https://github.com/jgraph/drawio
前置条件:安装Docker运行环境
安装方法:https://www.hash070.top/archives/docker-and-docker-compose-install.html
docker run -dit --rm --name="draw" -p 127.0.0.1:30088:8080 fjudith/draw.io# 另一个docker镜像docker run -dit --rm --name="draw" -p 127.0.0.1:30088:8080 jgraph/drawiofjudith/draw.io
我的反代配置文件如下
location / { proxy_pass http://127.0.0.1:30088; proxy_set_header Host $http_host;# proxy_http_version 1.1;# proxy_set_header Upgrade $http_upgrade;# proxy_set_header Connection "upgrade"; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Nginx-Proxy true; proxy_redirect off; }
不会用Nginx的话可以去看我的Nginx博客:https://www.hash070.top/archives/nginx-conf-note.html
]]>MySQL是时下流行的数据库,开发项目时非常常用,而数据库的安装一般比较麻烦,这时我们可以借助Docker和DockerCompose快捷地跑起来一个MySQL实例。
进行以下操作的前提条件是安装了Docker和DockerCompose
在Windows上安装Docker特别简单,只需进入Docker官网,下载并安装即可。
创建一个全新的文件夹,在里面新建一个名为docker-compose.yaml
的配置文件,如下图所示。
version: '3.7'services: mariadb: image: mariadb # mariadb是MySQL的一个分支,完全兼容MySQL,性能高于MySQL,推荐使用 restart: always environment: MARIADB_ROOT_PASSWORD: P@ssW*rd #这里的意思是: 设置默认root密码为 P@ssW*rd 建议根据自身需要修改。 container_name: mysql-test #定义容器名称 volumes: - "./mysql:/var/lib/mysql" - "./backup:/backup" ports: - "23306:3306" #对外暴露23306,防止和已有的MySQL冲突 #构建phpmyadmin服务 phpmyadmin: image: phpmyadmin restart: always environment: #PMA_ARBITRARY: 1#是否允许连接到任意服务器,设置为1是将会询问要连接的MySQL服务器地址 PMA_HOST: mysql-test #设置所连接的MySQL服务器名称 PMA_PORT: 3306 #设置所要连接的MySQL服务器端口 container_name: phpmyadmin depends_on: - mysql - web ports: - "28888:80" depends_on: - mariadb
在编写完成配置文件之后,进入终端(CMD或者PowerShell都行)并进入到docker-compose.yaml
所在目录,一行命令就可以启动了。
在浏览器中输入地址http://127.0.0.1:28888
,用户名root
,密码是P@ssW*rd
,进入就可以以Web网页的方式管理数据库了
数据库的端口是23306
,所以跑起来的数据库容器实例地址为127.0.0.1:23306
,进行到这一步就算大功告成了,下一步就可以对接程序进行调试了。
所以现在打算把笔记整理到博客上,但在这之前想分享一下我常用的笔记工具。
VSCode
VSCode
作为一个轻量级的编辑器,它的优势在于它强大的的插件社区,可以通过插件来扩展它的功能,比如Markdown
的预览、LaTeX
的预览、代码高亮、代码补全等等,这些功能都可以通过插件来实现,而且VSCode
的插件市场里面有很多优秀的插件,可以满足我们的需求。
大一刚开始的时候就用过VSCode
了,当时是想用它来当C语言的IDE,但它毕竟还只是一个轻量文本编辑器,靠插件确实能实现各种语言的支持,但是如果你只是一个小白,开发环境配不好的话用起来是很难受的,推荐使用IDEA
或者Visual Studio
,就不要折腾这玩意了。
这里我就分享一下我使用VSCode
的一些技巧和插件吧。
VSCode的设置同步功能非常好用,它不仅能同步你的VSCode设置,还能同步你的插件和插件的设置,如果你偶尔重装系统,或者有多台电脑的话,你就知道这个功能有多香了。
开启的方法很简单,点击左下角的设置按钮,登上你的GitHub账号就OK了。(由于GitHub已被屏蔽,需要使用魔法)
Markdown All in One
让VSCode支持MarkDown编辑语法和快捷键,编写MarkDown的必备拓展插件
MarkDown Preview Enhanced
编写完MarkDown怎么预览这些插件呢?除了使用像Typora这种专门的MarkDown软件,我们还可以使用MarkDown Preview Enhanced
这款插件来实现在VSCode内的实时预览。
就像这样
Markdown Paste
MarkDown是一个文本标记语言,可以引用图片但是本身不能存储图片,那么如何在MarkDown中方便地粘贴图片呢?
这个需求可以使用Markdown Paste
来解决。
有了这个插件,当我们使用Snipaste这样的截图软件截取图片到剪切板后,使用快捷键Ctrl + Alt + V
即可快速地将图片保存到电脑上,并将其该图片引用到MarkDown中。
Tips:
设置图片保存到相对路径可能有助于管理和备份图片。
例如我是这样设置的
这样的话,所有图片都将保存在相对于你正在编辑的MarkDown文本的img
文件夹下。
Typora有多好用和简洁相信不用我说了,配合图床+PicGo简直绝配,可惜正式版收费了,而且价格不是很美丽,学生党还是觉得有点贵了。但大家有条件还是推荐支持一下正版。
开发人员常常使用Gist记录他们的代码片段,但是Gist不仅仅是为极客和码农开发的,每个人都可以用到它。如果您听说过类似Pastebin或者Pastie这样的web应用的话,那您就可以看到它们和Gist很像,但Gist比它们要更优雅。因为这些免费应用一般含有广告,而且带有很多其他杂七杂八的功能。
Fleet是JetBrains公司推出的下一代IDE,它是一个新的分布式多语言编辑器和 IDE,采用了全新的用户界面和分布式架构从头开始构建,速度和性能确实比VSCode更好,笔者认为软件的设计和使用体验还算可以,多一个竞争产品我们就多一个选择,喜欢尝鲜的小伙伴们可以尝试一下。
]]>res/values/themes.xml
文件下配置Style(可选)这里我们可以定义一个引用了相关字体的Style,把字体的引用规则封装到这个Style中,到时候直接在布局文件里面调用就行了。
<style name="Regu_Noto"> <item name="android:fontFamily">@font/noto_regu</item> </style> <style name="Regu_Noto_Thin"> <item name="android:fontFamily">@font/noto_thin</item> </style>
使用style="@style/[Style名称]"
来调用Style
<TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="64dp" android:text="@string/welcome_msg" android:textSize="20dp" android:textColor="@color/black" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.498" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/logo_icon" style="@style/Regu_Noto_Thin"/>
也可以使用android:fontFamily="@font/[字体文件名称]"
的方法来调用
<TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="64dp" android:text="@string/welcome_msg" android:textSize="20dp" android:textColor="@color/black" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.498" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/logo_icon" android:fontFamily="@font/noto_thin"/>
除了直接在布局文件中直接指定字体,我们还可以在代码中动态设置字体。
第一步,把字体文件放到项目的资源目录下。
本次练习中,我将字体文件放到src/main/assets
目录下。注意Android studio
项目默认没有assets
目录,可以通过右键点击main目录,然后选择New
-> Folder
-> Assets Folder
新建assets
目录。
新建的assets目录和java,res目录在同一级。
第二步,在代码中动态设置:
TextView textView = (TextView) view.findViewById(R.id.fragment_text);Typeface type = Typeface.createFromAsset(view.getContext().getAssets(), "NotoSansHans-Regular.otf");textView.setTypeface(type);
如上,textView
即为我们要更改字体的文本,NotoSansHans-Regular.otf
是已经放好的字体文件。
参考文章:
https://blog.csdn.net/u011656025/article/details/112802591
]]>Nginx作为一款轻量高性能开源的Web服务器,深受大众欢迎并在服务器中得到了广泛应用。但是Nginx默认是不设防的,即不会自动防御DDoS攻击和CC攻击,因此攻击者可以轻易地发送大量的请求从而耗尽你的服务器资源、恶意盗刷你的服务器流量或者让你的后端服务器崩溃,因此配置Nginx基础防御是很有必要的,本篇文章就记录如何为你的Nginx配置一些基础的防御规则从而保护你的Web服务器。
相关文章链接:
示例配置代码:
#将这行代码放到Nginx配置文件的HTTP块中limit_conn_zone $binary_remote_addr zone=ip_addr:10m;
上面的一行代码的意思是以二进制格式标记客户端的 IP 地址,共享内存区域的名称是ip_addr
,区域大小是 10 兆字节。
然后在要应用该限制的Server块部分写入下列代码:
limit_conn ip_addr [单IP最大可接受TCP连接数];#例如,规定单个IP最多与服务器建立一个TCP连接,超出的连接将返回503limit_conn ip_addr 1;#实测如果你的网站是个普通的博客之类的静态为主的网站,不是什么下载站、视频站、音乐站,没有搞聊天通信什么需要使用TCP长连接的话,限制设置为1是不会影响正常用户访问的。#单条TCP连接的上传与下载速率限制#下载最高1000kb/sproxy_download_rate 1000k;#上传最高50kb/sproxy_upload_rate 500k;
建议根据实际情况来设置限制,写后记得好好测试一下再将配置上线,否则可能导致正常用户访问出现异常。
官方文档:https://docs.nginx.com/nginx/admin-guide/security-controls/controlling-access-proxied-tcp/
示例配置代码:
#将这行代码放到Nginx配置文件的HTTP块中limit_req_zone $binary_remote_addr zone=peripreq:10m rate=3r/s;
上面的一行代码的意思是:
limit_req_zone
:定义请求限制区域$binary_remote_addr
:以二进制的方式标记客户端IPzone=peripreq:10m
:本共享内存区域的名称是peripreq
,区域大小是 10 兆字节,它的大小与队列的最大长度有关。rate=3r/s
:定义单个IP最大可接受的请求速率为3次每秒(可自行更改这时我们就有一个限制区域
(limit_req_zone
)了,只需在需要应用该限制区域
的Server块
中引用这个区域名称就行。
引用方法:在要应用该限制的Server块部分写入如下代码
limit_req zone=peripreq burst=30 nodelay;
意思是:
limit_req
:请求数限制zone=peripreq
:区域规则名称为:peripreq
burst=30
:最大可接受突发请求数为30nodelay
:要求立即返回结果重点说明一下burst
和nodelay
关于burst
的突发请求数,如果一个IP超过了限制区域原本的限制(比如上面设置的是3r/s),那么就会开始使用突发请求数的配额,如果请求数超过了基础限制速率+突发请求数,则会将该请求放入队列或则直接返回503。
突发请求数会以基础限制速率的速度恢复。
关于nodelay
参数,这个参数决定了是否将超出的请求放到队列中处理,队列满了才会返回503。加了这个参数的话就是要求立即返回结果,不会将超出范围的请求放入队列中等待,而是立即返回503。
温馨提示:请求数!=用户看的网页数,正常打开一个网页可能会发送几十个请求,请使用网页调试工具查看打开一次自己的网站页面会发送多少个请求,请根据自己网站的实际情况调整并测试。
示例参数大小:
以菜鸟教程和知乎为例,打开一次它们的首页共发送了大约一百多次请求,那么rate=3r/s
,burst=200
是比较河里的。
这里可能有小伙伴会问:3r/s远远小于200,这难道不是很容易就超出限制了?
那是因为很多资源第一次请求后就缓存下来了,不会再向服务器索取了,因此只有第一次打开会发送这么多的请求数,用适当的突发设置请求应付一下就OK了。
上面的那些手段对于DoS
和CC
攻击可能有点用,但是如果遇到DDoS
就用处不是很大,对于这种分布式的攻击,我们应该首先考虑一下对后端的保护,防止服务器后端因同时收到太多请求而直接崩溃导致数据损坏等情况的发生。
示例配置代码:
#将这行代码放到Nginx配置文件的HTTP块中#下面的这个是针对站点本身的总请求次数限制,用于对后端的保护,防止DDoS攻击直接把后端给爆破了limit_req_zone $server_name zone=perserverreq:10m rate=20r/s;
上面的一行代码的意思是:
limit_req_zone
:定义请求限制区域$server_name
:以网站名为单位构建限制区域zone=peripreq:10m
:本共享内存区域的名称是perserverreq
,区域大小是 10 兆字节,它的大小与队列的最大长度有关。rate=20r/s
:定义单个站点最大可接受的请求速率为20次每秒(可自行更改然后同样在Server
部分引用该限制区域,再设置个突发请求限制以灵活应对突发请求
#允许最大突发50次服务器请求limit_req zone=perserverreq burst=100;
这里的20和100只是参考,这两个参数需要根据自己的服务器后端的最大并发处理能力来决定,
以上的那些手段只是配置了一下Nginx的并发限制,达到阈值后Nginx只是会给相应的IP返回错误,并不会拿那些恶意IP怎么样,光做到这些还是不太够的,至少要能把这些恶意IP拉黑掉。
为了实现拉黑功能可以借助一些WAF模块如:ModSecurity
、http_guard
、ngx_lua_waf
关于它们的使用方法大家可以上网上搜索一下,这里笔者来介绍一下如何使用Fail2Ban
来实现IP自动拉黑。
首先你需要在相应Server块定义网站错误日志,例如在server块中加上
error_log /www/wwwlogs/error.log;
Fail2Ban
需要读取这个日志文件以判断封禁IP
关于Fail2Ban
的配置和相关的用法(包括Nginx自动拉黑)笔者已经在这个文章中写过了,可以去看一下我的这篇文章:https://www.hash070.top/archives/fail2ban.html
这里就再写一下如何清空日志吧,我想到的方法很简单:使用echo命令用一个空格覆盖你的日志文件就OK了。例如:
echo " " > /root/nginx/logs/error.log
想要定时清理的话,借助crontab可以轻松实现,它的用法可以看我的这篇文章:https://www.hash070.top/archives/linux-crontab.html
]]>