Node.js(二)

748 阅读51分钟

Express

基本用法

中文官网:www.expressjs.com.cn/

基于 Node.js 平台,快速、开放、极简的 web 开发框架。Express 是 Node.js 最受欢迎的 Web 应用框架之一。它提供了简洁而灵活的方式来构建 Web 服务器和 API。Express 提供了一组强大的工具和功能,使得开发 Web 应用变得简单而高效。

安装

npm install express --save
npm i log4js

防盗链

获取referer

const referer = req.get('referer')
console.log(referer)
next()
//referer如果是直接打开的资源是获取不到的

代码

import express from 'express'
const app = express()
const whiteList = ['localhost','127.0.0.1']
cons preventHotLingkinng = (req,res,next)=>{
  const referer = req.get('referer')
if(referer){
  const {hostname}= new URL(referer)
  if(!whiteList.includes(hostname)){
    res.status(403).send('禁止访问'return
  }
}
next()
}

响应头和请求头

响应头 (http response headers)一般是以键值对的形式出现(key value)

实时更新js文件

npm install nodemon -g

同源策略:协议不同 端口不通 域名不同 浏览器就会拒绝该请求

预检请求 options 满足条件:

  1. Content-type application/json
  2. 或者是自定义请求头
  3. 非普通请求 patch put delete

webSocket 可以进行实时通讯 前端给后端实时发信息,后端给前端也可以实时发信息

Mysql

安装

Mysql是一种开源的关系型数据库 管理系统

官网:www.mysql.com/

  1. oracle 优秀商用数据库
  2. MongoDB 非关系型数据库
  3. sqLite 嵌入式数据库
  4. Mysqlg 关系型数据库

安装mysql并配置环境资源

image.png

此时则说明安装成功

sql语句

创建数据库

  1. 连接数据库
mysql -uroot -p

image.png

  1. 查看已有的库
show databases;

image.png

系统内置的4个数据库

information_schema

mysql

performance_schema

sys

  1. 创建自己的数据库
CEATE DATABASE IF NOT EXISTS 'xiaoli' 

//IF NOT EXISTS  如果这个数据库不存在就创建

  • 创建数据库的时候设置字符集DEFAULT CHARRACTER SET ='utf8mb4'

创建结果如下

image.png

创建数据表

CREATE TABLE 'user'(
  id INT NOT NULL AUTO_INCREMENT  PRIMARY KEY,
  name VARCHAR(100) COMMENT'名字'
  age INT COMMENT'年龄'
  address VARCHAR(200) COMMENT'地址'
  create_time TIMESHAMP DEFAULT_TIMESTAMP COMMENT'创建时间'
  
)COMMENT'用户表'
  • id 字段的名称
  • NOT NULL 不能为空
  • AUTO_INCREMENT 字段自增
  • PRIMARY KEY 把这个字段设置为主键
  • TIMESHAMP 代表时间初
  • DEFAULT_TIMESTAMP 默认时间
  • COMMENT'xxx ' 添加注释

image.png

查看里面数据

select * from user;

image.png

查询

  1. 查询单个列

SELECT [列名id] FROM 'user'[表名]

SELECT id FROM 'user' 
  1. 查询多个列

SELECT [列名id] FROM 'user'[表名]

SELECT id,name FROM 'user' 
  1. 查询所有列

SELECT * FROM 'user'

4.列的别名

SELECT id as user_id FROM 'user'

  1. 排序

SELECT * FROM user ORDER BY id DESC

SELECT * FROM user  ORDER BY [列名id] DESC/ASC;
//ASC 升序
//DESC 降序
  1. 限制查询结果

SELECT * FROM user LIMIT 开始行,数量

SELECT * FROM user LIMIT 04
//从0开始查到第4行

image.png

  1. 条件查询

SELECT * FROM user WHERE [类名 name] = [值 'iu']

SELECT * FROM user  WHERE  name =  'iu'

image.png

  1. 联合查询

SELECT * FROM user WHERE [类名 name] = [值 'iu']

SELECT * FROM user  WHERE  name =  'iu' AND age <=26;

满足一个条件的

SELECT * FROM user  WHERE  name =  'iu' OR age <=26;
  1. 模糊查询

将以德结尾的人查出来

SELECT * FROM user WHERE name = [模糊匹配LIKE] '%德'

  • _德 :表示模糊一个字符

  • __德:表示模糊两个字符

新增一个

INSERT INTO user('name','age','hobby') VALUES('德哥',27,'唱歌');

新增多个,逗号隔开即可

INSERT INTO user('name','age','hobby') VALUES ('德哥',27,'唱歌'),('iu',27,'唱歌')

UPDATE [表名'user'] SET name(列名) key=value(值)

需要知道更新到哪一条 WHERE id = 2

UPDATE 'user' SET name = 'iu',age=20,hobby=music WHERE id = 2

DELETE FROM 'user' WHERE id = 7;

//批量删除
DELETE FROM 'user' WHERE id IN7,10,11);

表达式

  1. 算术表达式:查出20以上的
SELECT age FROM 'user' WHERE age >=20
  1. 字符截取
SELECT LEFT('name',1) FROM 'user'

3.生成随机数

SELECT RAND() FROM 'user'

子查询

SELECT * FROM 'table' WHERE user_id = 1

连表查询

把两个表数据合并到一起

  1. 内连接
SELECT * FROM 'table' WHERE 'user_id' ='table' .'user_id'
  1. 外连接

    • 左连接
    • 右连接

LEFT JOIN [表名] ON [连接的条件]

左边的为驱动表 以驱动表为主

//左连接
SELECT * FROM 'user' LEFT JOIN [table] ON [user.id] = 'table' .'user_id'

//右连接
SELECT * FROM 'user' RIGHT JOIN [table] ON [user.id] = 'table' .'user_id'

knex

Knex是一个JavaScript ORM (Object-Relational Mapping) 库,它提供了一种轻量级的方式来操作MySQL等关系型数据库

官网:SQL Query Builder for Javascript | Knex.js (knexjs.org)

安装

npm install knex

knex代码直接写没有效果 必须要有.then接收

knex('student')
.insert({name:'小强',age:20})
//对应的SQL
insert into student ("name","age") values ("小强",20)

knex('student')
.where('name','德哥')
del()
//对应的SQL
delete from student where name='小明'

knex('student')
.where('age','<',27)
.update({
	age: 27
})
//对应的SQL
update student set age=18 where age<18;

knex('student')
.where({name:'德哥',age:27})
.select()
//对应的SQL
select * from student where name='德哥' and age=27;

where 写法:

where(对象)
where(key,value) //键值对
where(key,操作符,value)

prisma

Prisma是一个开源ORM框架

官网:Prisma | Next-generation ORM for Node.js & TypeScript

包含三部分:

  • Prisma Client: 查询构建器
  • Prisma Migrate: 数据迁移工具
  • Prisma Studio: 操作数据库的GUI工具
  1. 安装
 npm install prisma -g

2.创建一个项目 用prisma初始化 项目的模板为mysql

image.png

项目架构

MVC

MVC(Model View Controller)是一种软件设计的框架模式,它采用模型(Model)-视图(View)-控制器(controller)的方法把业务逻辑、数据与界面显示分离。

MVC的理念就是把数据处理、数据展示(界面)和程序/用户的交互三者分离开的一种编程模式。

image.png

  • V:View视图,Web程序中指用户可以看到的并可以与之进行数据交互的界面,比如一个Html网页界面,或者某些客户端的界面,在前面讲过,MVC可以为程序处理很多不同的视图,用户在视图中进行输出数据以及一系列操作,注意:视图中不会发生数据的处理操作。

  • M:Model模型:进行所有数据的处理工作,被模型返回的数据是中立的,就是说模型与数据格式无关,这样一个模型能为多个视图提供数据,由于应用于模型的代码只需写一次就可以被多个视图重用,所以减少了代码的重复性。

  • C:Controller控制器:负责接受用户的输入,并且调用模型和视图去完成用户的需求,控制器不会输出也不会做出任何处理,只会接受请求并调用模型构件去处理用户的请求,然后在确定用哪个视图去显示返回的数据

安装依赖

npm i inversify

npm i reflect-metadata

参考文献 MVC 模式 | 菜鸟教程 (runoob.com)

DTO层

import {IsNotEmpty} from 'class-validator'
export class UserDto{
  @IsNotEmpty({message:'名字必填'})
  name:string
  @IsNotEmpty({message:'邮件必填'})
  email:string
}

安装依赖的库:

npm i class-validator
npm i class-transformer

JWT

JWT (JSON Web Token) 是一种开放标准,用于在网络应用间安全地传输信息,通常用于认证和授权

组成:

  • 头部(Header)
  • 负载(Payload)
  • 签名(Signature)

官网:JSON Web Token Introduction - jwt.io

image.png

  1. 安装三个依赖
npm i passport passport-jwt jsonwebtoken
  1. header
{
  "alg": "HS256",
  "typ": "JWT"
}

3.Payload

iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT

4.Signature

Redis

将数据存在内存里的数据库,具有快速读写功能,适合存储经常读写的数据

字符串

image.png

列表

  1. 插入

左插入 :从头部插入(倒序)

lpush list x,y,z [element]

右插入:从尾部开始插入(正序)

rpush list 1,2,3 [element]

image.png

  1. 获取元素
LPUSH key index  
//获取列表中指定索引位置
LRANGE key star stop //获取列表中指定范围内元素

  1. 修改元素
LSET key index newValue
//修改列表中指定索引位置的元素的值

发布订阅

订阅端:

127.0.0.1:6379> ping
PONG
127.0.0.1:6379> SUBSCRIBE dingdada  #订阅名字为 dingdada 的频道
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "dingdada"
3) (integer) 1
#等待推送的信息
1) "message"  #消息
2) "dingdada"  #来自哪个频道的消息
3) "hello world\xef\xbc\x81"  # 消息的具体内容
1) "message"
2) "dingdada"
3) "my name is dyj\x81"

发送端

127.0.0.1:6379> ping
PONG
127.0.0.1:6379> PUBLISH dingdada "hello world!"  #发送消息到dingdada 频道
(integer) 1
127.0.0.1:6379> PUBLISH dingdada "my name is dyj"  #发送消息到dingdada 频道
(integer) 1

image.png

  1. PSUBSCRIBE 命令:订阅指定频道
PSUBSCRIBE + 频道  
//订阅给定的模式,可多个

  1. PUBLISH 命令:发送消息至指定频道
PUBLISH + 频道 +消息  
//将信息 message 发送到指定的频道 channel

  1. PUNSUBSCRIBE命令:退订
//指示客户端退订指定模式,若果没有提供模式则退出所有模式。
  1. SUBSCRIBE:订阅,同上一致
  2. UNSUBSCRIBE:退订,同上一致

事务

redis和mysql事务区别:

redis事务不支持回滚

mysql错了可以进行rollback回滚

打开事务

multi
//通过exec语句执行

ioredis

安装

npm i ioredis

lua

安装

一种轻量级可嵌入脚本语言

官网:www.lua.org/

  1. 安装并配置环境变量

执行此代码

lua54

image.png

此时为配置成功 2. vs扩展中安装lua 及lua debug

基本使用

  1. 全局变量
name = 'xiaoli'
--name=1   此为注释
print(name)
  1. 局部变量
do 
local name = 'xiaoli'
print(name)
end 
  1. 数组对象
local type = nil 
--表示空
  1. 数组
local type = {123}
  1. 对象
local obj = {name = 'xiaoli',age = 27}
  1. if判断
local a = 1
if a==1 then
   print(1)
elseif a==2 then
   print(2)
end
  1. 步进
for i=1101 do
   print(i)
end

--从一开始循环十遍
for i=1102 do
   print(i)
end
 --2为步长

输出结果:

image.png

注:数组索引从一开始

  1. 循环数组
local arr ={10,20,30}
for index,value in ipairs(arr) do
    print(index,value)
end

image.png

  1. 操作文件

读文件

local file = io.open("./index.txt","r")
local content = file:read("*a")
prinnt(content)

自动签到

安装

npm install node-schedule

案例

import schedule from 'node-schedule'
schedule.scheduleJob('*/5*****',function(){
    console.log('小李')
})
//每五秒钟执行那句话

//取消
schedule.scheduleJob('*/5*****',function(){
    console.log('小李')
})

serverLess

serverLess并不是一个技术,他只是一种架构模型

参考文档:www.npmjs.com/package/@se…

  1. 安装
npm install @serverless-devs/s -g
  1. 配置密钥

通过阿里云或者腾讯云

阿里云: link.juejin.cn/?target=htt…

执行以下代码添加密钥

s config add

image.png 通过以下代码检查是否添加成功

s config get -a 别名

image.png

此时说明成功

  1. 创建项目

image.png

目录下执行

s deploy

net

net模块是Node.js的核心模块之一,它提供了用于创建基于网络的应用程序的API。net模块主要用于创建TCP服务器和TCP客户端,以及处理网络通信。

image.png

场景

  1. 服务端之间的通讯

服务端之间的通讯可以直接使用TCP通讯,而不需要上升到http层

server.js:创建一个TCP服务,并且发送套接字,监听端口号3000

import net from 'net'


const server = net.createServer((socket) => {
   setInterval(()=>{
       socket.write('XiaoLi')
   },1000)
})
server.listen(3000,()=>{
    console.log('listening on 3000')
})

client.js : 连接server端,并且监听返回的数据


import net from 'net'

const client = net.createConnection({
    host: '127.0.0.1',
    port: 3000,
})

client.on('data', (data) => {
    console.log(data.toString())
})

  1. 从传输层实现http协议

创建一个TCP服务

import net from 'net'


const http = net.createServer((socket) => {
    socket.on('data', (data) => {
        console.log(data.toString())
    })
})
http.listen(3000,()=>{
     console.log('listening on 3000')
})

net.createServer创建 Unix 域套接字并且返回一个server对象接受一个回调函数

socket可以监听很多事件

  1. close 一旦套接字完全关闭就触发
  2. connect 当成功建立套接字连接时触发
  3. data 接收到数据时触发
  4. end 当套接字的另一端表示传输结束时触发,从而结束套接字的可读端

通过node http.js 启动之后我们使用浏览器访问一下

import net from 'net'

const html = `<h1>TCP Server</h1>`

const reposneHeader = [
    'HTTP/1.1 200 OK',
    'Content-Type: text/html',
    'Content-Length: ' + html.length,
    'Server: Nodejs',
    '\r\n',
    html
]

const http = net.createServer((socket) => {
    socket.on('data', (data) => {
        if(/GET/.test(data.toString())) {
            socket.write(reposneHeader.join('\r\n'))
            socket.end()
        }
    })
})
http.listen(3000, () => {
    console.log('listening on 3000')
})

image.png

socket.io

Socket 提供了实时的双向通信能力,可以实时地传输数据。客户端和服务器之间的通信是即时的,数据的传输和响应几乎是实时完成的,不需要轮询或定时发送请求

官网:link.juejin.cn/?target=htt…

聊天室

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        * {
            padding: 0;
            margin: 0;
        }

        html,
        body,
        .room {
            height: 100%;
            width: 100%;
        }

        .room {
            display: flex;
        }

        .left {
            width: 300px;
            border-right: 0.5px solid #f5f5f5;
            background: #333;
        }

        .right {
            background: #1c1c1c;
            flex: 1;
            display: flex;
            flex-direction: column;
        }

        .header {
            background: #8d0eb0;
            color: white;
            padding: 10px;
            box-sizing: border-box;
            font-size: 20px;
        }

        .main {
            flex: 1;
            padding: 10px;
            box-sizing: border-box;
            font-size: 20px;
            overflow: auto;
        }

        .main-chat {
            color: green;
        }

        .footer {
            min-height: 200px;
            border-top: 1px solid green;
        }

        .footer .ipt {
            width: 100%;
            height: 100%;
            color: green;
            outline: none;
            font-size: 20px;
            padding: 10px;
            box-sizing: border-box;
        }

        .groupList {
            height: 100%;
            overflow: auto;
        }

        .groupList-items {
            height: 50px;
            width: 100%;
            background: #131313;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
        }
    </style>
</head>
<div class="room">
    <div class="left">
        <div class="groupList">

        </div>
    </div>
    <div class="right">
        <header class="header">聊天室</header>
        <main class="main">

        </main>
        <footer class="footer">
            <div class="ipt" contenteditable></div>
        </footer>
    </div>
</div>

<body>
    <script type="module">
        const sendMessage = (message) => {
            const div = document.createElement('div');
            div.className = 'main-chat';
            div.innerText = `${message.user}:${message.text}`;
            main.appendChild(div)
        }
        const groupEl = document.querySelector('.groupList');
        const main = document.querySelector('.main');
        import { io } from "https://cdn.socket.io/4.7.4/socket.io.esm.min.js";
        const name = prompt('请输入你的名字');
        const room = prompt('请输入房间号');
        const socket = io('ws://localhost:3000');
        //键盘按下发送消息
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                e.preventDefault();
                const ipt = document.querySelector('.ipt');
                socket.emit('message', {
                    text: ipt.innerText,
                    room: room,
                    user: name
                });
                sendMessage({
                    text: ipt.innerText,
                    user: name,
                })
                ipt.innerText = '';
                
            }
        })
        //连接成功socket
        socket.on('connect', () => {
            socket.emit('join', { name, room });//加入一个房间
            socket.on('message', (message) => {
                sendMessage(message)
            })
            socket.on('groupList', (groupList) => {
                console.log(groupList);
                groupEl.innerHTML = ''
                Object.keys(groupList).forEach(key => {
                    const item = document.createElement('div');
                    item.className = 'groupList-items';
                    item.innerText = `房间名称:${key} 房间人数:${groupList[key].length}`
                    groupEl.appendChild(item)
                })
            })
        })
    </script>
</body>

</html>

index.js

import http from 'http'
import { Server } from 'socket.io'
import express from 'express'

const app = express()
app.use('*', (req, res, next) => {
    res.setHeader("Access-Control-Allow-Origin", "*");
    res.setHeader("Access-Control-Allow-Headers", "*");
    res.setHeader("Access-Control-Allow-Methods", "*");
    next()
})
const server = http.createServer(app)
const io = new Server(server, {
    cors: true //允许跨域
})
const groupList = {}
/**
 * [{1008:[{name,room,id}]}]
 */
io.on('connection', (socket) => {
    //加入房间
    socket.on('join', ({ name, room }) => {
        socket.join(room)
        if (groupList[room]) {
            groupList[room].push({ name, room, id: socket.id })
        } else {
            groupList[room] = [{ name, room, id: socket.id }]
        }
        socket.emit('message', { user: '管理员', text: `${name}进入了房间` })
        socket.emit('groupList', groupList)
        socket.broadcast.emit('groupList', groupList)
    })
    //发送消息
    socket.on('message', ({ text, room, user }) => {
        socket.broadcast.to(room).emit('message', {
            text,
            user
        })
    })
    //断开链接内置事件
    socket.on('disconnect', () => {
        Object.keys(groupList).forEach(key => {
            let leval = groupList[key].find(item => item.id === socket.id)
            if (leval) {
                socket.broadcast.to(leval.room).emit('message', { user: '管理员', text: `${leval.name}离开了房间` })
            }
            groupList[key] = groupList[key].filter(item => item.id !== socket.id)
        })
        socket.broadcast.emit('groupList', groupList)
    })
});

server.listen(3000, () => {
    console.log('listening on *:3000');
});

image.png

爬虫

爬虫,是指一种自动化程序或脚本,用于在互联网上浏览和提取信息。爬虫模拟人类用户在网页上的行为,通过HTTP协议发送请求,获取网页内容,然后解析并提取感兴趣的数据

安装

npm install puppeteer #爬虫 | 自动化UI测试

Puppeteer的一些主要特性:

  1. 自动化浏览器操作:Puppeteer可以以无头模式运行Chrome或Chromium,实现对网页的自动化操作,包括加载页面、点击、表单填写、提交等 还支持模拟用户行为,如鼠标移动、键盘输入等。
  2. 截图和生成PDF:Puppeteer可以对页面进行截图,保存为图像文件,也可以生成PDF文件。这对于生成网页快照、生成报告、进行页面测试等非常有用
  3. 爬虫和数据抓取:Puppeteer可以帮助你编写网络爬虫和数据抓取脚本。你可以通过模拟用户行为来导航网页、提取内容、执行JavaScript代码,并将数据保存到本地或进行进一步的处理
  4. 网页性能分析:Puppeteer提供了一些用于分析网页性能的API,例如测量页面加载时间、网络请求和资源使用情况等。这对于性能优化和监测非常有用
  5. 无头模式与调试模式:Puppeteer可以在无头模式下运行,即在后台运行Chrome或Chromium,无需显示浏览器界面。此外,它还支持调试模式,允许你在开发过程中检查和调试页面。

Python

官网:www.python.org/

此时说明安装成功

image.png

pip install wordcloud #生成词云图
pip install jieba #中文分词

index.js

import puppeteer from 'puppeteer'
import { spawn } from 'child_process'
const btnText = process.argv[2]
const browser = await puppeteer.launch({
    headless: false,//取消无头模式
});
const page = await browser.newPage(); //打开一个页面
page.setViewport({ width: 1920, height: 1080 }); //设置页面宽高
await page.goto('https://juejin.cn/'); //跳转到掘金
await page.waitForSelector('.side-navigator-wrap'); //等待这个元素出现

const elements = await page.$$('.side-navigator-wrap .nav-item-wrap span') //获取menu下面的span

const articleList = []
const collectFunc = async () => {
   //获取列表的信息
    await page.waitForSelector('.entry-list')
    const elements = await page.$$('.entry-list .title-row a')
    for await (let el of elements) {
        const text = await el.getProperty('innerText')
        const name = await text.jsonValue()
        articleList.push(name)
    }
    console.log(articleList)
    //调用python脚本进行中文分词 输出词云图
    const pythonProcess = spawn('python', ['index.py', articleList.join(',')])
    pythonProcess.stdout.on('data', (data) => {
        console.log(data.toString())
    })
    pythonProcess.stderr.on('data', (data) => {
        console.log(data.toString())
    })
    pythonProcess.on('close', (code) => {
        console.log(`child process exited with code ${code}`)
    })
}

for await (let el of elements) {
    const text = await el.getProperty('innerText') //获取span的属性
    const name = await text.jsonValue() //获取内容
    if (name.trim() === (btnText || '前端')) {
        await el.click() //自动点击对应的菜单
        collectFunc() //调用函数
    }
}

index.py

import jieba #引入结巴库
from wordcloud import WordCloud #引入词云图
import matplotlib.pyplot as plt
import sys
text = sys.argv[1]
words = jieba.cut(text) #中文分词
#添加字体文件 随便找一个字体文件就行 不然不支持中文
font = './font.ttf'
info = WordCloud(font_path=font,width=1000,height=800,background_color='white').generate(''.join(words))

#输出词云图
plt.imshow(info,interpolation='bilinear')
plt.axis('off')
plt.show()


addon

Nodejs c++扩展

c++编写的代码能够被编译成一个动态链接库(dll),可以被nodejs require引入使用,后缀是.node

.node文件的原理就是(window dll) (Mac dylib) (Linux so)

c++扩展编写语法:

  1. NAN(Native Abstractions for Nodejs) 一次编写,到处编译

    • 因为 Nodejs和V8都更新的很快所有每个版本的方法名也不一样,对我们开发造成了很大的问题例如

    • 0.50版本 Echo(const Prototype&proto)

    • 3.00版本 Echo(Object<Prototype>& proto)

NAN的就是一堆宏判断,判断各种版本的API,用来实现兼容所以他会到处编译

  1. N-API(node-api) 无需重新编译

    • 基于C的API

    • c++ 封装 node-addon-api

      N-API 是一个更现代的选择,它提供了一个稳定的、跨版本的 API,使得你的插件可以在不同版本的 Node.js 上运行,而无需修改代码。这大大简化了编写和维护插件的过程。

      对于 C++,你可以使用 node-addon-api,这是 N-API 的一个封装,提供了一个更易于使用的 C++ API。这将使你的代码更易于阅读和维护。

使用场景

  1. 使用C++编写的Nodejs库如node-sass node-jieba
  2. CPU密集型应用
  3. 代码保护

安装依赖

npm install --global --production windows-build-tools //管理员运行
//如果安装过python 以及c++开发软件就不需要装这个了
npm install node-gyp -g #全局安装
npm install node-addon-api -D #装到项目里

文件

大文件上传

安装依赖

  1. express 帮我们启动服务,并且提供接口
  2. multer 读取文件,存储
  3. cors 解决跨域

引入

import express from 'express'
import multer from 'multer'
import cors from 'cors'
import fs from 'node:fs'
import path from 'node:path'

提供两个接口

  1. up 用来存储切片
  2. merge 合并切片

初始化 multer.diskStorage

  • destination 存储的目录
  • filename 存储的文件名(我是通过index-文件名存储的你也可以改)

文件流下载

安装依赖

npm install express #启动服务 提供接口
npm install cors #解决跨域

核心知识响应头

  1. Content-Type 指定下载文件的 MIME 类型
  • application/octet-stream(二进制流数据)
  • application/pdf:Adobe PDF 文件。
  • application/json:JSON 数据文件
  • image/jpeg:JPEG 图像文件
  1. Content-Disposition 指定服务器返回的内容在浏览器中的处理方式。它可以用于控制文件下载、内联显示或其他处理方式
  • attachment:指示浏览器将响应内容作为附件下载。通常与 filename 参数一起使用,用于指定下载文件的名称
  • inline:指示浏览器直接在浏览器窗口中打开响应内容,如果内容是可识别的文件类型(例如图片或 PDF),则在浏览器中内联显示

http缓存

HTTP 缓存分类:

  • 强缓存
  • 协商缓存

这两种缓存都通过 HTTP 响应头来控制,目的是提高网站性能

强缓存

强缓存之后则不需要向服务器发送请求,而是从浏览器缓存读取分为(内存缓存)| (硬盘缓存)

  1. memory cache(内存缓存)

内存缓存存储在浏览器内存当中,一般刷新网页的时候会发现很多内存缓存

  1. disk cache(硬盘缓存)

硬盘缓存是存储在计算机硬盘中,空间大,但是读取效率比内存缓存慢

案例

Expires 的判断机制是:当客户端请求资源时,会获取本地时间戳,然后拿本地时间戳与 Expires 设置的时间做对比,如果对比成功,走强缓存,对比失败,则对服务器发起请求

node


import express from 'express'
import cors from 'cors'
const app = express()
app.use(cors())
app.get('/', (req, res) => {
    res.setHeader('Expires', new Date('2024-3-30 23:17:00').toUTCString()) //设置过期时间
    res.json({
        name: 'cache',
        version: '1.0.0'
    })
})

app.listen(3000, () => {
    console.log('Example app listening on port 3000!')
})

web端

<body>
    <button id="btn">send</button>
    <script>
       const btn = document.getElementById('btn');
       btn.addEventListener('click', () => {
           fetch('http://localhost:3000')
       })
    </script>
</body>

强缓存案例(Cache-Control)

Cache-Control 的值如下:

  • max-age:浏览器资源缓存的时长(秒)
  • no-cache:不走强缓存,走协商缓存
  • no-store:禁止任何缓存策略
  • public:资源既可以被浏览器缓存也可以被代理服务器缓存(CDN)
  • private:资源只能被客户端缓存

如果 max-age 和 Expires 同时出现 max-age 优先级高 缓存时间以他为主

node端

import express from 'express'
import cors from 'cors'
const app = express()
app.use(cors())
app.get('/', (req, res) => {
    res.setHeader('Cache-Control', 'public, max-age=20') //20秒
    res.json({
        name: 'cache',
        version: '1.0.0'
    })
})


app.listen(3000, () => {
    console.log('Example app listening on port 3000!')
})

协商缓存(Last-Modified)

Last-Modified 和 If-Modified-Since:服务器通过 Last-Modified 响应头告知客户端资源的最后修改时间。客户端在后续请求中通过 If-Modified-Since 请求头携带该时间,服务器判断资源是否有更新。如果没有更新,返回 304 状态码。

nodejs端

import express from 'express'
import cors from 'cors'
import fs from 'node:fs'
const getModifyTime = () => {
    return fs.statSync('./index.js').mtime.toISOString() //获取文件最后修改时间
}
const app = express()
app.use(cors())
app.get('/api', (req, res) => {
    res.setHeader('Cache-Control', 'no-cache, max-age=2592000')//表示走协商缓存
    const ifModifiedSince = req.headers['if-modified-since'] //获取浏览器上次修改时间
    res.setHeader('Last-Modified', getModifyTime())
    if (ifModifiedSince && ifModifiedSince === getModifyTime()) {
        console.log('304')
        res.statusCode = 304
        res.end()
        return
    } else {
        console.log('200')
        res.end('value')
    }
})


app.listen(3000, () => {
    console.log('Example app listening on port 3000!')
})

image.png

强缓存与协商缓存同时出现时,浏览器优先于强缓存 no cache 告诉浏览器 要走协商缓存不要走强缓存

httP2

HTTP/2(HTTP2)是超文本传输协议(HTTP)的下一个主要版本,它是对 HTTP/1.1 协议的重大改进。HTTP/2 的目标是改善性能、效率和安全性,以提供更快、更高效的网络通信

如何区分是http1还是http2

image.png

  1. 多路复用(Multiplexing):HTTP/2 支持在单个 TCP 连接上同时发送多个请求和响应。这意味着可以避免建立多个连接,减少网络延迟,提高效率

image.png

  1. 二进制分帧(Binary Framing):在应用层(HTTP2)和传输层(TCP or UDP)之间增加了二进制分帧层,将请求和响应拆分为多个帧(frames) 这种二进制格式的设计使得协议更加高效,并且容易解析和处理

image.png

帧:最小的通信单位,承载特定类型的数据,比如HTTP首部、负荷

HTTP2 帧类型:

  • 数据帧(Data Frame):用于传输请求和响应的实际数据
  • 头部帧(Headers Frame):包含请求或响应的头部信息
  • 优先级帧(Priority Frame):用于指定请求的优先级
  • 设置帧(Settings Frame):用于传输通信参数的设置
  • 推送帧(Push Promise Frame):用于服务器主动推送资源
  • PING 帧(PING Frame):用于检测连接的活跃性
  • 重置帧(RST_STREAM Frame):用于重置数据流或通知错误
  1. 头部压缩(Header Compression):HTTP/2 使用首部表(Header Table)和动态压缩算法来减少头部的大小 这减少了每个请求和响应的开销,提高了传输效率

请求一发送了所有的头部字段,第二个请求则只需要发送差异数据,这样可以减少冗余数据,降低开销

nodejs 实现http2

目前没有浏览器支持http请求访问http2,所以要用https

可以使用openssl 生成 tls证书

  1. 生成私钥
openssl genrsa -out server.key 1024

  1. 生成证书请求文件(用完可以删掉也可以保留)
openssl req -new -key server.key -out server.csr
  1. 生成证书
openssl x509 -req -in server.csr -out server.crt -signkey server.key -days 3650

http2 使用流的方式去传输

短链接

短链接是一种缩短长网址的方法,将原始的长网址转换为更短的形式

所需的依赖

  • epxress 启动服务提供接口
  • mysql2 knex依赖连接数据库
  • knex orm框架操作mysql
  • shortid 生成唯一短码

连接数据库 node.js

import knex from 'knex'
import express from 'express'
import shortid from 'shortid'
const app = express()
app.use(express.json())
const db = knex({
    client: 'mysql2',
    connection: {
        host: 'localhost',
        user: 'root',
        password: '11232331',
        database: 'project'
    }
})
//生成短码 存入数据库
app.post('/create_url', async (req, res) => {
    const { url } = req.body
    const short_id = shortid.generate()
    const result = await db('short').insert({ short_id, url })
    res.send(`http://localhost:3000/${short_id}`)
})
//获取短码 重定向
app.get('/:shortUrl', async (req, res) => {
    const short_id = req.params.shortUrl
    const result = await db('short').select('url').where('short_id', short_id)
    if (result && result[0]) {
        res.redirect(result[0].url)
    } else {
        res.send('Url not found')
    }
})

app.listen(3000, () => {
    console.log('Server is running on port 3000')
})

image.png

串口

串口技术是一种用于在计算机和外部设备之间进行数据传输的通信技术。它通过串行传输方式将数据逐位地发送和接收。

常见的串口设备有,扫描仪,打印机,传感器,控制器,采集器,电子秤等

安装的软件

  1. Keil uVision5 编写单片机代码
  2. stcai-isp 烧录单片机程序

SSO单点登录

单点登录(Single Sign-On,简称SSO)是一种身份认证和访问控制的机制,允许用户使用一组凭据(如用户名和密码)登录到多个应用程序或系统,而无需为每个应用程序单独提供凭据

SSO的优点:

用户友好性:用户只需登录一次,即可访问多个应用程序,提供了更好的用户体验和便利性 提高安全性:通过集中的身份验证,可以减少密码泄露和密码管理问题。此外,SSO还可以与其他身份验证机制(如多因素身份验证)结合使用,提供更强的安全性 简化管理:SSO可以减少管理员的工作量,因为他们不需要为每个应用程序单独管理用户凭据和权限

思路:每个应用是不同的,登录用的是一套,这时候可以模仿一下微信小程序的生成一个AppId作为应用ID,并且还可以创建一个secret,因为每个应用的权限可以不一样,所以最后生成的token也不一样,还需要一个url,登录之后重定向到该应用的地址,正规做法需要有一个后台管理系统用来控制这些,注册应用,删除应用

1. 安装依赖

  • express 启动服务编写接口
  • express-session 操作cookie
  • jsonwebtoken 生成token
  • cors 跨域

2. 目录结构

  • vue A项目 用vite创建一个就好 npm init vite
  • react B项目 用vite创建一个就好 npm init vite
  • server/index.js nodejs端
  • sso.html 登录页面

server-index.js

import express from 'express'
import session from 'express-session'
import fs from 'node:fs'
import cors from 'cors'
import jwt from 'jsonwebtoken'

const appToMapUrl = {
    'Rs6s2aHi': {
        url: "http://localhost:5173",
        name:'vue',
        secretKey: '%Y&*VGHJKLsjkas',
        token: ""
    },
    '9LQ8Y3mB': {
        url: "http://localhost:5174",
        secretKey: '%Y&*FRTYGUHJIOKL',
        name:'react',
        token: ""
    },
}
const app = express()
app.use(cors())
app.use(express.json())
app.use(session({
    secret: "$%^&*()_+DFGHJKL",
    cookie: {
        maxAge: 1000 * 60 * 60 * 24 * 7, //过期时间
    }
}))
const genToken = (appId) => {
    return jwt.sign({ appId }, appToMapUrl[appId].secretKey)
}
app.get('/login', (req, res) => {
   //注意看逻辑 如果登陆过 就走if 没有登录过就走下面的
    if (req.session.username) {
    //登录过
        const appId = req.query.appId
        const url = appToMapUrl[appId].url
        let token;
        //登录过如果存过token就直接取 没有存过就生成一个 因为可能有多个引用A登录过读取Token   B没有登录过生成Token 存入映射表
        if (appToMapUrl[appId].token) {
            token = appToMapUrl[appId].token
        } else {
            token = genToken(appId)
            appToMapUrl[appId].token = token
        }
        res.redirect(url + '?token=' + token)
        return
    }
    //没有登录 返回一个登录页面html
    const html = fs.readFileSync(`../sso.html`, 'utf-8')
    //返回登录页面
    res.send(html)
})
//提供protectd get接口 重定向到目标地址
app.get('/protectd', (req, res) => {
    const { appId,username,password } = req.query //获取应用标识
    const url = appToMapUrl[appId].url //读取要跳转的地址
    const token = genToken(appId) //生成token
    req.session.username = username //存储用户名称 表示这个账号已经登录过了 下次无需登录
    appToMapUrl[appId].token = token //根据应用存入对应的token
    res.redirect(url + '?token=' + token) //定向到目标页面
})
//启动3000端口
app.listen(3000, () => {
    console.log('http://localhost:3000')
})

soo.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
<!--这里会调用protectd接口 并且会传入 账号 密码 和 appId appId会从地址栏读取-->
    <form action="/protectd" method="get">
        <label for="username">
            账号:<input name="username" id="username" type="text">
        </label>
        <label for="password">密码:<input name="password" id="password" type="password"></label>
        <label for="appId"><input name="appId" value="" id="appId" type="hidden"></label>
        <button type="submit" id="button">登录</button>
    </form>
    <script>
       //读取AppId
        const appId = location.search.split('=')[1]
        document.getElementById('appId').value = appId
    </script>
</body>

</html>

A 应用这里用Vue展示 App.vue

<template>
    <h1>vue3</h1>
</template>

<script setup lang='ts'>
//如果有token代表登录过了 如果没有跳转到 登录页面也就是SSO 那个页面,并且地址栏携带AppID
const token = location.search.split('=')[1]
if (!token) {
    fetch('http://localhost:3000/login?appId=Rs6s2aHi').then(res => {
        location.href = res.url
    })
}
</script>

<style></style>

B应用使用React演示 App.tsx

import { useState } from 'react'
function App() {
  const [count, setCount] = useState(0)
  //逻辑其实一样的只是区分了不用应用的AppId
  const token = location.search.split('=')[1]
  if (!token) {
      fetch('http://localhost:3000/login?appId=9LQ8Y3mB').then(res => {
          location.href = res.url
      })
  }
  return (
    <>
     <h1>react</h1>
    </>
  )
}
export default App

SDL 单设备登录

SDL(Single Device Login)是一种单设备登录的机制,它允许用户在同一时间只能在一个设备上登录,当用户在其他设备上登录时,之前登录的设备会被挤下线

思路

设计数据结构

{
 id:{
    socket:ws实例
    fingerprint:浏览器指纹
  }
}

  1. 第一次登录的时候记录用户id,并且记录socket信息,和浏览器指纹
  2. 当有别的设备登录的时候发现之前已经连接过了,便使用旧的socket发送下线通知,并且关闭旧的socket,更新socket替换成当前新设备的ws连接

浏览器指纹

指纹技术有很多种,这里采用canvas指纹技术

实现代码

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <h1>SDL</h1>
    <script src="./md5.js"></script>
    <script>
       //浏览器指纹
        const createBrowserFingerprint = () => {
            const canvas = document.createElement('canvas')
            const ctx = canvas.getContext('2d')
            ctx.fillStyle = 'red'
            ctx.fillRect(0, 0, 1, 1)
            return md5(canvas.toDataURL())
        }
        //谷歌abf12f62e03d160f7f24144ef1778396
        //火狐80bea69bfc7cad5832d12e41714cf677
        //Edge abf12f62e03d160f7f24144ef1778396

        const ws = new WebSocket('ws://localhost:3306') //socket本地IP+端口
       //手机链接则为:192.168.101.46
        ws.addEventListener('open', () => {
            ws.send(JSON.stringify({
                action: 'login', //动作登录
                id: 1, //用户ID
                fingerprint: createBrowserFingerprint() //浏览器指纹
            }))
        })
        ws.addEventListener('message', (message) => {
            const data = JSON.parse(message.data)
            if (data.action === 'logout') {
                alert(data.message) //监听到挤下线操作提示弹框
            }
        })

    </script>
</body>

</html>

index.js

import express from 'express'
import { WebSocketServer } from 'ws'
import cors from 'cors'
const app = express()
app.use(cors())
app.use(express.json())
//存放数据结构
const connection = {}

const server = app.listen(3000)
const wss = new WebSocketServer({ server })

wss.on('connection', (ws) => {
    ws.on('message', (message) => {
        const data = JSON.parse(message)
        if (data.action === 'login') {
            if (connection[data.id] && connection[data.id].fingerprint) {
                console.log('账号在别处登录')
                //提示旧设备
                connection[data.id].socket.send(JSON.stringify({
                    action:'logout',
                    message:`你于${new Date().toLocaleString()}账号在别处登录` 
                }))
                connection[data.id].socket.close() //断开旧设备连接
                connection[data.id].socket = ws //更新ws
            } else {
                console.log('首次登录')
                connection[data.id] = {
                    socket: ws, //记录ws
                    fingerprint: data.fingerprint //记录指纹
                }
            }
        }
    })
})


image.png

SCL扫码登录

安装依赖

  • express 提供接口服务
  • jsonwebtoken 生成token
  • qrcode 生成二维码

流程图

image.png

思路

  const status = {
    0: '未授权',
    1: '已授权',
    2: '超时'
}

  1. 需要一个页面调用接口获取qrcode也就是二维码去展示,然后顺便展示一下状态,默认0 未授权
  2. 在这个页面轮询接口检查状态是否是已授权,如果是已授权或者超时就停止轮询
  3. 扫码之后会打开授权页面,在授权页面点击确认按钮进行授权分配token

目录结构

public

  • mandate.html 授权页面
  • qrcode.html 二维码页面

index.js (nodejs代码)

import express from 'express'
import qrcode from 'qrcode'
import jwt from 'jsonwebtoken'

let user = {

}
let userId = 1 //模拟一个用户
const app = express()
app.use(express.json())
app.use('/static', express.static('public')) //初始化静态目录
//初始化数据结构 记录用户和创建二维码的时间
//并且生成二维码的时候使用的是授权的那个页面并且把用户id带过去
app.get('/qrcode', async (req, res) => {
    user[userId] = {
        token:null,
        time: Date.now()
    }
    const code = await qrcode.toDataURL(`http://192.168.101.46:3000/static/mandate.html?userId=${userId}`)
    res.json({
        code,
        userId
    })
})
//授权确认接口 陈功授权之后生成token
app.post('/login/:userId', (req, res) => {
    const token = jwt.sign(req.params.userId, 'secret')
    user[req.params.userId].token = token
    user[req.params.userId].time = Date.now()
    res.json({
        token
    })
})
//检查接口 这个接口要被轮询调用检查状态,0未授权 1已授权 2超时
app.get('/check/:userId', (req, res) => {
    //判断超时时间
    if (Date.now() - user[userId].time > 1000 * 60 * 1) {
        return res.json({
            status: 2
        })
    }
    //如果有token那就是验证成功
    else if (user[1].token) {
        return res.json({
            status: 1
        })
    } else {
        return res.json({
            status: 0
        })
    }
})

app.listen(3000, () => {
    console.log('http://localhost:3000')
})

qrcode.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <img id="qrcode" src="" alt="">
    <div id="status-div"></div>
    <script>
        const status = {
            0: '未授权',
            1: '已授权',
            2: '超时'
        }
        const qrcode = document.getElementById('qrcode')
        const statusDiv = document.getElementById('status-div')
        let userId = null
        statusDiv.innerText = status[0]
        fetch('/qrcode').then(res => res.json()).then(res => {
            qrcode.src = res.code //获取二维码
            userId = res.userId //获取用户id
            let timer = setInterval(() => {
               //轮询调用检查接口
                fetch(`/check/${userId}`).then(res => res.json()).then(res => {
                    statusDiv.innerText = status[res.status]
                    //如果返回的状态是 超时 或者是已授权 就停止轮训
                    if (res.status != 0) {
                        clearInterval(timer)
                    }
                })
            }, 1000)
        })

    </script>
</body>

</html>

image.png

mandate.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div> <button id="btn" style="width: 100%;height: 50px;">同意授权</button></div>
    <div> <button style="width: 100%;height: 50px;margin-top: 20px;">拒绝授权</button></div>
    <script>
        const btn = document.getElementById('btn')
        let userId = location.search.slice(1).split('=')[1]
        btn.onclick = () => {
            //点击授权按钮
            fetch(`/login/${userId}`, {
                method: 'POST',
            }).then(res => res.json()).then(res => {
                alert(`授权成功`)
            }).catch(err => {
                alert(err)
            })
        }
    </script>
</body>

</html>

OpenAI

官网:openai.com/

远程桌面

远程桌面(Remote Desktop)是一种技术,允许用户通过网络远程连接到另一台计算机,并在本地计算机上控制远程计算机的操作

应用场景

  1. 远程桌面
  2. 远程服务器操作
  3. 云游戏
  4. 环境准备

python环境安装:

www.python.org/downloads/

c++ 环境安装

visualstudio.microsoft.com/zh-hans/tha…

全局依赖

npm install node-gyp -g

robotjs 需要依赖于c++ node-gyp依赖python 需要通过node-gyp编译 robotjs

项目依赖

  1. screenshot-desktop 截屏
  2. ws 实时传输
  3. get-pixels 获取图片大小
  4. robotjs 操作受控设备

杀毒

安装: link.juejin.cn/?target=htt…

  1. 下载完成之后安装 然后配置环境变量 安装完成之后目录会有一个 conf_examples 文件夹 它自带的

  2. clamd.conf

  3. freshclam.conf

更新病毒库

执行这个命令 freshclam

启动 clamd 服务

OSS

  1. OSS 是按照 Bucket 存储的

image.png

需要购买

link.juejin.cn/?target=htt…

OSS官方文档: help.aliyun.com/zh/oss/deve…

npm install ali-oss

import OSS from 'ali-oss';
import path from 'node:path'
const client = new OSS({
    region: 'oss-cn-beijing', //区域
    accessKeyId: 'XXXXXXXXXXXXXXXXXXXX上图的accessKeyId',
    accessKeySecret: 'XXXXXXXXXXXXXXXXX上图的accessKeySecret',
    bucket: 'nodejs-oss', //存储库
});

上传:

//第一个参数上传到OSS文件的名称  
//第二个参数本地图片的路径
const result = await client.put('1.jpg', './1.jpg');
console.log(result)

下载:

//第一个参数OSS图片名称
//第二个参数下载到本地的路径
await client.get('1.png',path.join(process.cwd(),'./1.png'));

删除:

//第一个参数删除存储库文件的名称
const result = await client.delete('1.png');

libuv

在Node.js中,libuv是作为其事件循环和异步I/O的核心组件而存在的。Node.js是构建在libuv之上的,它利用libuv来处理底层的异步操作,如文件I/O、网络通信和定时器等

在Nodejs中,事件循环分为6个阶段。每个阶段都有一个任务队列。当Node启动时,会创建一个事件循环线程,并依次按照下图所示顺序进入每个阶段,执行每个阶段的回调

Nodejs事件循环可以划分为两种: 微任务和宏任务

  1. 宏任务
  • timers 执行setTimeout和setInterval的回调
  • pending callbacks 执行推迟的回调如IO,计时器
  • idle,prepare 空闲状态 nodejs内部使用无需关心
  • poll 执行与I/O相关的回调(除了关闭回调、计时器调度的回调和5. setImmediate之外,几乎所有回调都执行) 例如 fs的回调 http回调
  • check 执行setImmediate的回调
  • close callback 执行例如socket.on('close', ...) 关闭的回调
  1. 微任务
  • process.nextTick
  • promise

Fastify

Fastify是一个web框架,高度专注于以最少的开销和强大的插件架构提供最佳的开发体验 它是运行在Node.js上的最快的Web框架之一

应用场景

  • 网关层
  • Nest唯二框架之一
  • 需要高性能的服务
  • 以太坊

使用

基本跟express一样

安装

npm install fastify

index.js

import fastify from "fastify";

const server = fastify();
//post接口
server.post("/", async (request, reply) => {
    const { name, version } = request.body;
    //返回json  支持直接return
    return {
        name,
        version
    }
});
//get接口
server.get("/", async (request, reply) => {
    reply.send(`${request.query.name}`);
});

server.listen({ port: 3000 }).then(() => console.log("server is running"))

路由

  1. method 定义请求方式 例如 get post put等

  2. url 匹配接口路径

  3. schema 含请求和响应模式的对象。它们需要采用JSON 架构格式

    • body 验证post接口的参数
    • querystring 验证地址栏上面的参数也就是get
    • params 验证动态参数
    • response 过滤并生成响应的模式,设置模式可以使我们的吞吐量提高 10-20%
  4. handler 请求处理函数

server.route({
    method: "GET",
    url: "/list",
    schema: {
        querystring: {
            type: "object",
            properties: {
                page: { type: "number" },
                pageNo: { type: "number" }
            },
            required: ["page", "pageNo"], //必填项
        },
        response: {
            200: {
                type: "object", //返回一个对象
                properties: { //返回的数属性描述
                    data: {  //返回data
                        type: "array", //是个数组类型
                        items: { //子集
                            type: "object", //是个对象
                            properties: {  //子集的属性
                                name: { type: "string" },
                                version: { type: "string" }
                            }
                        }
                    }
                }
            }
        }
    },
    handler: (request, reply) => {
        request.query.page
        return {
            data: [{ name: "fastify", version: "4.27.0" }]
        }
    }
})

插件编写

与 JavaScript 一样,一切都是对象,在 Fastify 中一切都是插件

Fastify 允许用户通过插件扩展其功能。插件可以是一组路由、服务器装饰器或其他任何东西。您需要使用一个或多个插件的 API 是register

fastify.register(plugin, [options])

  1. app就是fastify实例
  2. options就是传递过来的传参数
  3. done控制流程 跟express next一样
server.register(function (app, opts, done) {

    app.decorate(opts.name, (a, b) => a + b);

    const res = app.add(1, 2)

    console.log(res)

    done()
},{
    name:'add' //options
})

连接数据库

Fastify 的生态系统提供了一些用于连接各种数据库引擎的插件。link.juejin.cn/?target=htt…

需要用到什么插件都可以去生态去找

安装包

npm i @fastify/mysql

连接数据库

server.register(import('@fastify/mysql'),{
    connectionString: 'mysql://root:123456@localhost:3306/xiaoman', //账号,密码,IP,端口,库名
})

实现一个增加和查询


import fastify from "fastify";

const server = fastify({
    logger: false,
});

server.register(import('@fastify/mysql'), {
    connectionString: 'mysql://root:123456@localhost:3306/xiaoma',
})

//添加
server.post('/add',(request, reply) => {
    server.mysql.query("insert into user(create_time,name,hobby) values(?,?,?)", [new Date(), request.body.name, request.body.hobby], (err, results) => {
        if (err) {
            console.log(err);
            return reply.send(err);
        }
        reply.send({ results })
    })
})
//查询
server.get('/list',(request, reply) => {
   server.mysql.query("select * from user", (err, result) => {
        reply.send({ result })
    })
})


server.listen({ port: 3000 }).then(() => console.log("server is running"))

网关层

什么是网关层(gateway)?

网关层是位于客户端和后端服务之间的中间层,用于处理和转发请求。它充当了请求的入口点,并负责将请求路由到适当的后端服务,并将后端服务的响应返回给客户端。网关层在分布式系统和微服务架构中起到了关键的作用

安装依赖

npm install fastify
npm install express
npm install @fastify/caching #缓存
npm install @fastify/http-proxy #代理/负载均衡
npm install @fastify/rate-limit #限流
npm install opossum #熔断技术

目录结构

src/
  config/
    index.js
  proxy/
    index.js
  index.js
package.json
server.js

先编写外层的server.js 启动一个简单的服务

process.argv[2]这里使用process接受端口号这样就可以方便开启多个服务

import express from 'express'

const app = express()

app.get('/info', (req, res) => {
    res.json({
        code:200,
        port: process.argv[2],
    })
})

app.get('/', (req, res) => {
     res.json({
         code: 200
     })
})

app.listen(process.argv[2], () => console.log(`Server running on port ${process.argv[2]}`))

启动服务

node server.js 9001
node server.js 9002

网关层编写

src/index.js

核心功能实现

  1. 代理服务(代理服务做成了配置项是个数组 循环注册,这样可以统一入口)
  2. 熔断技术(检测服务是否正常运行,如果挂掉或者超时一定阈值就熔断)
  3. 缓存技术(底层其实也是协商缓存|强缓存)
  4. 限流技术(规定在多少时间内,只能发起几次请求,防止DDOS)

提示 :不是必须按照我这个实现,只是个参考,一般场景是需要实现这些功能的

proxy/index.js

import fastify from 'fastify'
import proxy from '@fastify/http-proxy' //负载代理技术
import rateLimit from '@fastify/rate-limit' //限流技术
import proxyConfig from './proxy/index.js' //请往下翻
import caching from '@fastify/caching' //缓存技术
import CircuitBreaker from 'opossum' //熔断技术
import { rateLimitConfig, cachingConfig, breakerConfig } from './config/index.js' //请往下翻
const app = fastify({
    logger: false
})
//熔断技术
const breaker = new CircuitBreaker((url) => {
    return fetch(url).then((res) => res.json()) //检测服务是否挂掉
}, breakerConfig)

app.register(caching, cachingConfig) //注册缓存服务

app.register(rateLimit, rateLimitConfig) //注册限流

proxyConfig.forEach(({ upstream, prefix, rewritePrefix, httpMethods }) => {
    app.register(proxy, {
         //请求代理服务之前触发熔断
        preHandler: (request, reply, done) => {
             //检测这个服务 如果服务挂掉立马熔断
            breaker.fire(upstream).then(() => done()).catch(() => reply.code(503).send('Circuit breaker tripped'))
        },
        upstream,
        prefix,
        rewritePrefix,
        httpMethods
    })
})


//启动服网关
app.listen({ port: 3000 }).then(() => console.log('server running on port 3000'))

网关层不一定只有一个

config/index.js

export default [
    {
        upstream: 'http://localhost:9001', //代理地址
        prefix: '/pc', //前缀
        rewritePrefix: '', //实际请求将pc 替换成 '' 因为后端服务器没有pc这个路由
        httpMethods: ['GET', 'POST'], //允许的请求方式
    },
    {
        upstream: 'http://localhost:9002',
        prefix: '/mobile',
        rewritePrefix: '',
        httpMethods: ['GET', 'POST'],
    }
]

其他:

export const rateLimitConfig = {
    max: 5, //每 1 分钟最多允许 5 次请求
    timeWindow: '1 minute', //一分钟
}

export const cachingConfig = {
    privacy: 'private', //缓存客户端服务器 禁止缓存代理服务器
    expiresIn: 1000 //缓存1s
}

export const breakerConfig = {
    errorThresholdPercentage: 40, //超过 40% 会触发熔断
    timeout: 1000, //超过 1s 会触发熔断
    resetTimeout: 5000, //熔断后 5s 会重置
}

启动服务

nodemon src/index.js

RabbitMQ

什么是RabbitMQ

  • RabbitMQ是一个开源的,在AMQP基础上完整的,可复用的企业消息系统
  • 支持主流的操作系统,Linux、Windows、MacOS等
  • 多种开发语言支持,Java、Python、Ruby、.NET、PHP、C/C++、javaScript等

image.png

RabbitMQ 的安装

  1. Rabbit MQ的依赖环境erlang

因为MQ是基于这个语言开发的

官网下载erlang:www.erlang.org/downloads

配置环境变量

  1. 安装IMQ

官网:www.rabbitmq.com/docs/instal…

  • 启动MQ

安装MQ插件拥有可视化面板

rabbitmq-plugins enable rabbitmq_management
  • 启动MQ命令

MQ默认端口5672

rabbitmq-server.bat start

访问: http://localhost:15672/#/ 账号密码都是guest

  • Nodejs使用

我们在Nodejs使用MQ 就是微服务应用,或者就是跨语言通讯

安装依赖

npm install amqplib

producer.js 生产者:

import express from 'express'
import amqplib from "amqplib";

const app = express()
//连接MQ
const connection = await amqplib.connect("amqp://localhost")
//创建一个通道
const channel = await connection.createChannel()
const queueName = "task_queue"
app.get('/send', (req, res) => {
    const message = req.query.message
    //发送消息
    channel.sendToQueue(queueName, Buffer.from(message),{
        persistent: true //持久化消息
    })
    res.send('send message success')
})


app.listen(3000,()=>{
    console.log('producer listen 3000')
})


consume.js 消费者:

import amqplib from "amqplib";
const queueName = "task_queue"
//连接
const connection = await amqplib.connect("amqp://localhost")
const channel = await connection.createChannel()

//连接队列
await channel.assertQueue(queueName, {
  durable: true //队列持久化
})
//消息者监听器
channel.consume(queueName, (msg) => {
  console.log(`[x] Received ${msg.content.toString()}`)
  channel.ack(msg) //确认消费该消息
})

持久化实现

生产者把消息推入队列此时宕机了,重启之后消息丢失,为了解决这个问题我们需要实现持久化策略

  1. 队列持久化 消费者连接队列的时候开启 durable: true 即可实现队列持久化

  2. 消息持久化 发送方 在发送消息的时候 开启 persistent: true 即可持久化

  3. 还可以跨语言通讯 python

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))

channel = connection.channel()

queueName = 'task_queue'

channel.queue_declare(queue=queueName,durable=True)

message = '小满zs'

channel.basic_publish(exchange='',routing_key=queueName,body=message)

connection.close()

MQ进阶用法

发布订阅

点对点通讯生产者发送一条消息通过路由投递到Queue,只有一个消费者能消费到 也就是一对一发送

布订阅就是生产者的消息通过交换机写到多个队列,不同的订阅者消费不同的队列,也就是实现了一对多

image.png

发布订阅的模式

  1. Direct(直连)模式:把消息放到交换机指定key的队列里面。

  2. Topic(主题)模式: 把消息放到交换机指定key的队列里面,额外增加使用 "*" 匹配一个单词或使用 "#" 匹配多个单词

  3. Headers(头部)模式:把消息放到交换机头部属性去匹配队列

  4. Fanout(广播)模式:把消息放入交换机所有的队列,实现广播

代码编写

  1. direct模式编写 主要就是通过 routingKey 匹配实现路由 这里的zs就是routingKey

producer.js:

import amqplib from 'amqplib'
const connection = await amqplib.connect('amqp://localhost:5672')
//创建一个频道
const channel = await connection.createChannel() 
//声明一个交换机
/**
 * @param {String} exchange 交换机的名称
 * @param {String} type "direct" | "topic" | "headers" | "fanout" | "match" | 使用广播模式
 * @param {Object} options {durable: true} //开启消息持久化
 */
await channel.assertExchange('logs', 'direct', {
    durable: true
})
//发送消息
/**
 * @param {String} exchange 交换机的名称
 * @param {String} routingKey 路由键
 * @param {Buffer} content 消息内容
 */
 //这里的zs就是routingKey
channel.publish('logs', 'zs', Buffer.from('小满direct模式发送的消息'))

//断开
await channel.close()
await connection.close()
process.exit(0)

consume.js:

import amqplib from 'amqplib'
const connection = await amqplib.connect('amqp://localhost:5672')
const channel = await connection.createChannel() //创建一个频道

await channel.assertExchange('logs', 'direct', {
    durable: true
})

//添加一个队列
const { queue } = await channel.assertQueue('queue1', {
    durable: true
})
//绑定交换机
/**
 * @param {String} queue 队列名称
 * @param {String} exchange 交换机名称
 * @param {String} routingKey 路由键
 */
//匹配对应的zs值才能收到
await channel.bindQueue(queue, 'logs', 'zs')
//接收消息
channel.consume('queue1', (msg) => {
    console.log(msg.content.toString());
}, {
    noAck: true //自动确认消息被消费
})

consume2.js:

import amqplib from 'amqplib'
const connection = await amqplib.connect('amqp://localhost:5672')
const channel = await connection.createChannel() //创建一个频道

await channel.assertExchange('logs', 'direct', {
    durable: true
})

//添加一个队列
const { queue } = await channel.assertQueue('queue2', {
    durable: true
})
//绑定交换机
/**
 * @param {String} queue 队列名称
 * @param {String} exchange 交换机名称
 * @param {String} routingKey 路由键
 */
 //匹配对应的zs值才能收到
await channel.bindQueue(queue, 'logs', 'zs')
//接收消息
channel.consume('queue2', (msg) => {
    console.log(msg.content.toString());
}, {
    noAck: true //自动确认消息被消费
})

  1. Topic模式编写

我们把模式切换成了Topic 并且publish 发布的时候 routingKey 换成了 xm.xxxxxxxx

producer.js

import amqplib from 'amqplib'
const connection = await amqplib.connect('amqp://localhost:5672')
//创建一个频道
const channel = await connection.createChannel() 
//声明一个交换机
/**
 * @param {String} exchange 交换机的名称
 * @param {String} type "direct" | "topic" | "headers" | "fanout" | "match" | 使用广播模式
 * @param {Object} options {durable: true} //开启消息持久化
 */
await channel.assertExchange('topic', 'topic', {
    durable: true
})
//发送消息
/**
 * @param {String} exchange 交换机的名称
 * @param {String} routingKey 路由键
 * @param {Buffer} content 消息内容
 */
 //注意这儿匹配规则换了 换成xm.xxxxxxxxxxxxxxxxxxxxx
channel.publish('logs', 'xm.sadsdsdasdasdasdsda', Buffer.from('小满topic模式发送的消息'))

//断开
await channel.close()
await connection.close()
process.exit(0)

消费者匹配(注意这里匹配规则xm.'使用了 就是模糊匹配的意思)

import amqplib from 'amqplib'
const connection = await amqplib.connect('amqp://localhost:5672')
const channel = await connection.createChannel() //创建一个频道

await channel.assertExchange('topic', 'topic', {
    durable: true
})

//添加一个队列
const { queue } = await channel.assertQueue('queue1', {
    durable: true
})
//绑定交换机
/**
 * @param {String} queue 队列名称
 * @param {String} exchange 交换机名称
 * @param {String} routingKey 路由键 *匹配一个单词 #匹配多个单词
 */
 //这儿变化了
await channel.bindQueue(queue, 'topic', 'xm.*')
//接收消息
channel.consume('queue1', (msg) => {
    console.log(msg.content.toString());
}, {
    noAck: true //自动确认消息被消费
})

  1. Headers模式

生产者(注意 publish 增加第四个参数开启了header 添加了data参数)

import amqplib from 'amqplib'
const connection = await amqplib.connect('amqp://localhost:5672')
//创建一个频道
const channel = await connection.createChannel() 
   //声明一个交换机
   /**
    * @param {String} exchange 交换机的名称
    * @param {String} type "direct" | "topic" | "headers" | "fanout" | "match" | 使用广播模式
    * @param {Object} options {durable: true} //开启消息持久化
    */
   await channel.assertExchange('headers', 'headers', {
       durable: true
   })
   //发送消息
   /**
    * @param {String} exchange 交换机的名称
    * @param {String} routingKey 路由键
    * @param {Buffer} content 消息内容
    * @param {Object} options {headers: {'key': 'value'}} //定义匹配规则
    */
    //嘿 这儿变了
   channel.publish('headers', '', Buffer.from('小满headers模式发送的消息'),{
       headers: {
           data:'xmzs'
       }
   })

   //断开
   await channel.close()
   await connection.close()
   process.exit(0)

消费者(bindQueue 增加一个对象 属性跟生产者对应即可)

   import amqplib from 'amqplib'
   const connection = await amqplib.connect('amqp://localhost:5672')
   const channel = await connection.createChannel() //创建一个频道
   await channel.assertExchange('headers', 'headers')

   //添加一个队列
   const { queue } = await channel.assertQueue('queue1')
   //绑定交换机
   /**
    * @param {String} queue 队列名称
    * @param {String} exchange 交换机名称
    * @param {String} routingKey 路由键 *匹配一个单词 #匹配多个单词
    */
   await channel.bindQueue(queue, 'headers', '',{
       data:'xmzs' //注意这儿不加headers 直接放值即可
   })
   //接收消息
   channel.consume(queue, (msg) => {
       console.log(msg.content.toString());
   }, {
       noAck: true //自动确认消息被消费
   })
 
  1. Fanout模式 生产者(其实也就是routingKey 变成一个空值实现全体广播)
import amqplib from 'amqplib'
const connection = await amqplib.connect('amqp://localhost:5672')
//创建一个频道
const channel = await connection.createChannel()
//声明一个交换机
/**
* @param {String} exchange 交换机的名称
* @param {String} type "direct" | "topic" | "headers" | "fanout" | "match" | 使用广播模式
* @param {Object} options {durable: true} //开启消息持久化
*/
await channel.assertExchange('fanout', 'fanout')
//发送消息
/**
* @param {String} exchange 交换机的名称
* @param {String} routingKey 路由键
* @param {Buffer} content 消息内容
*/
channel.publish('fanout', '', Buffer.from('小满fanout模式发送的消息'))

//断开
await channel.close()
await connection.close()
process.exit(0)

消费者(routingKey接受空值即可 就算有值也会被忽略)

import amqplib from 'amqplib'
const connection = await amqplib.connect('amqp://localhost:5672')
const channel = await connection.createChannel() //创建一个频道
await channel.assertExchange('fanout', 'fanout')

//添加一个队列
const { queue } = await channel.assertQueue('queue1')
//绑定交换机
/**
* @param {String} queue 队列名称
* @param {String} exchange 交换机名称
* @param {String} routingKey 路由键 *匹配一个单词 #匹配多个单词
*/
await channel.bindQueue(queue, 'fanout', '')
//接收消息
channel.consume(queue, (msg) => {
console.log(msg.content.toString());
}, {
noAck: true //自动确认消息被消费
})

MQ高级用法-延时消息

什么是延时消息?

Producer 将消息发送到 MQ 服务端,但并不期望这条消息立马投递,而是延迟一定时间后才投递到 Consumer 进行消费,该消息即延时消息

插件安装

RabbitMQ延迟队列插件下载

下载地址 github.com/rabbitmq/ra…

安装

把下载好的文件拖到你的rabbitMQ下面的plugins目录里面

#举例
D:\Applaaction\rabbitmq_server-3.13.0\plugins

启用插件

执行下面的命令

rabbitmq-plugins enable rabbitmq_delayed_message_exchange

检查是否成功: 打开可视化面版访问 http://localhost:15672/#/  账号密码都是 guest

发现新增了一个延迟队列类型 x-delayed-message

应用场景

image.png

案例

  1. 我们使用新增的延时类型切换一下type类型 x-delayed-message
  2. 连接交换机的时候增加arguments对象 添加 x-delayed-type 目标交换机类型 这儿使用direct
  3. 发布消息的时候增加头部信息 x-delay:延时的时间(毫秒)

生产者:producer.js

import amqplib from 'amqplib'
//1.连接MQ
const connection = await amqplib.connect('amqp://localhost:5672')
//2.创建一个通道
const channel = await connection.createChannel()
//3.创建交换机
/**
 * @param {string} exchange 交换机名称 随便写
 * @param {string} type 交换机类型 direct fanout topic headers x-delayed-message
 * @param {options} options 可选配置项
 */
//这个方法就是说如果你创建过这个交换机就不会再创建了 如果没有创建过这个交换机就会创建
await channel.assertExchange('delayed-1', 'x-delayed-message',{
    arguments:{
        'x-delayed-type': 'direct' //目标交换机类型
    }
})


//4.发送消息
/**
 * @param {string} exchange 要发送到交换机的名称
 * @param {string} routingKey 匹配路由的key
 * @param {Buffer} buffer 要发送的消息
 * @param {options} options 可选配置项
 */
channel.publish('delayed-1', 'time', Buffer.from('延时消息'),{
    headers:{
        'x-delay': 10000 //延时 10秒
    }
})
//断开连接
await channel.close()
await connection.close()
process.exit(0)

消费者:consume.js

import amqplib from 'amqplib'
//1.连接MQ
const connection = await amqplib.connect('amqp://localhost:5672')
//2.创建一个通道
const channel = await connection.createChannel()
//3.创建交换机
await channel.assertExchange('delayed-1', 'x-delayed-message',{
    arguments:{
        'x-delayed-type': 'direct' //目标交换机类型
    }
})
//4.创建一个队列
const { queue } = await channel.assertQueue('queue-1')
//5.交换机跟队列要绑定
/**
 * @param {string} queue 队列名称
 * @param {string} exchange 交换机名称
 * @param {string} routingKey 匹配路由的key
 */
channel.bindQueue(queue, 'delayed-1', 'time')
//6.消费消息
channel.consume(queue, (msg) => {
    console.log(msg.content.toString())
}, {
    noAck: true
})

Kafka

简介

Kafka 的主要设计目标是提供一个可持久化的、高吞吐量的、容错的消息传递系统。它允许你以发布-订阅的方式发送和接收流数据,并且可以处理大量的消息,同时保持低延迟。Kafka 的设计还强调了分布式的特性,使得它可以在大规模集群中运行,处理大量数据和高并发的请求

kafka架构

image.png

  1. Producer:生产者,消息的产生者,是消息的入口
  2. Broker:Broker 是 kafka 一个实例,每个服务器上有一个或多个 kafka 的实例,简单的理解就是一台 kafka 服务器,​​kafka cluster​​表示集群的意思
  3. Topic:消息的主题,可以理解为消息队列,kafka的数据就保存在topic。在每个 broker 上都可以创建多个 topic
  4. Partition:Topic的分区,每个 topic 可以有多个分区,分区的作用是做负载,提高 kafka 的吞吐量。同一个 5. topic 在不同的分区的数据是不重复的,partition 的表现形式就是一个一个的文件夹
  5. Message:每一条发送的消息主体。
  6. Consumer:消费者,即消息的消费方,是消息的出口。
  7. Consumer Group:我们可以将多个消费组组成一个消费者组,在 kafka 的设计中同一个分区的数据只能被消费者组中的某一个消费者消费。同一个消费者组的消费者可以消费同一个topic的不同分区的数据,这也是为了提高kafka的吞吐量
  8. Zookeeper:kafka 集群依赖 zookeeper 来保存集群的的元信息,来保证系统的可用性

安装Kafka

安装JDK

因为Kafka用了java编写

java官网 www.oracle.com/java/techno…

新版安装成功后无需配置环境变量自己就配好了,旧版本则需手动配置一下 与rabbiteMQ 那一章节一样

需要在环境变量配置 JAVA_HOME 因为zookeeper 要读取这个环境变量 这个是必须的

测试是否安装成功

java --version

安装ZooKeeper

因为Kafka也用到了ZooKeeper

ZooKeeper 官网:zookeeper.apache.org/releases.ht…

  1. 把 apache-zookeeper-3.9.2-bin.tar.gz 解压到你喜欢的目录 重命名 zookeeper
  2. 打开 zookeeper\conf ,把zoo_sample.cfg重命名成zoo.cfg
  3. 打开 zoo.cfg 修改 dataDir 改成 ./zookeeper/data
  4. D:\Applaaction\zookeeper\bin(例子) 配置到环境变量里面
  5. 启动 打开cmd 执行 zkServer (启动完成之后不要关闭)

安装Kafka

Kafka 官网:kafka.apache.org/

  1. 把下载好的文件解压到你喜欢的目录里面
  2. 把kafka文件夹名字 重新命名更短的名字 否则后面执行命令会报错(因为cmd不能支持很长的命令)
  3. 打开kafka/config/server.properties 文件 修改 log.dirs 改为 log.dirs=./logs
  4. 启动kafka 在kafka根目录执行 以下命令
.\bin\windows\kafka-server-start.bat .\config\server.properties

安装依赖 kafkajs 官网 kafka.js.org/docs/gettin…

npm i kafkajs

进阶用法

  1. server.properties配置文件

server.properties是Kafka服务器的配置文件,它用于配置Kafka服务的各个方面,包括网络设置、日志存储、消息保留策略、安全认证

#broker的全局唯一编号,不能重复
broker.id=0
#端口号
port=9092
#处理网络请求的线程数量
#接收线程会将接收到的消息放到内存中,然后再从内存中写入磁盘。
num.network.threads=3
#用来处理磁盘IO的线程数量
#消息从内存中写入磁盘是时候使用的线程数量。
num.io.threads=8
#发送套接字的缓冲区大小
socket.send.buffer.bytes=102400
#接受套接字的缓冲区大小
socket.receive.buffer.bytes=102400
#请求套接字的缓冲区大小
socket.request.max.bytes=104857600
#kafka运行日志存放的路径
log.dirs=./logs
#topic在当前broker上的分区个数
num.partitions=1
#用来恢复和清理data下数据的线程数量
num.recovery.threads.per.data.dir=1
#每个topic的分区数
offsets.topic.replication.factor=1
#每个topic的副本数
transaction.state.log.replication.factor=1
#每个topic的最小副本数
transaction.state.log.min.isr=1
#日志保留时间,单位小时 168就是7天
log.retention.hours=168
#定期检查日志是否过期的间隔,单位毫秒
log.retention.check.interval.ms=300000
#日志清理器是否启用
log.cleaner.enable=true
#zookeeper地址
zookeeper.connect=localhost:2181
#zookeeper连接超时时间
zookeeper.connection.timeout.ms=18000
#zookeeper会话超时时间
group.initial.rebalance.delay.ms=0

  1. producer.properties配置文件 producer.properties是Kafka生产者客户端的配置文件,用于配置Kafka生产者的行为和属性。当你使用Kafka生产者API发送消息到Kafka集群时,可以使用该配置文件
#配置生产者的broker列表 可以配置多个,以逗号隔开 也就是做集群的
#来获取每一个topic的分片数等元数据信息。
bootstrap.servers=localhost:9092
# 配置数据压缩方式 有none,gzip,snappy,lz4,zstd
compression.type=none
#客户端等待请求的响应的最长时间 超时时间
#request.timeout.ms=
#定期发送消息的时间间隔,一般配合batch.size使用,例如设置了50ms,那么每50ms就会发送一次消息合集
#linger.ms=
#每次发送给Kafka服务器请求消息的最大大小
#max.request.size=
#批量发送消息比如说设置了值16KB,那么消息内容凑够16KB就会被发送出去,否则就不会发送,这样可以避免单条消息太大导致的发送失败
#batch.size=
#约束producer缓存池的大小,默认是32MB,可以根据实际情况调整
#buffer.memory=

  1. consumer.properties配置文件 用于配置Kafka消费者的属性。它包含了一系列用于定义消费者行为的参数和数值
#定义KafkaBroker列表 可以配置多个,以逗号隔开 也就是做集群的
bootstrap.servers=localhost:9092
#定义消费者组的ID
group.id=test-consumer-group
#用于指定当消费者加入一个消费者组但没有可用的消费位移时的行为
#有三种选项 earliest/latest/none
#earliest:表示消费者将从最早的可用消费位移开始消费。消费者将从主题的最早消息开始消费,即使这些消息已经过期。
#latest:表示消费者将从最新的可用消费位移开始消费。消费者将从主题的最新消息开始消费,即跳过已经过期的消息。
#none:表示如果没有可用的消费位移,消费者将抛出异常。这样可以确保消费者只消费已经提交的消费位移。
#auto.offset.reset=
#心跳间隔用于保持消费者活跃状态
#session.timeout.ms
#指定消费者一次性获取最大的消息数量,如果为0表示不限制
#fetch.max.bytes=1048576
#指定消费者一次性获取的最大等待时间,如果为0表示不限制
#fetch.max.wait.ms=500

高阶

Kafka集群操作

  1. 创建多个kafka服务

拷贝一份kafka完整目录改名为kafka2

修改配置文件 kafka2/config/server.properties 这个文件

broker.id=1 //唯一broker
port=9093 //切换端口
listeners=PLAINTEXT://:9093 //切换监听源

启动zooKeeper和kafka和kafka2

.\bin\windows\kafka-server-start.bat .\config\server.properties

事务

KafkaJS 提供了对 Kafka 事务的支持,可以使用它来执行具有事务特性的操作。Kafka 事务用于确保一组相关的消息要么全部成功提交,要么全部回滚,从而保持数据的一致性

import { Kafka, CompressionTypes } from 'kafkajs'

const kafka = new Kafka({
    clientId: 'my-app', //客户端标识
    brokers: ['localhost:9092', 'localhost:9093'], //kafka集群
})

//生产者
const producer = kafka.producer({
    transactionalId: '填写事务ID',
    maxInFlightRequests: 1, //最大同时发送请求数
    idempotent: true, //是否开启幂等提交
})
//连接服务器
await producer.connect()

const transaction = await producer.transaction()
try {
    await transaction.send({
        topic: 'xiaoman',
        messages: [{ value: '100元' }],
    })
    await transaction.commit() // 事务提交
}
catch (e) {
    console.log(e)
    await transaction.abort() // 事务提交失败,回滚
}
await admin.disconnect()

Nacos

什么是Nacos?

官网 nacos.io/zh-cn/docs/…

Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理解决方案

安装

nacos.io/download/na…

  1. 下载完成解压到你喜欢的目录即可 记得名称改短一点如 nacos
  2. 修改配置文件连接数据库 D:\Applaaction\nacos\conf\application.properties
//把注释打开默认是注释掉的 大概在42行左右
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=123456

  1. 导入nacos数据库文件并且注册数据库
CREATE DATABASE `nacos`
    DEFAULT CHARACTER SET = 'utf8mb4';

  1. 编写启动命令 放在桌面上即可
cd D:\Applaaction\nacos\bin
./startup.cmd -m standalone

5 .启动nacos

双击刚才编写好的启动脚本即可

ElasticSearch

全文检索

什么是ElasticSearch?

ElasticSearch 是一个开源的、分布式的搜索和分析引擎,特别擅长处理大规模的日志和文本数据

ElasticSearch 常用于日志和事件数据的实时搜索、分析以及大型文本数据的全文检索

场景

  1. 应用程序日志分析:实时分析应用程序日志,以便快速发现和解决问题。
  2. 网站搜索:为网站提供快速的全文搜索功能,提升用户体验
  3. 地图服务:提供基于位置的搜索和导航服务

安装elastic

www.elastic.co/cn/download…

  1. 选择你所拥有的操作系统下载即可

  2. 解压到你喜欢的目录即可

  3. 启动运行bin目录下面的 elasticsearch 文件

  4. 修改elastic密码 执行bin 目录 下面的 elasticsearch-reset-password -u elastic -i 然后输出密码即可 例如 123456

  5. 关闭https打开 elasticSearch/config/elasticsearch.yml 修改为false

访问http://localhost:9200 即可 默认端口9200

  1. 账号是 elastic 密码是第四条你修改之后的密码 返回以下信息即可成功

安装 kibana

Kibana是一个开源的分析和可视化平台,设计用于和Elasticsearch一起工作,你用Kibana来搜索,查看,并和存储在Elasticsearch索引中的数据进行交互

下载地址www.elastic.co/cn/download…

选择对应的操作系统即可

Kibana基于Nodejs环境开发,需要安装Nodejs 安装过的忽略

对接Node.js

安装依赖包

npm install @elastic/elasticsearch

增删改差

核心概念

  1. 索引类似于关系型数据库中的数据库概念。它是一个包含文档的集合。每个索引都有一个名字,这个名字在进行搜索、更新、删除等操作时作为标识使用,其实也就是类似于数据库的database
  2. 文档(document) 文档是Elasticsearch中的基本信息单元,类似于关系型数据库中的行(row)。文档是以JSON格式存储的,每个文档包含一个或多个字段(field),字段是键值对的形式
import { Client } from '@elastic/elasticsearch';
const client = new Client({
    node: 'http://localhost:9200',
    auth: {
        username: 'elastic',
        password: '123456',
    },
});
//创建索引 + 数据
const user = await client.index({
    index: 'user-data',
    document: {
        user: 1,
        age: 18,
        name: 'jack',
    }
});
//查询数据
const response = await client.get({
    index: 'user-data',
    id:user._id //id可以指定也可以让elasticsearch自动生成
});
//搜索
const result = await client.search({
    index: 'user-data', //指定索引
    query: { //查询条件
        match: {
            name: 'jack' //模糊查询
        }
    },
    size: 1 //指定返回条数
});
console.log(result.hits.hits); //打印搜索结果
//删除
await client.delete({
    index: 'user-data',
    id: user._id
});

可视化


# 创建索引
PUT /task-index


# 添加文档到索引
POST /task-index/_doc
{
    "id": "1",
    "title": "不是哥们",
    "description": "杰哥不要啊杰哥??????????、啊"
}


# 搜索信息 q就是query的简写
GET /task-index/_search?q="杰哥"

搜索

模拟假数据

  1. 安装库@faker-js/faker模拟假数据的一个库非常好用支持中文

  2. 使用中文 locale: [zh_CN], 设置即可

  3. 生成名字,邮箱,手机号,id,年龄,性别

  4. 生成完成之后使用fs写入data.json文件

import { Faker, zh_CN, } from '@faker-js/faker'
const faker = new Faker({
    locale: [zh_CN],
})
const generate = (total = 100) => {
    let arr = []
    for (let i = 0; i < total; i++) {
        arr.push({
            name: faker.person.fullName(),
            email: faker.internet.email(),
            phone: faker.string.numeric({ length: 11 }),
            id: faker.string.uuid(),
            age: faker.number.int({ min: 18, max: 60 }),
            isMale: faker.datatype.boolean(),
        })
    }
    return arr
}

fs.writeFileSync('./data.json', JSON.stringify(generate(), null, 2))

假数据

 [{
    "name": "隐强",
    "email": "k7nggq88@126.com",
    "phone": "79533230542",
    "id": "945e80bb-9ece-428b-925c-1ed01e26d660",
    "age": 44,
    "isMale": true
  },
   ......]

Node.js集成ElasticSearch

  1. fs读取刚才写入的文件
  2. 安装ElasticSearch的包
  3. @elastic/elasticsearc 连接elastic 两种模式可以使用apiKey,也可以用账号密码的模式,这儿使用账号密码,生产使用apiKey
  4. 检查有没有创建过这个索引如果重复创建会报错
  5. 如果没有创建过这个索引就创建,并且构建映射表 也就是字段 properties
  6. 批量插入数据封装一个函数 bulkInsert
  7. 实现插入的函数 bulkInsert
  8. 搜索
//1.第一步
const data = fs.readFileSync('./data.json', 'utf-8')
const arr = JSON.parse(data)
//2.第二步
import { Client } from '@elastic/elasticsearch';
//3.第三步
const client = new Client({
    node: 'http://localhost:9200',
    auth: {
        username: 'elastic',
        password: '123456',
    },
});
//4.第四步
const exists = await client.indices.exists({ index: 'users' });
//5.第五步
if (!exists) {
    await client.indices.create({
        index: 'users',
        mappings: {
            properties: {
                name: { type: 'text', fields: { keyword: { type: 'keyword', } } },
                email: { type: 'text' },
                phone: { type: 'text' },
                id: { type: 'text' },
                age: { type: 'integer' },
                isMale: { type: 'boolean' },
            }
        }
    })
    //6.第六步
    await bulkInsert(arr);
}
//7.第七步
const bulkInsert = async (data) => {
    const operations = [];
    data.forEach((item) => {
        operations.push({
            index: {
                _index: 'users',
                _id: item.id
            },
        })
        operations.push(item)
    })
    //批量插入
    await client.bulk({ refresh: true, operations })
}
//8.搜索
const response = await client.search({
    index: 'users',
    query: {
        match_all: {},
    },
    size: 100
});
console.log(response.hits.hits);

全部查询

match_all 就是全部查询 注意默认只返回10条,你可以配置size看你想要返回的条数

const response = await client.search({
    index: 'users',
    query: {
        match_all: {}, //空对象即可
    },
    size: 100 //返回100条
});

  1. 模糊查询

模糊查询会进行分词,匹配所有的关键词

使用match进行模糊查询,输入需要匹配的字段如name 后面是 value 如 隐强 他会匹配数据中所有包含 隐强 这两个字的内容 我的数据中含有 隐强 蒋强 高启强 因此返回三条

const response = await client.search({
    index: 'users',
    query: {
        match: {
            name: '隐强'
        },
    },
    size: 100
});
console.log(response.hits.hits);

  1. 精确查询

如果需要支持精准查询 需要设置 name: { type: 'text', fields: { keyword: { type: 'keyword', } } }, 因为text类型默认会支持分词,为了全文搜索设计,但是如果要同时支持 全文匹配 + 精准匹配 需要设置 type keyword

注意这儿就不使用match了,改成term [字段.keyword] = [value] 查询

const response = await client.search({
    index: 'users',
    query: {
        term: {
            'name.keyword': '隐强'
        }
    },
    size: 100
});
console.log(response.hits.hits);

  1. 组合查询
  • must 必须匹配的条件 这儿匹配了(隐强)
  • filter 条件过滤 这儿匹配了年龄(20-60岁的人)
  • must_not 必须不匹配 (这儿表示返回的值不能有带国字的人)
  • should 可选的条件 (这儿匹配了隐强)
const response = await client.search({
    index: 'users',
    query: {
       bool:{
          must: {
             match: {
                name: '隐强'
             }
          },
          filter: {
             range: {
                age: {
                   gte: 20,
                   lte: 60
                }
             }
          },
          must_not: {
             match: {
                name: '国'
             }
          },
          should: {
             match: {
                name: '隐强'
             }
          }
       }
    },
    size: 100
});
console.log(response.hits.hits);

  1. 聚合查询

聚合查询在Elasticsearch中用来对数据进行统计、汇总和分析,它能够提供关于数据集的深入见解和洞察

案例 统计各个年龄出现的次数 注意使用 aggs 不再是 query 了

const response = await client.search({
    index: 'users',
    aggs: {
        age: {
            terms: {
                field: 'age'
            }
        }
    },
    size: 100
});
console.log(response.aggregations.age.buckets);

返回值

key:表示聚合的字段值,这里看起来是年龄。

doc_count:表示具有该年龄的文档数量。

集群

Node.js 本身是单线程的,集群模块提供了一种方式,通过使用多个进程来并行处理工作负载

实例

Node.js 提供了内置的 cluster 模块来创建和管理集群。使用这个模块,可以轻松地创建多个工作进程并共享同一个服务器端口

index.js

import cluster from 'node:cluster'
import http from 'node:http'
import os from 'node:os'
const cpus = os.cpus().length

// 主进程
if (cluster.isPrimary) {
    for (let i = 0; i < cpus; i++) {
        cluster.fork() // 创建子进程
    }
}
// 子进程
else {
   http.createServer((req, res) => {
        res.writeHead(200)
        res.end('cluster is running')
    }).listen(3000,()=>{
        console.log('http://127.0.0.1:3000')
    })
}

使用 ps node 查询进程

集群运行跟非集群运行对比

编写一个非集群的服务,index2.js 非集群运行

import http from 'node:http'

http.createServer((req, res) => {
    res.writeHead(200)
    res.end('cluster is running')
}).listen(6000,()=>{
    console.log('http://127.0.0.1:6000')
})

安装测试工具

npm install -g loadtest

执行命令

loadtest http://localhost:3000 -n 50000 -c 100
loadtest http://localhost:6000 -n 50000 -c 100

-c表示并发用户数或并发连接数。在这种情况下,-c 100表示在进行负载测试时,同时模拟100个并发用户或建立100个并发连接

-n表示总请求数或总请求数量。在这种情况下,-n 50000表示在进行负载测试时,将发送总共50000个请求到目标网站

部署pm2

Node.js如何部署?

如果要部署Nodejs项目,第一点肯定是需要有台服务器,第二点需要一个部署工具这里使用pm2

PM2

PM2 是一个非常流行的 Node.js 进程管理工具,比如监听文件改动自动重启,统一管理多个进程,内置的负载均衡,日志系统等

基本使用

1.安装

npm install pm2 -g

2.基本使用

随便创建一个服务 当然express koa nestjs也都是能用

import http from 'node:http'

http.createServer((req, res) => {
    res.writeHead(200)
    res.end('cluster is running')
}).listen(6000,()=>{
    console.log('http://127.0.0.1:6000')
})

  1. 启动一个服务 或者多个服务都是可以的
pm2 start app.js xx.js bb.js
  1. 查看当前正在运行的node进程
pm2 list

  1. 停止一个node进程
pm2 stop [process_id]

  1. 重启服务
pm2 restart [process_id]

  1. 删除服务
pm2 delete [process_id]
  1. 开机自启

linux

  • 先运行一个脚本如 pm2 start app.js
  • 保存进程信息 pm2 save
  • 生成启动脚本 pm2 startup
  • 开机自启命令 pm2 startup systemd
  • 保存自启命令 pm2 save
  • 删除自动启动 pm2 unstartup systemd
  • 保存删除启动 pm2 save

windows

  • 安装windows自动启动包 npm install pm2-windows-startup -g
  • 安装自启脚本 pm2-startup install
  • 启动服务 pm2 start xxxx
  • 保存自启服务 pm2 save
  • 删除自动启动 pm2-startup uninstall

3.日志

可以收集各种日志反馈调试问题

pm2 log

4.监控面板

可以实时监控所有由 PM2 管理的进程。这个监控面板提供了丰富的实时数据,包括 CPU 使用率、内存使用情况、重启次数、日志输出等信息

  • 实时监控:在开发和生产环境中实时监控应用程序的性能,及时发现和处理异常。

  • 调试和诊断:查看日志输出,帮助调试和诊断问题。

  • 资源管理:监控资源使用情况,优化应用程序的性能和资源分配

pm2 monit

5.负载均衡

当然pm2内部封装了集群的能力可以让我们的应用程序更加强大

pm2 start index.js -i [max | number] 

可以指定经线程数量,也可以设置max直接设置最高

6.配置文件

调用下面命令在项目中生成配置文件 ecosystem.config.js 或者手动创建也可以

pm2 init simple

ecosystem.config.js

apps: [{ 
    name: "my-app", 
    script: "./app.js", 
    instances: 4, 
    exec_mode: "cluster", 
    watch: true, max_memory_restart: "200M", 
    env: { NODE_ENV: "development", PORT: 3000 }, 
    env_production:{ NODE_ENV: "production", PORT: 8080 } 
}]

apps:一个包含应用程序配置对象的数组,每个对象代表一个应用程序。

name:应用程序名称,用于在 PM2 中标识

script:要启动的脚本文件路径

instances:实例数量,可以是具体数字或者 max,以利用所有可用的 CPU 核心

exec_mode:执行模式,常用值有 fork(默认)和 cluster

watch:启用文件监视,如果文件有变化,应用会自动重启

max_memory_restart:当内存使用超过指定值时自动重启应用

env:普通环境变量配置

env_production:生产环境变量配置,使用 pm2 start ecosystem.config.js --env production 命令启动时生效

启用配置文件

pm2 start ecosystem.config.json #这样就可以了 不用在单独指定js文件了

部署服务器操作也是一样的