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 ()); 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 ()); console .log ('已获取完毕' ); }) }) req.write (JSON .stringify ({"name" : "tom" })); req.end ();
更多基本用法,之前已经记录过了:初识NodeJS—http模块
反向代理 直接访问 :浏览器直接请求最终提供资源的服务器,中间没有经过任何的 http 代理服务器。正向代理(forward proxy) :代理的对象是客户端,代理服务器代替客户端(浏览器)去访问目标服务器,获取资源返回,并不直接面向最终提供资源的服务器。代理服务器可以对外隐藏客户端细节,代理服务器会对外屏蔽掉客户端信息。通常需要客户端主动配置代理服务器的信息。反向代理(reverse proxy) :代理的对象是服务端,客户端(浏览器)不需要任何配置。反向代理服务器代替服务端(服务器)接收请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给客户端,此时反向代理服务器和普通的服务器对外就是一个服务器,客户端不需要知道内部网络的信息。也称为透明代理 。
反向代理作用:
保证内网的安全: 防止外部网络直接访问内网
负载均衡: 将请求分发到多个后端服务器上,避免某个服务器过载,提高整体性能和可用性
缓存和性能优化: 将请求的结果和静态资源缓存起来,减少对服务器的请求,还可以资源进行压缩、合并、优化等操作
高可用: 当某个服务器出现故障时,可以自动将请求转发到其他服务器上
域名和路径重写: 将请求的域名和路径重写,转发到其他服务器上,提高了灵活性和可维护性
通过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 , { '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 , host : 'smtp.qq.com' , secure : true , 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)const app = express ()app.get ('/txt' , (req, res ) => { console .log (req.method ); console .log (req.url ); console .log (req.httpVersion ); console .log (req.headers ); console .log (req.path ); console .log (req.query ); console .log (req.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 ) }) app.use (express.static (path.resolve (__dirname, 'public' ))) app.use (express.json ()) app.use (express.urlencoded ({ extended : true })) app.post ('/login' , (req, res ) => { console .log (req.body ) res.send ('hello post' ) }) const logger = (req, res, next ) => { console .log ('logger' ) next () } const router = express.Router ();router.get ('/list' , logger, (req, res ) => { 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服务端常用的日志收集工具:winston 、log4js 、bunyan 、npmlog 等
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" } }, }); const logger = log4js.getLogger ("default" )const app = express ()const LoggerMiddleware = (req, res, next ) => { logger.debug (`[${req.method} ] ${req.url} ` ) 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' }, OFF : { value : Number .MAX_VALUE , colour : 'grey' }, });
在categories.default.level
中配置日志等级,只会输出级别相等或更高级别的日志。
ALL OFF 这两个等级并不会直接在业务代码中使用。其它级别分别对应 Logger 实例的七个方法。在调用这些方法的时候,就相当于为这些日志定了级。
logger.trace()
详细信息
logger.debug()
调试信息
logger.info()
一般信息
logger.warn()
警告信息
logger.error()
错误信息
logger.fatal()
致命信息
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下有两个属性:
appenders :用于配置日志输出方式,可以配置多个输出方式,例如:控制台、日志文件、邮件等。
categories :用于配置日志分类,每个分类可以设置多个appender,也可以配置日志等级。
log4js.getLogger(category)
获取指定分类的日志记录器。
若不传入,则会使用default分类配置。
若传入不存在的分类名,也使用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 : { appenders : ["console" ], level : "debug" , }, access : { appenders : ["out" ], level : "info" , }, error : { appenders : ["err" ], level : "error" , 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 const defaultLogger = log4js.getLogger () defaultLogger.debug ("Time:" , new Date ()); const otherLogger = log4js.getLogger ("other" )otherLogger.debug ("Time:" , new Date ()); const accessLogger = log4js.getLogger ("access" )accessLogger.debug ("Time:" , new Date ()); accessLogger.info ("Time:" , new Date ()); const errorLogger = log4js.getLogger ("error" )errorLogger.error ("Time:" , new Error ("error" ));
日志落盘Appender Appender 用于配置日志输出方式
默认Appenders: 若不进行任何配置,日志将输出到标准输出,但也定义了默认日志级别OFF,这意味着实际上不会输出任何日志。
自定义Appenders: 传入一个对象,key为Appender名,value为Appender配置。
常用的Appender.type:
console
:通过V8的console输出到控制台
stdout
:通过标准输出到控制台
stderr
:通过标准错误输出到控制台
file
:输出为指定日志文件
dateFile
:按照特定日期滚动生成多个日志文件
multiFile
动态配置不同category下或者不同变量控制下,落盘到不同文件
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 : { type : "stdout" , layout : { type : "colored" } }, file : { type : "file" , filename : "server.log" , maxLogSize : 1024 * 1024 * 10 , backups : 3 , encoding : "utf-8" , compress : true , keepFileExt : true , fileNameSep : "-" , }, date : { type : "dateFile" , filename : "app.log" , pattern : "yyyy-MM-dd" , }, multi : { type : "multiFile" , base : "logs/" , property : "categoryName" , extension : ".log" , timeout : 0 , }, error : { type : "logLevelFilter" , appender : "file" , level : "error" , }, }, categories : { default : { appenders : ["out" , "error" ], level : "debug" }, date : { appenders : ["out" , "date" ], level : "debug" }, multi : { appenders : ["out" , "multi" ], level : "debug" }, }, }); const defaultLogger = log4js.getLogger ()const dateLogger = log4js.getLogger ('date' )const multiLogger = log4js.getLogger ('multi' )defaultLogger.debug ('Time:' , new Date ()) defaultLogger.error ('Time:' , new Error ('error' )) dateLogger.debug ('Time:' , new Date ()) multiLogger.debug ('Time:' , new Date ())
对于日志滚动和日期日志,先创建一个原名日志文件,当发生滚动或日期变更时,重命名该文件为原名+序号 或原名+日期 ,然后创建一个新的原名日志文件继续写入,依次循环。
其它接口 log4js.isConfigured()
返回boolean,表示log4js.configure()先前是否成功调用(getLogger()也会隐式调用configure())
1 2 3 console .log (log4js.isConfigured ()) log4js.getLogger () console .log (log4js.isConfigured ())
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" );
HTTP标头 HTTP 标头 (header)允许客户端和服务器通过 HTTP 请求(request)或者响应(response)传递附加信息
HTTP 标头由它的名称(不区分大小写)后跟随一个冒号(:),冒号后跟随它具体的值。该值之前的空格会被忽略。
根据不同的消息上下文,标头可以分为:
请求标头 包含有关要获取的资源或客户端或请求资源的客户端的更多信息。
响应标头 包含有关响应的额外信息,例如响应的位置或者提供响应的服务器。
表示标头 包含资源主体的信息,例如主体的 MIME 类型或者应用的编码/压缩方案。
有效负荷标头 包含有关有效载荷数据表示的单独信息,包括内容长度和用于传输的编码。
请求表头 HTTP请求头(HTTP request headers)是在HTTP请求中发送的元数据信息,用于描述请求的特性、内容和行为。
常见请求头:
Accept :客户端能够处理的媒体类型,值:text/html
、application/json
等
Accept-Encoding :客户端能够处理的编码方式,值:gzip
、deflate
等
Accept-Language :客户端能够处理的语言,值:zh-CN
、en-US
等
Connection :连接方式,值:keep-alive
、close
等
Content-Type :请求体的MIME类型,值:text/html
、application/json
等
Content-Length 请求体的长度,单位字节
Host :请求的主机名,值:www.baidu.com
、localhost:8888
等
Referer :请求来源,值:https://www.baidu.com
、http://localhost:8888
等
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
等
Cache-Control 缓存控制,值:no-cache
、max-age=3600
等
Cookie 请求携带的cookie,值:name=tom;
等
Range 用于断点续传,值:bytes=0-1023
等,指定第一个字节的位置和最后一个字节的位置
Sec-Ch-Ua 浏览器的 UA 字符串,值:"Google Chrome";v="120"
等
Sec-Ch-Ua-Mobile 浏览器是否为移动端,值:?0
、?1
等
Sec-Fetch-Dest 请求的目标资源类型,值:document
、image
等
Sec-Fetch-Mode 请求的模式,值:navigate
、cors
等
Sec-Fetch-Site 请求的站点类型,值:same-origin
、cross-site
等
Sec-Ch-Ua-Platform 浏览器的平台,值:"Windows"
、"macOS"
等
响应标头 HTTP响应头(HTTP response headers)是在HTTP响应中发送的元数据信息,用于描述响应的特性、内容和行为。
常见响应头:
Content-Type :响应体的MIME类型,值:text/html
、application/json
等
Content-Length :响应体的长度,单位字节
Content-Encoding :响应体的编码方式,值:gzip
、deflate
等
Content-Language :响应体的语言,值:zh-CN
、en-US
等
Content-Disposition :响应体的附加信息,值:attachment; filename="filename.jpg"
,表示附件,浏览器会提示下载
Location :重定向的地址,值:https://www.baidu.com
Set-Cookie :设置cookie,值:name=tom;
等
Cache-Control :缓存控制,值:no-cache
、max-age=3600
等
Expires :缓存过期时间,值:Wed, 21 Oct 2024 07:28:00 GMT
Last-Modified :资源最后修改时间,值:Wed, 21 Oct 2024 07:28:00 GMT
Etag :资源的唯一标识,例如:"5d8b9b4e-2a"
,用于缓存验证
Access-Control-Allow-Origin :允许跨域的域名,值:*
、http://localhost:8080
等
Access-Control-Allow-Methods :允许跨域的请求方法,值:GET
、POST
等
Access-Control-Allow-Headers :允许跨域的请求头 ,值:Content-Type
、Authorization
等
Access-Control-Allow-Credentials :是否允许跨域携带cookie,值:true
、false
等
Access-Control-Max-Age :预检请求的有效期,单位秒,值:3600
等
Access-Control-Expose-Headers :允许跨域的响应头 ,值:Content-Type
、Authorization
等,若请求没有携带凭据(Cookie或认证信息),*
才会被当作一个特殊的通配符,否则会被简单地当作标头名称。默认情况下,仅暴露CORS安全列表的响应标头
Strict-Transport-Security :强制使用HTTPS,值:max-age=31536000; includeSubDomains
,表示一年内所有子域名都必须使用HTTPS
X-Frame-Options :防止网页被嵌入到iframe中,值:DENY
、SAMEORIGIN
等
X-XSS-Protection :防止XSS攻击,值:1; mode=block
、0
等
X-Content-Type-Options :防止MIME类型被修改,值:nosniff
等
Referrer-Policy :控制referer的发送,值:no-referrer
、strict-origin-when-cross-origin
等
Content-Security-Policy :控制资源加载,值:default-src 'self'
、script-src 'self'
等
X-Powered-By :服务器信息,值:Express
、PHP/7.4.3
等
Server :服务器信息,值:nginx/1.18.0
、Apache/2.4.46
等
Date :响应时间,值:Wed, 21 Oct 2024 07:28:00 GMT
Connection :连接方式,值:keep-alive
、close
等
Transfer-Encoding :传输编码,值:chunked
、gzip
等
Vary :缓存策略,值:Accept-Encoding
、User-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 26 27 28 const allowedOrigins = ["127.0.0.1:5500" , undefined ];app.use ((req, res, next ) => { const origin = req.headers .origin ; if ( (origin && allowedOrigins.some ((item ) => origin.includes (item))) || allowedOrigins.includes (origin) ) { res.header ('Access-Control-Allow-Origin' , origin); 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' ); res.header ("Access-Control-Allow-Credentials" , true ); if (req.method .toLowerCase () == 'options' ) { res.send (200 ); } else { next (); } } else { res.status (403 ).send ('Forbidden' ); } })
跨域使用Cookie
服务端 将Access-Control-Allow-Credentials
设为 true
客户端 请求时,设置XMLHttpRequest.withCredentials
为 true 或 Fetch API 的 credentials
选项为 include
1 2 3 4 5 fetch ('http://127.0.0.1:8888/api' , { method : 'GET' , credentials : 'include' , }).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 即跨域资源共享。
存在以下情况时,浏览器会发送预检请求:
使用了自定义的请求头(非简单请求)
使用了非简单请求方法(PUT、DELETE等)
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 ) => { res.set ('Content-Type' , 'text/event-stream' ); res.set ('Cache-Control' , 'no-cache' ); res.set ('Connection' , 'keep-alive' ); let i = 1 ; setInterval (() => { res.write ('event: test\n' ); res.write (`id: ${i++} \n` ); res.write ('retry: 3000\n' ); res.write (`data: ${JSON .stringify({ time: new Date ().toLocaleString(), })} \n\n` ); }, 1000 ); });
res.write
发送消息: 每一次发送的信息,由若干个message 组成,每个message之间用\n\n
分隔。 每个message内部由若干行 组成,每行格式:[field]: value\n
,每行之间用\n
分隔。
[field]
可以取四个值:
event :事件类型名,默认为 message。
id :事件的ID,也可以是每一条数据的编号。
retry :出错的重连时间间隔,单位毫秒。
data :事件的数据,必须是字符串,如果数据有多行,使用\n分隔。
此外,还可以有冒号开头的行,表示注释。通常,服务器每隔一段时间就会向浏览器发送一个注释,保持连接不中断。 如 : This is a comment\n
id的作用: 每个message都应该有一个独特的id,浏览器用lastEventId属性读取id的值。一旦连接断线,浏览器会发送一个 HTTP 头,里面包含一个特殊的Last-Event-ID头信息,将这个值发送回来,用来帮助服务器端重建连接。因此,这个头信息可以被视为一种同步机制。
EventSource 前端使用EventSource(url, options)
创建SSE对象,监听对应事件,获得服务端响应的数据。
本质是发送了一次GET请求,但是不会关闭连接,而是保持连接,等待服务端推送数据。
options 配置项:
withCredentials
Boolean,是否允许发送 Cookie 和 HTTP 认证信息。默认为 false。
headers
Object,要发送的请求头信息。
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 }); console .log ('连接中' , sse.readyState ); sse.addEventListener ('open' , () => { console .log ('连接已建立' , sse.readyState ); }); sse.addEventListener ('test' , (e ) => { console .log (e.type ); console .log (e.lastEventId ); console .log (JSON .parse (e.data )); }); setTimeout (() => { sse.close (); console .log ('连接已关闭' , sse.readyState ); }, 4000 );
EventSource.readyState
,表明连接的当前状态。
0 :相当于常量EventSource.CONNECTING
,表示连接还未建立,或者断线正在重连。
1 :相当于常量EventSource.OPEN
,表示连接已经建立,可以接受数据。
2 :相当于常量EventSource.CLOSED
,表示连接已断,且不会重连。
事件:
open
EventSource 对象已经和服务器建立了连接,并开始接收来自服务器的数据,此时 readyState 为 1。
error
在建立连接或接收服务器数据时发生了错误。
message
服务器发送了一个没有指定事件名的消息,此时 event.type 为 message。
自定义事件名:服务器发送了一个指定事件名的消息,此时 event.type 为自定义事件名。
WebSocket双工通信 WebSocket 是建立在单个 TCP 连接上的全双工通信协议 ,允许客户端和服务器之间进行实时双向通信。
WebSocket 教程-阮一峰 你不知道的 WebSocket-阿宝哥
特点:
建立在 TCP 协议之上,服务器端的实现比较容易。
与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
数据格式比较轻量,性能开销小,通信高效。
有状态的协议,之后通信时可以省略部分状态信息
可以发送文本,也可以发送二进制数据。
没有同源限制,客户端可以与任意服务器通信。
协议标识符是ws
(如果加密,则为wss
),服务器网址就是 URL。
客户端 WebSocket 对象用于创建 WebSocket 连接,通过该连接可以发送和接收数据。
1 2 3 4 const ws = new WebSocket ('ws://localhost:8080' );ws.onopen = () => { console .log ('Connected' ); };
readyState
属性
0 :相当于常量WebSocket.CONNECTING
,表示连接还未建立,或者断线正在重连。
1 :相当于常量WebSocket.OPEN
,表示连接已经建立,可以通讯。
2 :相当于常量WebSocket.CLOSING
,表示连接正在关闭。
3 :相当于常量WebSocket.CLOSED
,表示连接已断,且不会重连。
事件:
open
WebSocket 连接已建立。
message
接收到服务器发送的数据。
error
连接出错。
close
连接关闭。
方法:
send(data)
向服务器发送数据。
close()
关闭连接。
请求和响应头 1、请求头:
Upgrade 指定为websocket
,表示要升级协议为 WebSocket(HTTP 协议提供了一种特殊的机制,这一机制允许将一个已建立的连接升级成新的、不相容的协议)
Connection 指定为Upgrade
,表示要升级连接。
Sec-WebSocket-Key Base64编码的16字节随机字符串,用于验证服务器是否正确处理了握手请求。
Sec-WebSocket-Version WebSocket协议的版本号,当前为13。
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、响应头:
101 Switching Protocols 101 状态码,表示协议切换成功。
Upgrade 指定为websocket
,表示要升级协议为 WebSocket。
Connection 指定为Upgrade
,表示要升级连接。
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' ; const ws = new webSocket.Server ({ port : 8080 }, () => { console .log ('ws服务器启动成功' ); }); ws.on ('connection' , (client ) => { console .log ('有客户端连接' ); client.on ('message' , (msg ) => { console .log ('有消息' , msg); client.send ('服务器收到了消息' ); }); });
webSocket服务事件:
connection
客户端连接事件,参数为客户端对象。
close
客户端关闭事件。
error
服务器出错事件。
headers
客户端请求头事件,参数为请求头对象。
listening
服务器监听事件。
client 客户端事件:
message
客户端消息事件,参数为消息内容。
close
客户端关闭事件。
error
客户端出错事件。
open
客户端打开事件。
ping
客户端ping事件。
pong
客户端pong事件。
unexpected-response
客户端响应事件。
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' ; const ws = new webSocket.Server ({ port : 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 连接长时间不使用、发生网络波动、弱网环境可能导致连接断开,通常还需要实现心跳检测,以保持连接、检测连接断开后重连。
在服务端或客户端实现均可,没有一个固定的实现方式,通常是在客户端使用定时器发送心跳包。