NodeJS笔记-系列
初识NodeJS
Express框架
NodeJS-MongoDB
NodeJS接口、会话控制
Node-回眸[一]
Node-回眸[二]
Node-回眸[三]

http

http 模块用于创建HTTP服务器或客户端。

createServer()方法创建一个HTTP服务器,该方法返回http.Server实例。
http.request()http.get()用于创建HTTP客户端,返回http.ClientRequest实例。

创建HTTP服务器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const server = http.createServer()

server.on('request', (req, res) => {
if(req.method === 'GET'){
res.write('GET:')
res.end('hello world')
}else if(req.method === 'POST'){
req.on('data', (chunk) => {
console.log(chunk.toString());
})
res.write('POST:')
res.end('hello world')
}
})

server.listen(8888, () => {
console.log('Server is running on http://localhost:8888');
});
创建HTTP客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
http.get('http://localhost:8888', (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
})
res.on('end', () => {
console.log(data.toString()); // GET:hello world
console.log('已获取完毕');
})
})

const req = http.request({
host: 'localhost',
port: 8888,
path: '/',
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
}, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
})
res.on('end', () => {
console.log(data.toString()); // POST:hello world
console.log('已获取完毕');
})
})
req.write(JSON.stringify({"name": "tom"}));
req.end(); // 结束请求

更多基本用法,之前已经记录过了:初识NodeJS—http模块

反向代理

直接访问:浏览器直接请求最终提供资源的服务器,中间没有经过任何的 http 代理服务器。
正向代理(forward proxy):代理的对象是客户端,代理服务器代替客户端(浏览器)去访问目标服务器,获取资源返回,并不直接面向最终提供资源的服务器。代理服务器可以对外隐藏客户端细节,代理服务器会对外屏蔽掉客户端信息。通常需要客户端主动配置代理服务器的信息。
反向代理(reverse proxy):代理的对象是服务端,客户端(浏览器)不需要任何配置。反向代理服务器代替服务端(服务器)接收请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给客户端,此时反向代理服务器和普通的服务器对外就是一个服务器,客户端不需要知道内部网络的信息。也称为透明代理

反向代理作用:

  1. 保证内网的安全:防止外部网络直接访问内网
  2. 负载均衡:将请求分发到多个后端服务器上,避免某个服务器过载,提高整体性能和可用性
  3. 缓存和性能优化:将请求的结果和静态资源缓存起来,减少对服务器的请求,还可以资源进行压缩、合并、优化等操作
  4. 高可用:当某个服务器出现故障时,可以自动将请求转发到其他服务器上
  5. 域名和路径重写:将请求的域名和路径重写,转发到其他服务器上,提高了灵活性和可维护性

通过http模块可以直接实现反代,但通常用第三方库,例如:http-proxy-middleware

先写好一个简单的后端服务器:

server.js
1
2
3
4
5
http.createServer((req, res) => {
res.end('101010101')
}).listen(8888, () => {
console.log('Server is running on http://localhost:8888');
})

写好反代规则:

config.js
1
2
3
4
5
6
7
8
9
10
11
module.exports = {
server: {
proxy: {
//代理的路径
'/api': {
target: 'http://localhost:8888', //转发的地址
changeOrigin: true, //是否有跨域
}
}
}
}

再配置反向代理服务器:

proxy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const http = require('http')
const { createProxyMiddleware } = require('http-proxy-middleware')
const url = require('url')

const config = require('./config')
const proxyList = config.server.proxy

http.createServer((req, res) => {
const pathname = url.parse(req.url).pathname;
// 判断是否需要代理
if (Object.keys(proxyList).includes(pathname)) {
// 创建代理对象并转发请求
const proxy = createProxyMiddleware(config.server.proxy[pathname]);
proxy(req, res);
return;
}
res.end('hello world')
}).listen(80, () => {
console.log('Server is running on http://localhost:80');
})

结果:

1
2
3
[HPM] Proxy created: /  -> http://localhost:8888
请求 http://localhost/ 返回 hello world
请求 http://localhost/api 返回 101010101

动静分离

动静分离是指将动态资源和静态资源分开处理和分发,以提高服务器的性能和安全性。

动态资源:指每次请求时动态生成的资源,例如:API接口、动态页面等。
静态资源:指长时间不会变化的资源,例如:html、css、js、图片、视频、音频等。

静态资源通常使用GET请求获取,mime模块可以根据文件后缀名获取MIME类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import http from 'http';
import fs from 'fs';
import url from 'url';
import path from 'path';
import mime from 'mime';

const __filename = url.fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

http.createServer((req, res) => {
const { pathname } = url.parse(req.url, true);
if (req.method === 'GET') {

// 请求静态资源目录
if (req.url.startsWith("/public")) {
const filePath = path.normalize(__dirname + pathname);
console.log(filePath)
if (!fs.existsSync(filePath)) {
res.statusCode = 404;
res.end('404');
return;
}
res.writeHead(200, {
// 根据资源类型设置MIME类型
'Content-Type': mime.getType(pathname)
})
return fs.createReadStream(filePath).pipe(res);

} else {
return res.end('GET:hello world')
}
} else if (req.method === 'POST') {
return res.end('POST:hello world')
}
res.statusCode = 404;
return res.end('404');
}).listen(8888, () => {
console.log('Server is running on http://localhost:8888');
})

常见MIME类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
-   文本文件:
- text/plain:纯文本文件
- text/html:HTML 文件
- text/css:CSS 样式表文件
- text/javascript:JavaScript 文件
- application/json:JSON 数据

- 图像文件:
- image/jpeg:JPEG 图像
- image/png:PNG 图像
- image/gif:GIF 图像
- image/svg+xml:SVG 图像

- 音频文件:
- audio/mpeg:MPEG 音频
- audio/wav:WAV 音频
- audio/midi:MIDI 音频

- 视频文件:
- video/mp4:MP4 视频
- video/mpeg:MPEG 视频
- video/quicktime:QuickTime 视频

- 应用程序文件:
- application/pdf:PDF 文件
- application/zip:ZIP 压缩文件
- application/x-www-form-urlencoded:表单提交数据
- multipart/form-data:多部分表单数据

邮件服务

nodemailer是一个简单易用的NodeJS邮件发送库。文档

QQ邮件服务文档,需开启POP3/SMTP,并生成授权码。

createTransport() 创建邮件传输器对象,transport.sendMail() 发送邮件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import nodemailer from 'nodemailer'
import http from 'http'
import url from 'url'
import email from './email.json' assert { type: "json" }

const transport = nodemailer.createTransport({
service: "qq", // 邮箱服务商
port: 587, // SMTP 端口
host: 'smtp.qq.com', // QQ邮箱的SMTP地址
secure: true, // 当设置为 true 时,表示使用安全连接(Secure Connection),
// 通常是通过 TLS 或 SSL 协议来加密邮件传输。
auth: {
user: email.user, // 邮箱账号
pass: email.pass // 邮箱授权码 | 密码
}
})

http.createServer((req, res) => {
const { pathname } = url.parse(req.url, true);
if (req.method === 'POST' && pathname === '/email') {
let data = '';
req.on('data', (chunk) => {
data += chunk.toString();
})
req.on('end', () => {
const { to, subject, text } = JSON.parse(data);
transport.sendMail({
from: email.user,
to,
subject,
text
}).then(() => {
res.end('发送成功')
}).catch((err) => {
res.end('发送失败')
})
})
res.end('hello world')
} else {
res.statusCode = 404;
res.end('404');
}
}).listen(8888, () => {
console.log('Server is running on http://localhost:8888');
})

express

使用http模块写服务端太麻烦了,所以有了express等 web 应用框架。

express是一个最小且灵活的 Node.js Web 应用程序框架,提供了路由、中间件等功能,支持模板引擎。

基本用法之前已经记录过了:Express框架

快速回顾
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import express from 'express'
import path from 'path'
import url from 'url'

const __filename = url.fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)

// 创建一个 express 实例
const app = express()

// get 请求接口
app.get('/txt', (req, res) => {
//express兼容http原生操作
console.log(req.method);//请求方法
console.log(req.url);//请求url
console.log(req.httpVersion);//http版本
console.log(req.headers);//请求头
//express封装方法
console.log(req.path);//请求路径
console.log(req.query);//查询字符串
console.log(req.ip);//用户ip
console.log(req.get('host'));//获取特定请求头的属性值
// 响应文本
res.send('hello get')
})

// 路由参数
app.get('/user/:id', (req, res) => {
console.log(req.params.id)
res.send(req.params.id)
})

// 设置静态资源目录,会自动添加Mime类型
app.use(express.static(path.resolve(__dirname, 'public')))
// 一般用路由响应动态资源,如搜索结果等,用静态资源中间件响应静态资源,如html、css等

// 解析请求体需要使用中间件
app.use(express.json()) // 解析 json 格式的请求体
app.use(express.urlencoded({ extended: true })) // 解析表单格式的请求体

// post 请求接口
app.post('/login', (req, res) => {
console.log(req.body)
res.send('hello post')
})

// 创建中间件,本质上是一个回调函数
const logger = (req, res, next) => {
console.log('logger')
next()
}

// 创建路由,每个路由都相当于一个小的 express 实例
const router = express.Router();

// 使用中间件
router.get('/list', logger, (req, res) => {
// 响应 json 数据
res.json({
code: 0,
msg: 'ok',
data: [1, 2, 3]
})
})

// 使用路由,指定路由前缀
app.use('/api', router)

// 监听端口,启动服务
app.listen(8888, () => {
console.log('Server is running on http://localhost:8888');
})

log4js

node服务端常用的日志收集工具:winstonlog4jsbunyannpmlog

log4js 较为灵活、配置多样但轻量,可以将日志输出到文件、控制台、邮件等。文档

log4js.configure() 配置日志记录器、分类,log4js.getLogger() 获取指定分类的日志记录器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import express from 'express'
import log4js from 'log4js'

// 配置日志记录器
log4js.configure({
appenders: {
// 控制台输出
out: {
type: "stdout",
layout: {
type: "colored"
}
},
// 日志文件
file: {
type: "file",
// 日志文件路径
filename: "server.log"
}
},
// 日志分类
categories: {
// 默认分类
default: {
// 输出方式:控制台、日志文件
appenders: ["out", "file"],
// 只有日志等级大于或等于当前配置的日志等级时,才会输出
level: "debug"
}
},
});
// 获取default分类的日志记录器
const logger = log4js.getLogger("default")

const app = express()

const LoggerMiddleware = (req, res, next) => {
logger.debug(`[${req.method}] ${req.url}`)
// [2024-01-19T22:11:10.053] [DEBUG] default - [GET] /
next()
}

// 全局使用中间件
app.use(LoggerMiddleware)

app.get('*', (req, res) => {
res.send('hello world')
})

app.listen(8888, () => {
console.log('Server is running on http://localhost:8888');
})

日志分级Level

日志分级可以将不同级别的日志在控制台中采用不同的颜色,也可以帮助在生产时有选择地生成不同级别的日志。

log4js默认的日志分级有9级,也可以自定义级别。

默认日志级别,value为优先级,从低到高
1
2
3
4
5
6
7
8
9
10
11
Level.addLevels({
ALL: { value: Number.MIN_VALUE, colour: 'grey' },
TRACE: { value: 5000, colour: 'blue' },
DEBUG: { value: 10000, colour: 'cyan' },
INFO: { value: 20000, colour: 'green' },
WARN: { value: 30000, colour: 'yellow' },
ERROR: { value: 40000, colour: 'red' },
FATAL: { value: 50000, colour: 'magenta' },
MARK: { value: 9007199254740992, colour: 'grey' }, // 2^53
OFF: { value: Number.MAX_VALUE, colour: 'grey' },
});

categories.default.level中配置日志等级,只会输出级别相等或更高级别的日志。

ALL OFF 这两个等级并不会直接在业务代码中使用。其它级别分别对应 Logger 实例的七个方法。在调用这些方法的时候,就相当于为这些日志定了级。

  1. logger.trace() 详细信息
  2. logger.debug() 调试信息
  3. logger.info() 一般信息
  4. logger.warn() 警告信息
  5. logger.error() 错误信息
  6. logger.fatal() 致命信息
  7. logger.mark() 标记信息

自定义级别:
levels属性用于定义自定义日志级别,或重新定义现有日志级别

1
2
3
4
5
6
7
8
log4js.configure({
levels: {
custom: {
value: 10000, // 数值越大优先级越高
colour: "blue", // 代表的颜色
}
},
});

日志分类Category

除了日志分级,还可以对日志进行分类Category,两个维度对日志进行区分。

log4js.configure(config) 传入配置对象,用于配置日志记录器、分类、输出方式等。config下有两个属性:

  1. appenders:用于配置日志输出方式,可以配置多个输出方式,例如:控制台、日志文件、邮件等。
  2. categories:用于配置日志分类,每个分类可以设置多个appender,也可以配置日志等级。

log4js.getLogger(category)获取指定分类的日志记录器。

  1. 若不传入,则会使用default分类配置。
  2. 若传入不存在的分类名,也使用default,但输出时仍然是传入的分类名。
配置分类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

log4js.configure({
appenders: {
console: {
type: "console",
},
out: {
type: "stdout",
},
err: {
type: "stderr",
},
},
categories: {
// 必须配置名为default的分类,否则会报错
default: {
// 该分类使用的输出方式
appenders: ["console"],
// 只有日志等级大于或等于当前配置的日志等级时,才会输出
// 可输出的等级:debug、info、warn、error、fatal、mark
level: "debug", // 不区分大小写
},
access: {
appenders: ["out"],
level: "info",
},
error: {
appenders: ["err"],
level: "error",
// 输出调用栈信息,默认为false
enableCallStack: true,
},
}
})
使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 获取default日志记录器
const defaultLogger = log4js.getLogger() // 不指定分类,默认为default
defaultLogger.debug("Time:", new Date());
// 蓝色 [2024-01-20T12:00:11.508] [DEBUG] default - Time: 2024-01-20T04:00:11.507Z

// 指定不存在的分类,会使用default分类,但输出时会保留传入的分类名
const otherLogger = log4js.getLogger("other")
otherLogger.debug("Time:", new Date());
// [2024-01-20T12:02:43.564] [DEBUG] other - Time: 2024-01-20T04:02:43.564Z

// 获取access日志记录器
const accessLogger = log4js.getLogger("access")
accessLogger.debug("Time:", new Date()); // 不会输出,因为debug等级低于info
accessLogger.info("Time:", new Date());
// 绿色 [2024-01-20T12:01:09.590] [INFO] access - Time: 2024-01-20T04:01:09.590Z

// 获取error日志记录器
const errorLogger = log4js.getLogger("error")
errorLogger.error("Time:", new Error("error"));
// 红色 [2024-01-20T12:01:52.282] [ERROR] error - Time: 2024-01-20T04:01:52.282Z
// at file:///c:/chuckle/qx/NodeJS-new/express/%E6%97%A5%E5%BF%97%E5%88%86%E7%B1%BB.mjs:53:28
// at ModuleJob.run (node:internal/modules/esm/module_job:218:25)
// at async ModuleLoader.import (node:internal/modules/esm/loader:329:24)
// at async loadESM (node:internal/process/esm_loader:28:7)
// at async handleMainPromise (node:internal/modules/run_main:113:12)

日志落盘Appender

Appender用于配置日志输出方式

默认Appenders:若不进行任何配置,日志将输出到标准输出,但也定义了默认日志级别OFF,这意味着实际上不会输出任何日志。

自定义Appenders:传入一个对象,key为Appender名,value为Appender配置。

常用的Appender.type:

  1. console:通过V8的console输出到控制台
  2. stdout:通过标准输出到控制台
  3. stderr:通过标准错误输出到控制台
  4. file:输出为指定日志文件
  5. dateFile:按照特定日期滚动生成多个日志文件
  6. multiFile 动态配置不同category下或者不同变量控制下,落盘到不同文件
  7. smtp 将日志输出到邮件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
log4js.configure({
// 配置多个日志输出方式
appenders: {
out: { // Appender名
// 控制台输出
type: "stdout", // console | stdout | stderr
// 通过 layout 可以自定义日志输出的格式
// 所有appender都可以配置 layout
layout: {
type: "colored" // 给日志加上颜色,默认使用
// basic [默认] 在日志的内容前面会加上时间、日志的级别和类别
// messagePassThrough 仅格式化日志事件数据,不输出时间戳、级别或类别
// colored 在 basic 的基础上给日志加上颜色
// pattern 自定义格式化字符串,使用 %r 替换日志级别,%p 替换日志类别,%c 替换日志内容等
}
},
file: {
// 输出为日志文件
type: "file",
// 日志文件路径,相对于当前工作区
filename: "server.log",
// 单个日志文件最大大小,单位字节,如果未指定或为 0,则不会发生日志滚动。
maxLogSize: 1024 * 1024 * 10, // 10MB,默认为未定义
// 日志滚动期间要保留的旧日志文件的数量
backups: 3, // 默认为 5
/* 下面还有一些流的配置 */
// 文件编码
encoding: "utf-8", // 默认为 "utf-8"
// 使用 gzip 压缩备份文件(备份文件将具有.gz扩展名)
compress: true, // 默认为 false
// 滚动日志文件时保留文件扩展名
// file.log变为file.1.log而不是file.log.1
keepFileExt: true, // 默认为 false
// 滚动时的文件名分隔符
fileNameSep: "-", // 设置为file-1.log,默认为"."
// 从 log4js 版本 4.x 开始,file也可以采用dateFile的任何选项。因此可以同时按日期和大小滚动文件。
},
date: {
type: "dateFile",
filename: "app.log",
// 用于确定何时滚动日志的模式,同时也是日志文件名的一部分
pattern: "yyyy-MM-dd", // 默认为yyyy-MM-dd
/* 还有一些流的配置,和file差不多 */
},
multi: {
type: "multiFile", // 分割日志,多文件输出
base: "logs/", // 日志文件的基本路径
// 用于分割文件的值
property: "categoryName", // 按日志类别分割
extension: ".log", // 日志文件的扩展名
// 可选活动超时(毫秒),之后文件将被关闭。
timeout: 0,
// multiFile在底层使用file因此它还可以使用file的几乎所有选项
},
error: {
// 日志等级过滤器
type: "logLevelFilter",
// 使用的日志记录器
appender: "file",
// 只有日志等级大于或等于当前配置的日志等级时,才会输出
// 即只输出error以上的日志到file
level: "error",
},
},
// 日志分类
categories: {
// 默认分类
default: {
// debug以上的日志输出到out,其中error还会输出到文件(这对于将错误输出到邮件很有用)
appenders: ["out", "error"],
// 只有日志等级大于或等于当前配置的日志等级时,才会输出
level: "debug"
},
// date分类
date: {
appenders: ["out", "date"],
level: "debug"
},
// multi分类
multi: {
appenders: ["out", "multi"],
level: "debug"
},
},
});

// 获取default分类的日志记录器
const defaultLogger = log4js.getLogger()
// 获取date分类的日志记录器
const dateLogger = log4js.getLogger('date')
// 获取multi分类的日志记录器
const multiLogger = log4js.getLogger('multi')
// 输出日志
defaultLogger.debug('Time:', new Date()) // 只输出到控制台
defaultLogger.error('Time:', new Error('error')) // 输出到控制台和server.log
dateLogger.debug('Time:', new Date()) // app.log
multiLogger.debug('Time:', new Date()) // logs/multi.log

对于日志滚动和日期日志,先创建一个原名日志文件,当发生滚动或日期变更时,重命名该文件为原名+序号原名+日期,然后创建一个新的原名日志文件继续写入,依次循环。

其它接口

log4js.isConfigured() 返回boolean,表示log4js.configure()先前是否成功调用(getLogger()也会隐式调用configure())

1
2
3
console.log(log4js.isConfigured()) // false
log4js.getLogger() // 隐式调用configure()
console.log(log4js.isConfigured()) // true

log4js.shutdown(cb) 接受一个回调,当 log4js 关闭所有附加程序并完成写入日志事件时将调用该回调。当程序退出时使用此选项,以确保所有日志都写入文件、套接字已关闭等。

1
2
3
log4js.shutdown(() => {
console.log('log4js shutdown');
})

log4js.addLayout(type, fn) 添加自定义布局,type为布局名,fn为布局处理函数。详见

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
log4js.addLayout("json", function (config) {
return function (logEvent) {
return JSON.stringify(logEvent) + config.separator;
};
});
log4js.configure({
appenders: {
out: { type: "stdout", layout: { type: "json", separator: "," } },
},
categories: {
default: { appenders: ["out"], level: "info" },
},
});
const logger = log4js.getLogger("json-test");
logger.info("this is just a test");
// {"startTime":"2024-01-20T05:40:05.325Z","categoryName":"json-test","data":["this is just a test"],
// "level":{"level":20000,"levelStr":"INFO","colour":"green"},"context":{},"pid":22684},

HTTP标头

HTTP 标头(header)允许客户端和服务器通过 HTTP 请求(request)或者响应(response)传递附加信息

HTTP 标头由它的名称(不区分大小写)后跟随一个冒号(:),冒号后跟随它具体的值。该值之前的空格会被忽略。

根据不同的消息上下文,标头可以分为:

  1. 请求标头包含有关要获取的资源或客户端或请求资源的客户端的更多信息。
  2. 响应标头包含有关响应的额外信息,例如响应的位置或者提供响应的服务器。
  3. 表示标头包含资源主体的信息,例如主体的 MIME 类型或者应用的编码/压缩方案。
  4. 有效负荷标头包含有关有效载荷数据表示的单独信息,包括内容长度和用于传输的编码。

请求表头

HTTP请求头(HTTP request headers)是在HTTP请求中发送的元数据信息,用于描述请求的特性、内容和行为。

常见请求头:

  1. Accept:客户端能够处理的媒体类型,值:text/htmlapplication/json
  2. Accept-Encoding:客户端能够处理的编码方式,值:gzipdeflate
  3. Accept-Language:客户端能够处理的语言,值:zh-CNen-US
  4. Connection:连接方式,值:keep-aliveclose
  5. Content-Type:请求体的MIME类型,值:text/htmlapplication/json
  6. Content-Length 请求体的长度,单位字节
  7. Host:请求的主机名,值:www.baidu.comlocalhost:8888
  8. Referer:请求来源,值:https://www.baidu.comhttp://localhost:8888
  9. User-Agent:客户端信息,值:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.61 Safari/537.36
  10. Cache-Control 缓存控制,值:no-cachemax-age=3600
  11. Cookie 请求携带的cookie,值:name=tom;
  12. Range 用于断点续传,值:bytes=0-1023等,指定第一个字节的位置和最后一个字节的位置
  13. Sec-Ch-Ua 浏览器的 UA 字符串,值:"Google Chrome";v="120"
  14. Sec-Ch-Ua-Mobile 浏览器是否为移动端,值:?0?1
  15. Sec-Fetch-Dest 请求的目标资源类型,值:documentimage
  16. Sec-Fetch-Mode 请求的模式,值:navigatecors
  17. Sec-Fetch-Site 请求的站点类型,值:same-origincross-site
  18. Sec-Ch-Ua-Platform 浏览器的平台,值:"Windows""macOS"

响应标头

HTTP响应头(HTTP response headers)是在HTTP响应中发送的元数据信息,用于描述响应的特性、内容和行为。

常见响应头:

  1. Content-Type:响应体的MIME类型,值:text/htmlapplication/json
  2. Content-Length:响应体的长度,单位字节
  3. Content-Encoding:响应体的编码方式,值:gzipdeflate
  4. Content-Language:响应体的语言,值:zh-CNen-US
  5. Content-Disposition:响应体的附加信息,值:attachment; filename="filename.jpg",表示附件,浏览器会提示下载
  6. Location:重定向的地址,值:https://www.baidu.com
  7. Set-Cookie:设置cookie,值:name=tom;
  8. Cache-Control:缓存控制,值:no-cachemax-age=3600
  9. Expires:缓存过期时间,值:Wed, 21 Oct 2024 07:28:00 GMT
  10. Last-Modified:资源最后修改时间,值:Wed, 21 Oct 2024 07:28:00 GMT
  11. Etag:资源的唯一标识,例如:"5d8b9b4e-2a",用于缓存验证
  12. Access-Control-Allow-Origin:允许跨域的域名,值:*http://localhost:8080
  13. Access-Control-Allow-Methods:允许跨域的请求方法,值:GETPOST
  14. Access-Control-Allow-Headers:允许跨域的请求头,值:Content-TypeAuthorization
  15. Access-Control-Allow-Credentials:是否允许跨域携带cookie,值:truefalse
  16. Access-Control-Max-Age:预检请求的有效期,单位秒,值:3600
  17. Access-Control-Expose-Headers:允许跨域的响应头,值:Content-TypeAuthorization等,若请求没有携带凭据(Cookie或认证信息),*才会被当作一个特殊的通配符,否则会被简单地当作标头名称。默认情况下,仅暴露CORS安全列表的响应标头
  18. Strict-Transport-Security:强制使用HTTPS,值:max-age=31536000; includeSubDomains,表示一年内所有子域名都必须使用HTTPS
  19. X-Frame-Options:防止网页被嵌入到iframe中,值:DENYSAMEORIGIN
  20. X-XSS-Protection:防止XSS攻击,值:1; mode=block0
  21. X-Content-Type-Options:防止MIME类型被修改,值:nosniff
  22. Referrer-Policy:控制referer的发送,值:no-referrerstrict-origin-when-cross-origin
  23. Content-Security-Policy:控制资源加载,值:default-src 'self'script-src 'self'
  24. X-Powered-By:服务器信息,值:ExpressPHP/7.4.3
  25. Server:服务器信息,值:nginx/1.18.0Apache/2.4.46
  26. Date:响应时间,值:Wed, 21 Oct 2024 07:28:00 GMT
  27. Connection:连接方式,值:keep-aliveclose
  28. Transfer-Encoding:传输编码,值:chunkedgzip
  29. Vary:缓存策略,值:Accept-EncodingUser-Agent

CORS

跨域资源共享(Cross-Origin Resource Sharing,CORS)用于在浏览器中实现跨域请求访问资源的权限控制。

当一个网页发起跨域请求时,浏览器会根据同源策略(Same-Origin Policy)进行限制。同源策略要求请求的源(协议、域名和端口)必须与资源的源相同,否则请求会被浏览器拒绝。

AJAX请求相关-跨域

express设置CORS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 请求来源白名单
const allowedOrigins = ["127.0.0.1:5500", undefined];
app.use((req, res, next) => {
const origin = req.headers.origin;
// 判断请求来源是否在白名单内
if (allowedOrigins.some(item => origin.includes(item))) {
// 设置允许跨域的域名,*代表允许任意域名跨域
res.header('Access-Control-Allow-Origin', origin);
// 允许的header类型,如下设置允许自定义header、允许Content-Type为非默认值等,按需删改
res.header('Access-Control-Allow-Headers', "*, Origin, X-Requested-With, Content-Type, Accept, Authorization");
// 跨域允许的请求方式
res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, PATCH ,OPTIONS');
// 跨域的时候是否携带cookie
// 需要与 XMLHttpRequest.withCredentials 或 Fetch API 的 Request() 构造函数中的 credentials 选项结合使用
res.header("Access-Control-Allow-Credentials", true);
if (req.method.toLowerCase() == 'options') {
res.send(200); // 让options请求快速结束
}
else {
next();
}
} else {
res.status(403).send('Forbidden');
}
})

跨域使用Cookie

  1. 服务端Access-Control-Allow-Credentials设为 true
  2. 客户端请求时,设置XMLHttpRequest.withCredentialstrue 或 Fetch API 的 credentials 选项为 include
1
2
3
4
5
fetch('http://127.0.0.1:8888/api', {
method: 'GET',
credentials: 'include', // 跨域允许携带Cookie
}).then(res => res.text())
.then(data => console.log(data))

注意:此时服务端不能设置Access-Control-Allow-Origin*,必须指定具体的域名,否则会报CORS错误

1
2
index.html:1 Access to fetch at 'http://127.0.0.1:8888/api' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy: 
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'.

Cookie属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
1. 名称:Cookie的name。
2. 值:Cookie的value。
3. Domain: Cookie的域。如果设成xxx.com(一级域名),那么子域名x.xxx.com(二级域名),都可以使用xxx.com的Cookie。
4. Path:Cookie的路径。如果设为/,则同域名全部路径均可使用该Cookie。如果设为/xxx/,则只有路径为/xxx/可以使用该Cookie。
5. Expires / Max-Age:Cookie的超时时间。如果值为时间,则在到达指定时间后Cookie失效。如果值为Session(会话),Cookie会同Session一起失效,当整个浏览器关闭的时候Cookie失效。
6. Size:Cookie的大小。
7. HttpOnly:值为true时,Cookie只会在Http请求头中存在,不能通过doucment.cookie(JavaScript)访问Cookie。
8. Secure:值为true时,只能通过https来传输Cookie。
9. SameSite:
值为Strict,完全禁止第三方Cookie,跨站时无法使用Cookie。
值为Lax,允许在跨站时使用Get请求携带Cookie,下面有一个表格介绍Lax的Cookie使用情况。
值为None,允许跨站跨域使用Cookie,前提是将Secure属性设置为true。
10. Priority :Cookie的优先级。值为Low/Medium/High,当Cookie数量超出时,低优先级的Cookie会被优先清除。

一些额外的情况,可能还需要进行如下操作,才能跨域使用Cookie:
将Cookie的SameSite值设为None,Secure值改为true,并且使用https。

自定义响应头

默认情况下,仅暴露CORS安全列表的响应标头

其它响应头包括自定义的,需要使用 Access-Control-Expose-Headers 配置。

只有当请求没有携带凭据(Cookie或认证信息),*才会被当作一个特殊的通配符,否则会被简单地当作标头名称。

预检请求

预检请求(preflight request)是浏览器在发送真正的请求之前,先发送一个OPTIONS请求,用于检查服务器是否支持 CORS 即跨域资源共享。

存在以下情况时,浏览器会发送预检请求:

  1. 使用了自定义的请求头(非简单请求)
  2. 使用了非简单请求方法(PUT、DELETE等)
  3. Content-Type不是application/x-www-form-urlencoded、multipart/form-data、text/plain之一

Access-Control-Allow-Headers 如下设置允许自定义header、允许Content-Type为非默认值等:
"*, Origin, X-Requested-With, Content-Type, Accept, Authorization"

SSE单工通信

Server-Sent Events(SSE)服务器发送事件,也称为事件流,用于服务器向客户端单向实时、持续地发送消息。属于单工通信。可以使用自定义事件(Custom Events)来发送具有特定类型的事件数据。

SSE 基于 HTTP 协议,利用了其长连接特性,在客户端与服务器之间建立一条持久化连接,并通过这条连接实现服务器向客户端的单向实时数据推送。

Server-Sent Events 教程-阮一峰

使用SSE需要设置响应头:

1
2
3
res.set('Content-Type', 'text/event-stream');
res.set('Cache-Control', 'no-cache');
res.set('Connection', 'keep-alive');

一个后端示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
app.get('/sse', (req, res) => {
// 使用SSE必要的响应头
res.set('Content-Type', 'text/event-stream');
res.set('Cache-Control', 'no-cache');
res.set('Connection', 'keep-alive');
let i = 1;
// 模拟实时发送数据给客户端
setInterval(() => {
// events: 自定义事件类型名,默认为 message
res.write('event: test\n'); // 消息内部使用\n分隔
// id: 事件ID,也可以是每一条数据的编号
res.write(`id: ${i++}\n`); // 别漏了\n
// retry: 重连时间间隔
res.write('retry: 3000\n'); // 出错时的重连时间间隔,如网络错误导致连接中断
// data: 该事件附带的数据
res.write(`data: ${JSON.stringify({
time: new Date().toLocaleString(),
})}\n\n`); // 消息之间以\n\n分隔
}, 1000);
});

res.write发送消息:
每一次发送的信息,由若干个message组成,每个message之间用\n\n分隔。
每个message内部由若干行组成,每行格式:[field]: value\n,每行之间用\n分隔。

[field]可以取四个值:

  1. event:事件类型名,默认为 message。
  2. id:事件的ID,也可以是每一条数据的编号。
  3. retry:出错的重连时间间隔,单位毫秒。
  4. data:事件的数据,必须是字符串,如果数据有多行,使用\n分隔。

此外,还可以有冒号开头的行,表示注释。通常,服务器每隔一段时间就会向浏览器发送一个注释,保持连接不中断。
: This is a comment\n

id的作用:
每个message都应该有一个独特的id,浏览器用lastEventId属性读取id的值。一旦连接断线,浏览器会发送一个 HTTP 头,里面包含一个特殊的Last-Event-ID头信息,将这个值发送回来,用来帮助服务器端重建连接。因此,这个头信息可以被视为一种同步机制。

EventSource

前端使用EventSource(url, options)创建SSE对象,监听对应事件,获得服务端响应的数据。

本质是发送了一次GET请求,但是不会关闭连接,而是保持连接,等待服务端推送数据。

options 配置项:

  1. withCredentials Boolean,是否允许发送 Cookie 和 HTTP 认证信息。默认为 false。
  2. headers Object,要发送的请求头信息。
  3. retryInterval Number,与服务器失去连接后,重新连接的时间间隔。默认为 1000 毫秒。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const sse = new EventSource('http://localhost:8888/sse', {
withCredentials: true // 允许跨域携带cookie
});
console.log('连接中', sse.readyState); // 连接中 0
sse.addEventListener('open', () => {
console.log('连接已建立', sse.readyState); // 连接已建立 1
});
sse.addEventListener('test', (e) => {
console.log(e.type); // 事件名 test
console.log(e.lastEventId); // 当前该事件的ID
console.log(JSON.parse(e.data)); // 事件数据
// {time: '2024/1/21 22:54:03'}
});
setTimeout(() => {
sse.close(); // 关闭连接
console.log('连接已关闭', sse.readyState); // 连接已关闭 2
}, 4000);

EventSource.readyState,表明连接的当前状态。

  1. 0:相当于常量EventSource.CONNECTING,表示连接还未建立,或者断线正在重连。
  2. 1:相当于常量EventSource.OPEN,表示连接已经建立,可以接受数据。
  3. 2:相当于常量EventSource.CLOSED,表示连接已断,且不会重连。

事件:

  1. open EventSource 对象已经和服务器建立了连接,并开始接收来自服务器的数据,此时 readyState 为 1。
  2. error 在建立连接或接收服务器数据时发生了错误。
  3. message 服务器发送了一个没有指定事件名的消息,此时 event.type 为 message。
  4. 自定义事件名:服务器发送了一个指定事件名的消息,此时 event.type 为自定义事件名。

WebSocket双工通信

WebSocket 是建立在单个 TCP 连接上的全双工通信协议,允许客户端和服务器之间进行实时双向通信。

WebSocket 教程-阮一峰
你不知道的 WebSocket-阿宝哥

特点:

  1. 建立在 TCP 协议之上,服务器端的实现比较容易。
  2. 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  3. 数据格式比较轻量,性能开销小,通信高效。
  4. 有状态的协议,之后通信时可以省略部分状态信息
  5. 可以发送文本,也可以发送二进制数据。
  6. 没有同源限制,客户端可以与任意服务器通信。
  7. 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

客户端

WebSocket 对象用于创建 WebSocket 连接,通过该连接可以发送和接收数据。

1
2
3
4
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => {
console.log('Connected');
};

readyState 属性

  1. 0:相当于常量WebSocket.CONNECTING,表示连接还未建立,或者断线正在重连。
  2. 1:相当于常量WebSocket.OPEN,表示连接已经建立,可以通讯。
  3. 2:相当于常量WebSocket.CLOSING,表示连接正在关闭。
  4. 3:相当于常量WebSocket.CLOSED,表示连接已断,且不会重连。

事件:

  1. open WebSocket 连接已建立。
  2. message 接收到服务器发送的数据。
  3. error 连接出错。
  4. close 连接关闭。

方法:

  1. send(data) 向服务器发送数据。
  2. close() 关闭连接。

请求和响应头

1、请求头:

  1. Upgrade 指定为websocket,表示要升级协议为 WebSocket(HTTP 协议提供了一种特殊的机制,这一机制允许将一个已建立的连接升级成新的、不相容的协议)
  2. Connection 指定为Upgrade,表示要升级连接。
  3. Sec-WebSocket-Key Base64编码的16字节随机字符串,用于验证服务器是否正确处理了握手请求。
  4. Sec-WebSocket-Version WebSocket协议的版本号,当前为13。
  5. Sec-WebSocket-Extensions 指定扩展,如压缩等。
1
2
3
4
5
6
7
8
9
10
11
12
GET ws://localhost:8080/ HTTP/1.1
Host: localhost:8080
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://127.0.0.1:5500
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: zh-CN,zh;q=0.9
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: iXGVJiUgpjqmAIMXKj592w==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

2、响应头:

  1. 101 Switching Protocols 101 状态码,表示协议切换成功。
  2. Upgrade 指定为websocket,表示要升级协议为 WebSocket。
  3. Connection 指定为Upgrade,表示要升级连接。
  4. Sec-WebSocket-Accept 通过ws协议指定算法处理后的Sec-WebSocket-Key,表示ws已握手成功,后续消息收发使用ws协议。
1
2
3
4
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: Ifs+JScoZK0aP3knDahEYlTi9Eg=

服务端

ws 模块是 Node.js 的 WebSocket 模块,用于创建 WebSocket 服务器。

1
2
pnpm i ws
pnpm i @types/ws -D

使用该模块,省去了手动处理 WebSocket 握手、消息解析等繁琐的工作,抽象出了各种事件,方便使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import webSocket from 'ws'; // 引入ws

// 创建一个ws服务器
const ws = new webSocket.Server({
port: 8080 // 监听8080端口
}, () => {
console.log('ws服务器启动成功');
});

// 监听客户端的连接
ws.on('connection', (client) => {
console.log('有客户端连接');
// 监听客户端发来的消息
client.on('message', (msg) => {
console.log('有消息', msg);
// 向客户端发送消息
client.send('服务器收到了消息');
});
});

webSocket服务事件:

  1. connection 客户端连接事件,参数为客户端对象。
  2. close 客户端关闭事件。
  3. error 服务器出错事件。
  4. headers 客户端请求头事件,参数为请求头对象。
  5. listening 服务器监听事件。

client 客户端事件:

  1. message 客户端消息事件,参数为消息内容。
  2. close 客户端关闭事件。
  3. error 客户端出错事件。
  4. open 客户端打开事件。
  5. ping 客户端ping事件。
  6. pong 客户端pong事件。
  7. unexpected-response 客户端响应事件。
  8. upgrade 客户端协议升级事件。

简易公共聊天室

后端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import webSocket from 'ws'; // 引入ws

// 创建一个ws服务器
const ws = new webSocket.Server({
port: 8080 // 监听8080端口
}, () => {
console.log('ws服务器启动成功');
});

// 监听客户端的连接
ws.on('connection', (client) => {
console.log('新客户端连接');
// 监听客户端发来的消息
client.on('message', (msg) => {
console.log('新消息:', msg.toString());
// 向所有客户端广播消息
ws.clients.forEach((client) => {
client.send(msg.toString());
});
});
// 监听客户端断开连接
client.on('close', () => {
console.log('有客户端断开连接');
});
});

前端:

1
2
3
4
5
6
7
8
<body>
<div>
<ul id="list"></ul>
<input type="text" id="input">
<button id="send">发送</button>
</div>
<script src="index.js"></script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const ws = new WebSocket('ws://localhost:8080');

const list = document.getElementById('list');
const input = document.getElementById('input');
const send = document.getElementById('send');

send.addEventListener('click', () => {
if (!input.value) {
return;
}
ws.send(input.value);
input.value = '';
});

ws.onmessage = (event) => {
const li = document.createElement('li');
li.textContent = event.data;
list.appendChild(li);
};

当 socket 连接长时间不使用、发生网络波动、弱网环境可能导致连接断开,通常还需要实现心跳检测,以保持连接、检测连接断开后重连。

在服务端或客户端实现均可,没有一个固定的实现方式,通常是在客户端使用定时器发送心跳包。