用 Node.js 手写 WebSocket 协议
目录
引言
从 http 到 websocekt 的切换
Sec-WebSocket-Key 与 Sec-WebSocket-Accept
全新的二进制协议
自己实现一个 websocket 服务器
按照协议格式解析收到的Buffer
取出opcode
取出MASK与payload长度
根据mask key读取数据
根据类型处理数据
frame 帧
数据的发送
完整代码
总结
引言
我们知道,http 是一问一答的模式,客户端向服务器发送 http 请求,服务器返回 http 响应。这种模式对资源、数据的加载足够用,但是需要数据推送的场景就不合适了。
有同学说,http2 不是有 server push 么?那只是推资源用的:
比如浏览器请求了 html,服务端可以连带把 css 一起推送给浏览器。浏览器可以决定接不接收。对于即时通讯等实时性要求高的场景,就需要用 websocket 了。
从 http 到 websocekt 的切换
websocket 严格来说和 http 没什么关系,是另外一种协议格式。但是需要一次从 http 到 websocekt 的切换过程。
切换过程详细来说是这样的:
1. 请求的时候带上这几个 header:
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: Ia3dQjfWrAug/6qm7mTZOg==
前两个很容易理解,就是升级到 websocket 协议的意思。第三个 header 是保证安全用的一个 key。
2. 服务端返回这样的 header:
HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: JkE58n3uIigYDMvC+KsBbGZsp1A=
和请求 header 类似,Sec-WebSocket-Accept 是对请求带过来的 Sec-WebSocket-Key 处理之后的结果。
加入这个 header 的校验是为了确定对方一定是有 WebSocket 能力的,不然万一建立了连接对方却一直没消息,那不就白等了么。
Sec-WebSocket-Key 与 Sec-WebSocket-Accept
那 Sec-WebSocket-Key 经过什么处理能得到 Sec-WebSocket-Accept 呢?我用 node 实现了一下,是这样的:
const crypto = require('crypto');function hashKey(key) {const sha1 = crypto.createHash('sha1');sha1.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');return sha1.digest('base64');
}
也就是用客户端传过来的 key,加上一个固定的字符串,经过 sha1 加密之后,转成 base64 的结果。
这个字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 是固定的,不信你搜搜看:
随便找个有 websocket 的网站,比如知乎就有:
过滤出 ws 类型的请求,看看这几个 header,是不是就是前面说的那些。这个 Sec-WebSocket-Key 是 wk60yiym2FEwCAMVZE3FgQ==
而响应的 Sec-WebSocket-Accept 是 XRfPnS+8xl11QWZherej/dkHPHM=
我们算算看:
是不是一毛一样!这就是 websocket 升级协议时候的 Sec-WebSocket-Key 对应的 Sec-WebSocket-Accept 的计算过程。
这一步之后就换到 websocket 的协议了,那是一个全新的协议。
全新的二进制协议
勾选 message 这一栏可以看到传输的消息,可以是文本、可以是二进制。
全新的协议?那具体是什么样的协议呢?这样的:
大家习惯的 http 协议是 key:value 的 header ,带个 body 的:
它是文本协议,每个 header 都是容易理解的字符。这样好懂是好懂,但是传输占的空间太大了。而 websocket 是二进制协议,一个字节可以用来存储很多信息:
比如协议的第一个字节(8个二进制bit位),就存储了 FIN(结束标志)、opcode(内容类型是 binary 还是 text) 等信息。
第二个字节存储了 mask(是否有加密),payload(数据长度)。
仅仅两个字节,存储了多少信息呀!这就是二进制协议比文本协议好的地方。
我们看到的 weboscket 的 message 的收发,其实底层都是拼成这样的格式。
只是浏览器帮我们解析了这种格式的协议数据。
这就是 weboscket 的全部流程了。其实还是挺清晰的,一个切换协议的过程,然后是二进制的 weboscket 协议的收发。
自己实现一个 websocket 服务器
那我们就用 Node.js 自己实现一个 websocket 服务器吧!
1. 定义个 MyWebsocket 的 class:
const { EventEmitter } = require('events');
const http = require('http');class MyWebsocket extends EventEmitter {constructor(options) {super(options);const server = http.createServer();server.listen(options.port || 8080);server.on('upgrade', (req, socket) => {});}
}
继承 EventEmitter 是为了可以用 emit 发送一些事件,外界可以通过 on 监听这个事件来处理。
我们在构造函数里创建了一个 http 服务,当 ungrade 事件发生,也就是收到了 Connection: upgrade 的 header 的时候,返回切换协议的 header。
返回的 header 前面已经见过了,就是要对 sec-websocket-key 做下处理。
server.on('upgrade', (req, socket) => {this.socket = socket;socket.setKeepAlive(true);const resHeaders = ['HTTP/1.1 101 Switching Protocols','Upgrade: websocket','Connection: Upgrade','Sec-WebSocket-Accept: ' + hashKey(req.headers['sec-websocket-key']),'',''].join('\r\n');socket.write(resHeaders);socket.on('data', (data) => {console.log(data)});socket.on('close', (error) => {this.emit('close');});
});
我们拿到 socket,返回上面的 header,其中 key 做的处理就是前面聊过的算法:
function hashKey(key) {const sha1 = crypto.createHash('sha1');sha1.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');return sha1.digest('base64');
}
就这么简单,就已经完成协议切换了。不信我们试试看。引入我们实现的 ws 服务器,跑起来:
const MyWebSocket = require('./ws');
const ws = new MyWebSocket({ port: 8080 });ws.on('data', (data) => {console.log('receive data:' + data);
});ws.on('close', (code, reason) => {console.log('close:', code, reason);
});
然后新建这样一个 html:
<!DOCTYPE HTML>
<html>
<body><script>const ws = new WebSocket("ws://localhost:8080");ws.onopen = function () {ws.send("发送数据");setTimeout(() => {ws.send("发送数据2");}, 3000)};ws.onmessage = function (evt) {console.log(evt)};ws.onclose = function () {};</script>
</body></html>
用浏览器的 WebSocket api 建立连接,发送消息。
用 npx http-server . 起个静态服务。然后浏览器访问这个 html:
这时打开 devtools 你就会发现协议切换成功了:
这 3 个 header 还有 101 状态码都是我们返回的。message 里也可以看到发送的消息:
再去服务端看看,也收到了这个消息:
只不过是 Buffer 的,也就是二进制的。
按照协议格式解析收到的Buffer
取出opcode
接下来只要按照协议格式解析这个 Buffer,并且生成响应格式的协议数据 Buffer 返回就可以收发 websocket 数据了。
这一部分还是比较麻烦的,我们一点点来看。
我们需要第一个字节的后四位,也就是 opcode。
这样写:
const byte1 = bufferData.readUInt8(0);
let opcode = byte1 & 0x0f;
读取 8 位无符号整数的内容,也就是一个字节的内容。参数是偏移的字节,这里是 0。
通过位运算取出后四位,这就是 opcode 了。
取出MASK与payload长度
然后再处理第二个字节:
第一位是 mask 标志位,后 7 位是 payload 长度。
可以这样取:
const byte2 = bufferData.readUInt8(1);
const str2 = byte2.toString(2);
const MASK = str2[0];
let payloadLength = parseInt(str2.substring(1), 2);
还是用 buffer.readUInt8 读取一个字节的内容。
先转成二进制字符串,这时第一位就是 mask,然后再截取后 7 位的子串,parseInt 成数字,这就是 payload 长度了。
这样前两个字节的协议内容就解析完了。
有同学可能问了,后面咋还有俩 payload 长度呢?
这是因为数据不一定有多长,可能需要 16 位存长度,可能需要 32 位。
于是 websocket 协议就规定了如果那个 7 位的内容不超过 125,那它就是 payload 长度。
如果 7 位的内容是 126,那就不用它了,用后面的 16 位的内容作为 payload 长度。
如果 7 位的内容是 127,也不用它了,用后面那个 64 位的内容作为 payload 长度。
其实还是容易理解的,就是 3 个 if else。
用代码写出来就是这样的:
let payloadLength = parseInt(str2.substring(1), 2);let curByteIndex = 2;if (payloadLength === 126) {payloadLength = bufferData.readUInt16BE(2);curByteIndex += 2;
} else if (payloadLength === 127) {payloadLength = bufferData.readBigUInt64BE(2);curByteIndex += 8;
}
这里的 curByteIndex 是存储当前处理到第几个字节的。
如果是 126,那就从第 3 个字节开始,读取 2 个字节也就是 16 位的长度,用 buffer.readUInt16BE 方法。
如果是 127,那就从第 3 个字节开始,读取 8 个字节也就是 64 位的长度,用 buffer.readBigUInt64BE 方法。
这样就拿到了 payload 的长度,然后再用这个长度去截取内容就好了。
根据mask key读取数据
但在读取数据之前,还有个 mask 要处理,这个是用来给内容解密的:
读 4 个字节,就是 mask key。
再后面的就可以根据 payload 长度读出来。
let realData = null;if (MASK) {const maskKey = bufferData.slice(curByteIndex, curByteIndex + 4); curByteIndex += 4;const payloadData = bufferData.slice(curByteIndex, curByteIndex + payloadLength);realData = handleMask(maskKey, payloadData);
} else {realData = bufferData.slice(curByteIndex, curByteIndex + payloadLength);;
}
然后用 mask key 来解密数据。
这个算法也是固定的,用每个字节的 mask key 和数据的每一位做按位异或就好了:
function handleMask(maskBytes, data) {const payload = Buffer.alloc(data.length);for (let i = 0; i < data.length; i++) {payload[i] = maskBytes[i % 4] ^ data[i];}return payload;
}
这样,我们就拿到了最终的数据!
根据类型处理数据
但是传给处理程序之前,还要根据类型来处理下,因为内容分几种类型,也就是 opcode 有几种值:
const OPCODES = {CONTINUE: 0,TEXT: 1, // 文本BINARY: 2, // 二进制CLOSE: 8,PING: 9,PONG: 10,
};
我们只处理文本和二进制就好了:
handleRealData(opcode, realDataBuffer) {switch (opcode) {case OPCODES.TEXT:this.emit('data', realDataBuffer.toString('utf8'));break;case OPCODES.BINARY:this.emit('data', realDataBuffer);break;default:this.emit('close');break;}
}
文本就转成 utf-8 的字符串,二进制数据就直接用 buffer 的数据。
这样,处理程序里就能拿到解析后的数据。
我们来试一下:
之前我们已经能拿到 weboscket 协议内容的 buffer 了:
而现在我们能正确解析出其中的数据:
至此,我们 websocket 协议的解析成功了!
frame 帧
这样的协议格式的数据叫做 frame,也就是帧:
数据的发送
解析可以了,接下来我们再实现数据的发送。
发送也是构造一样的 frame 格式。
定义这样一个 send 方法:
send(data) {let opcode;let buffer;if (Buffer.isBuffer(data)) {opcode = OPCODES.BINARY;buffer = data;} else if (typeof data === 'string') {opcode = OPCODES.TEXT;buffer = Buffer.from(data, 'utf8');} else {console.error('暂不支持发送的数据类型')}this.doSend(opcode, buffer);
}doSend(opcode, bufferDatafer) {this.socket.write(encodeMessage(opcode, bufferDatafer));
}
根据发送的是文本还是二进制数据来对内容作处理。
然后构造 websocket 的 frame:
function encodeMessage(opcode, payload) {//payload.length < 126let bufferData = Buffer.alloc(payload.length + 2 + 0);;let byte1 = parseInt('10000000', 2) | opcode; // 设置 FIN 为 1let byte2 = payload.length;bufferData.writeUInt8(byte1, 0);bufferData.writeUInt8(byte2, 1);payload.copy(bufferData, 2);return bufferData;
}
我们只处理数据长度小于 125 的情况。
第一个字节是 opcode,我们把第一位置 1 ,通过按位或的方式 写入。
服务端给客户端回消息不需要 mask,所以第二个字节就是 payload 长度。
分别把这前两个字节的数据写到 buffer 里,指定不同的 offset:
bufferData.writeUInt8(byte1, 0);
bufferData.writeUInt8(byte2, 1);
之后把 payload 数据放在后面:
payload.copy(bufferData, 2);
这样一个 websocket 的 frame 就构造完了。
我们试一下:
收到客户端消息后,每两秒回一个消息。
收发消息都成功了!
完整代码
就这样,我们自己实现了一个 websocket 服务器,实现了 websocket 协议的解析和生成!
完整代码如下:
MyWebSocket:
//ws.js
const { EventEmitter } = require('events');
const http = require('http');
const crypto = require('crypto');function hashKey(key) {const sha1 = crypto.createHash('sha1');sha1.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');return sha1.digest('base64');
}function handleMask(maskBytes, data) {const payload = Buffer.alloc(data.length);for (let i = 0; i < data.length; i++) {payload[i] = maskBytes[i % 4] ^ data[i];}return payload;
}const OPCODES = {CONTINUE: 0,TEXT: 1,BINARY: 2,CLOSE: 8,PING: 9,PONG: 10,
};function encodeMessage(opcode, payload) {//payload.length < 126let bufferData = Buffer.alloc(payload.length + 2 + 0);;let byte1 = parseInt('10000000', 2) | opcode; // 设置 FIN 为 1let byte2 = payload.length;bufferData.writeUInt8(byte1, 0);bufferData.writeUInt8(byte2, 1);payload.copy(bufferData, 2);return bufferData;
}class MyWebsocket extends EventEmitter {constructor(options) {super(options);const server = http.createServer();server.listen(options.port || 8080);server.on('upgrade', (req, socket) => {this.socket = socket;socket.setKeepAlive(true);const resHeaders = ['HTTP/1.1 101 Switching Protocols','Upgrade: websocket','Connection: Upgrade','Sec-WebSocket-Accept: ' + hashKey(req.headers['sec-websocket-key']),'',''].join('\r\n');socket.write(resHeaders);socket.on('data', (data) => {this.processData(data);// console.log(data);});socket.on('close', (error) => {this.emit('close');});});}handleRealData(opcode, realDataBuffer) {switch (opcode) {case OPCODES.TEXT:this.emit('data', realDataBuffer.toString('utf8'));break;case OPCODES.BINARY:this.emit('data', realDataBuffer);break;default:this.emit('close');break;}}processData(bufferData) {const byte1 = bufferData.readUInt8(0);let opcode = byte1 & 0x0f; const byte2 = bufferData.readUInt8(1);const str2 = byte2.toString(2);const MASK = str2[0];let curByteIndex = 2;let payloadLength = parseInt(str2.substring(1), 2);if (payloadLength === 126) {payloadLength = bufferData.readUInt16BE(2);curByteIndex += 2;} else if (payloadLength === 127) {payloadLength = bufferData.readBigUInt64BE(2);curByteIndex += 8;}let realData = null;if (MASK) {const maskKey = bufferData.slice(curByteIndex, curByteIndex + 4); curByteIndex += 4;const payloadData = bufferData.slice(curByteIndex, curByteIndex + payloadLength);realData = handleMask(maskKey, payloadData);} this.handleRealData(opcode, realData);}send(data) {let opcode;let buffer;if (Buffer.isBuffer(data)) {opcode = OPCODES.BINARY;buffer = data;} else if (typeof data === 'string') {opcode = OPCODES.TEXT;buffer = Buffer.from(data, 'utf8');} else {console.error('暂不支持发送的数据类型')}this.doSend(opcode, buffer);}doSend(opcode, bufferDatafer) {this.socket.write(encodeMessage(opcode, bufferDatafer));}
}module.exports = MyWebsocket;
Index:
const MyWebSocket = require('./ws');
const ws = new MyWebSocket({ port: 8080 });ws.on('data', (data) => {console.log('receive data:' + data);setInterval(() => {ws.send(data + ' ' + Date.now());}, 2000)
});ws.on('close', (code, reason) => {console.log('close:', code, reason);
});
html:
<!DOCTYPE HTML>
<html>
<body><script>const ws = new WebSocket("ws://localhost:8080");ws.onopen = function () {ws.send("发送数据");setTimeout(() => {ws.send("发送数据2");}, 3000)};ws.onmessage = function (evt) {console.log(evt)};ws.onclose = function () {};</script>
</body></html>
总结
实时性较高的需求,我们会用 websocket 实现,比如即时通讯、游戏等场景。
websocket 和 http 没什么关系,但从 http 到 websocket 需要一次切换的过程。
这个切换过程除了要带 upgrade 的 header 外,还要带 sec-websocket-key,服务端根据这个 key 算出结果,通过 sec-websocket-accept 返回。响应是 101 Switching Protocols 的状态码。
这个计算过程比较固定,就是 key + 固定的字符串 通过 sha1 加密后再 base64 的结果。
加这个机制是为了确保对方一定是 websocket 服务器,而不是随意返回了个 101 状态码。
之后就是 websocket 协议了,这是个二进制协议,我们根据格式完成了 websocket 帧的解析和生成。
这样就是一个完整的 websocket 协议的实现了。
我们自己手写了一个 websocket 服务,有没有感觉对 websocket 的理解更深了呢?
相关文章:

用 Node.js 手写 WebSocket 协议
目录 引言 从 http 到 websocekt 的切换 Sec-WebSocket-Key 与 Sec-WebSocket-Accept 全新的二进制协议 自己实现一个 websocket 服务器 按照协议格式解析收到的Buffer 取出opcode 取出MASK与payload长度 根据mask key读取数据 根据类型处理数据 frame 帧 数据的发…...

Xilinx AXI VIP使用教程
AXI接口虽然经常使用,很多同学可能并不清楚Vivado里面也集成了AXI的Verification IP,可以当做AXI的master、pass through和slave,本次内容我们看下AXI VIP当作master时如何使用。 新建Vivado工程,并新建block design,命…...
mysql主主架构搭建,删库恢复
mysql主主架构搭建,删库恢复 搭建mysql主主架构环境信息安装msql服务mysql1mysql2设置mysql2同步mysql1设置mysql1同步mysql2授权测试用账户 安装配置keepalivedmysql1检查脚本mysql2检查脚本 备份策略mysqldump全量备份mysqldump增量备份数据库目录全量备份 删除my…...
pythonweek1
引言 做任何事情都要脚踏实地,虽然大一上已经学习了python的基础语法,大一下也学习了C加加中的类与对象,但是自我觉得基础还不太扎实,又害怕有什么遗漏,所以就花时间重新学习了python的基础,学习Python的基…...

进程虚拟地址空间区域划分
目录 图示 详解 代码段 备注:x86 32位linux环境下,进程虚拟地址空间区域划分 图示 详解 用户空间 用于存储用户进程代码和数据,只能由用户进程访问 内核空间 用于存储操作系统内核代码和数据,只能由操作系统内核访问 text t…...

OpenAI Code Interpreter 的开源实现:GPT Code UI
本篇文章聊聊 OpenAI Code Interpreter 的一众开源实现方案中,获得较多支持者,但暂时还比较早期的项目:GPT Code UI。 写在前面 这篇文章本该更早的时候发布,但是 LLaMA2 发布后实在心痒难忍,于是就拖了一阵。结合 L…...

macOS Ventura 13.5 (22G74) 正式版发布,ISO、IPSW、PKG 下载
macOS Ventura 13.5 (22G74) 正式版发布,ISO、IPSW、PKG 下载 本站下载的 macOS Ventura 软件包,既可以拖拽到 Applications(应用程序)下直接安装,也可以制作启动 U 盘安装,或者在虚拟机中启动安装。另外也…...
Electron 主进程和渲染进程传值及窗口间传值
1 渲染进程调用主进程得方法 下面是渲染进程得代码: let { ipcRenderer} require( electron ); ipcRenderer.send( xxx ); //渲染进程中调用 下面是主进程得代码: var { ipcMain } require( electron ); ipcMain.on("xxx",function () { } )...
C#设计模式之---建造者模式
建造者模式(Builder Pattern) 建造者模式(Builder Pattern)是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。建造者模式使得建造代码与表示代码的分离,可以使客户端不必知道…...

output delay 约束
output delay 约束 一、output delay约束概述二、output delay约束系统同步三、output delay约束源同步 一、output delay约束概述 特别注意:在源同步接口中,定义接口约束之前,需要用create_generated_clock 先定义送出的随路时钟。 二、out…...

html2Canvas+jsPDF 下载PDF 遇到跨域的对象存储的图片无法显示
一、问题原因 对象存储的域名和你网址的域名不一样,此时用Canvas相关插件 将DOM元素转化为PDF,就会出现跨域错误。 二、解决办法 两步 1. 图片元素上设置属性 crossorigin"anonymous" 支持原生img和eleme组件 2. 存储桶设置资源跨域访问…...

【C#】并行编程实战:异步流
本来这章该讲的是 ASP .NET Core 中的 IIS 和 Kestrel ,但是我看了下这个是给服务器用的。而我只是个 Unity 客户端程序,对于服务器的了解趋近于零。 鉴于我对服务器知识和需求的匮乏,这里就不讲原书(大部分)内容了。本…...

在家下载论文使用哪些论文下载工具比较好
在家下载论文如果不借助论文下载工具是非常艰难的事情,因为很多查找下载论文的数据库都是需要账号权限才可使用的。 例如,我们查找中文论文常用的知网、万方等数据库以及众多国外论文数据库。 在家下载知网、万方数据库论文可用下面的方法:…...
【LeetCode 算法】Handling Sum Queries After Update 更新数组后处理求和查询-Segment Tree
文章目录 Handling Sum Queries After Update 更新数组后处理求和查询问题描述:分析代码线段树 Tag Handling Sum Queries After Update 更新数组后处理求和查询 问题描述: 给你两个下标从 0 开始的数组 n u m s 1 和 n u m s 2 nums1 和 nums2 nums1…...

基于Linux操作系统中的MySQL数据库SQL语句(三十一)
MySQL数据库SQL语句 目录 一、SQL语句类型 1、DDL 2、DML 3、DCL 4、DQL 二、数据库操作 1、查看 2、创建 2.1、默认字符集 2.2、指定字符集 3、进入 4、删除 5、更改 6、练习 三、数据表操作 (一)数据类型 1、数值类型 1.1、TINYINT …...
【Matlab】基于BP神经网络的数据回归预测新数据(Excel可直接替换数据)
【Matlab】基于BP神经网络的数据回归预测新数据(Excel可直接替换数据) 1.模型原理2.数学公式3.文件结构4.Excel数据5.分块代码5.1 main.m5.2 NewData.m6.完整代码6.1 main.m6.2 NewData.m7.运行结果1.模型原理 基于BP神经网络的数据回归预测是一种常见的机器学习方法,用于处…...

HTTPS连接过程中的中间人攻击
HTTPS连接过程中的中间人攻击 HTTPS连接过程中间人劫持攻击 HTTPS连接过程 https协议就是httpssl/tls协议,如下图所示为其连接过程: HTTPS连接的整个工程如下: https请求:客户端向服务端发送https请求;生成公钥和私…...

redis启动失败,oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
在redis文件夹下,启动redis正常。 但是加入到system后启动redis失败。 一直处于starting状态。 对比正常redis服务的配置之后,把redis.conf里的守护进程关掉就可以了(但是没用system管理之前,直接./redis.server启动是可以的&…...

milvus: 专为向量查询与检索设计的向量数据库
1. 什么是milvus? milvus docs milvus release Milvus的目标是:store, index, and manage massive embedding vectors generated by deep neural networks and other machine learning (ML) models. Milvus 向量数据库专为向量查询与检索设计…...

【C# 数据结构】Heap 堆
【C# 数据结构】Heap 堆 先看看C#中有那些常用的结构堆的介绍完全二叉树最大堆 Heap对类进行排序实现 IComparable<T> 接口 对CompareTo的一点解释 参考资料 先看看C#中有那些常用的结构 作为 数据结构系类文章 的开篇文章,我们先了解一下C# 有哪些常用的数据…...

高等数学(下)题型笔记(八)空间解析几何与向量代数
目录 0 前言 1 向量的点乘 1.1 基本公式 1.2 例题 2 向量的叉乘 2.1 基础知识 2.2 例题 3 空间平面方程 3.1 基础知识 3.2 例题 4 空间直线方程 4.1 基础知识 4.2 例题 5 旋转曲面及其方程 5.1 基础知识 5.2 例题 6 空间曲面的法线与切平面 6.1 基础知识 6.2…...

HBuilderX安装(uni-app和小程序开发)
下载HBuilderX 访问官方网站:https://www.dcloud.io/hbuilderx.html 根据您的操作系统选择合适版本: Windows版(推荐下载标准版) Windows系统安装步骤 运行安装程序: 双击下载的.exe安装文件 如果出现安全提示&…...
Spring Boot面试题精选汇总
🤟致敬读者 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉 📘博主相关 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...

如何在最短时间内提升打ctf(web)的水平?
刚刚刷完2遍 bugku 的 web 题,前来答题。 每个人对刷题理解是不同,有的人是看了writeup就等于刷了,有的人是收藏了writeup就等于刷了,有的人是跟着writeup做了一遍就等于刷了,还有的人是独立思考做了一遍就等于刷了。…...

html-<abbr> 缩写或首字母缩略词
定义与作用 <abbr> 标签用于表示缩写或首字母缩略词,它可以帮助用户更好地理解缩写的含义,尤其是对于那些不熟悉该缩写的用户。 title 属性的内容提供了缩写的详细说明。当用户将鼠标悬停在缩写上时,会显示一个提示框。 示例&#x…...
【生成模型】视频生成论文调研
工作清单 上游应用方向:控制、速度、时长、高动态、多主体驱动 类型工作基础模型WAN / WAN-VACE / HunyuanVideo控制条件轨迹控制ATI~镜头控制ReCamMaster~多主体驱动Phantom~音频驱动Let Them Talk: Audio-Driven Multi-Person Conversational Video Generation速…...

LINUX 69 FTP 客服管理系统 man 5 /etc/vsftpd/vsftpd.conf
FTP 客服管理系统 实现kefu123登录,不允许匿名访问,kefu只能访问/data/kefu目录,不能查看其他目录 创建账号密码 useradd kefu echo 123|passwd -stdin kefu [rootcode caozx26420]# echo 123|passwd --stdin kefu 更改用户 kefu 的密码…...

安全突围:重塑内生安全体系:齐向东在2025年BCS大会的演讲
文章目录 前言第一部分:体系力量是突围之钥第一重困境是体系思想落地不畅。第二重困境是大小体系融合瓶颈。第三重困境是“小体系”运营梗阻。 第二部分:体系矛盾是突围之障一是数据孤岛的障碍。二是投入不足的障碍。三是新旧兼容难的障碍。 第三部分&am…...
Mysql8 忘记密码重置,以及问题解决
1.使用免密登录 找到配置MySQL文件,我的文件路径是/etc/mysql/my.cnf,有的人的是/etc/mysql/mysql.cnf 在里最后加入 skip-grant-tables重启MySQL服务 service mysql restartShutting down MySQL… SUCCESS! Starting MySQL… SUCCESS! 重启成功 2.登…...

Linux 中如何提取压缩文件 ?
Linux 是一种流行的开源操作系统,它提供了许多工具来管理、压缩和解压缩文件。压缩文件有助于节省存储空间,使数据传输更快。本指南将向您展示如何在 Linux 中提取不同类型的压缩文件。 1. Unpacking ZIP Files ZIP 文件是非常常见的,要在 …...