用 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 的理解更深了呢?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/6776.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

多种语言示例采集数据【淘宝天猫1688拼多多API系列】可高并发线程

长话短说&#xff0c;节约彼此宝贵时间&#xff0c;我们以淘宝商品详情数据为例&#xff1a; 请求方式&#xff1a;HTTPS POST GET 请求地址&#xff1a;https://o0b.cn/anzexi taobao.item_get 公共参数 名称类型必须描述技术交流18179014480keyString是调用key&#xf…

rust学习-智能指针

适用场景 有一个在编译时未知大小的类型&#xff0c;想在需要确切大小的上下文使用该类型值 示例1 无意义的例子&#xff1a;将一个单独的值存放在堆上并不是很有意义&#xff0c;b更应该放到栈上 fn main() {let b Box::new(5);// box 在 main 的末尾离开作用域时&#x…

[论文阅读笔记24]Social-STGCNN: A Social Spatio-Temporal GCNN for Human Traj. Pred.

论文: 论文地址 代码: 代码地址 作者在这篇文章中直接用GNN对目标的轨迹时空特征进行建模, 并用时序CNN进行预测, 代替了训练难度较大和速度较慢的RNN类方法. 0. Abstract 行人轨迹预测是一个比较有挑战性的任务, 有着许多的应用. 一个行人的轨迹不仅是由自己决定的, 而且受…

产品设计思考:如何平衡用户习惯和用户体验

在产品设计领域&#xff0c;平衡用户习惯与用户体验之间的关系是一个重要而复杂的任务。 用户习惯是指用户在长期使用产品过程中逐渐形成的一种行为模式&#xff0c;而用户体验则是用户在与产品交互时所感受到的整体感受。 在追求良好的用户体验的同时&#xff0c;还需要考虑用…

2023 年第二届钉钉杯大学生大数据挑战赛 初赛 B:美国纽约公共自行车使用量预测分析 问题二Python代码分析

2023 年第二届钉钉杯大学生大数据挑战赛 初赛 B&#xff1a;美国纽约公共自行车使用量预测分析 问题二 相关链接 【2023 年第二届钉钉杯大学生大数据挑战赛】 初赛 B&#xff1a;美国纽约公共自行车使用量预测分析 问题一Python代码分析 【2023 年第二届钉钉杯大学生大数据挑…

进阶级!美创暗数据发现和分类分级系统获中国信通院权威测评

近日&#xff0c;美创科技暗数据发现和分类分级系统顺利通过中国信通院“数据分类分级能力检验进阶级”认证&#xff0c;获颁数据安全产品检验证书。 数据分类分级进阶级评测通过主动探测识别率、被动探测识别率、用户相关数据识别准确率、分级标识准确率等技术指标的检测&…

AMEYA360代理线:ROHM开发出EcoGaN™减少服务器和AC适配器等的损耗和体积!

全球知名半导体制造商ROHM&#xff08;总部位于日本京都市&#xff09;面向数据服务器等工业设备和AC适配器等消费电子设备的一次侧电源*1&#xff0c;开发出集650V GaN HEMT*2和栅极驱动用驱动器等于一体的Power Stage IC“BM3G0xxMUV-LB”&#xff08;BM3G015MUV-LB、BM3G007…

【C++】继承

&#x1f307;个人主页&#xff1a;平凡的小苏 &#x1f4da;学习格言&#xff1a;命运给你一个低的起点&#xff0c;是想看你精彩的翻盘&#xff0c;而不是让你自甘堕落&#xff0c;脚下的路虽然难走&#xff0c;但我还能走&#xff0c;比起向阳而生&#xff0c;我更想尝试逆风…

了解Unity编辑器之组件篇Video(二)

Video Player组件&#xff1a;用于在游戏中播放视频的组件。它提供了一系列属性来控制视频的播放、显示和交互。 1.Source&#xff08;视频源&#xff09;&#xff1a;用于指定视频的来源。可以选择两种不同的视频源类型&#xff1a; &#xff08;1&#xff09;Vieo Clip&#…

RDIFramework.NET CS敏捷开发框架 V6.0发布(支持.NET6+、Framework双引擎,全网唯一)

全新RDIFramework.NET V6.0 CS敏捷开发框架发布&#xff0c;全网唯一支持.NET6&#xff0c;Framework双引擎&#xff0c;降低开发成本&#xff0c;提高产品质量&#xff0c;提升用户体验与开发团队稳定性&#xff0c;做软件就选RDIFramework.NET开发框架。 1、RDIFramework.NET…

《网络是怎样连接的》(二.1)

(83条消息) 《网络是怎样连接的》&#xff08;一&#xff09;_qq_38480311的博客-CSDN博客 本文主要取材于 《网络是怎样连接的》 第二章。 目录 &#xff08;1&#xff09;创建套接字 &#xff08;2&#xff09;连接服务器 &#xff08;3&#xff09;收发数据 &#xf…

jdk,jre和jvm三者的关系和区别

目录 一、三者的关系 二、JDK的概念 三、JRE的概念 四、JVM的概念 五、三者区别 一、三者的关系 从图中可以清楚地看到&#xff0c;他们之间的关系是JDK包含JRE, JRE又包含JVM。 因此&#xff0c;JDK包含JRE和JVM。 JDK JRE Java 开发工具包 [Java,Javac,Javadoc,Javap…

【stm32L152】段码屏驱动注解、MX_LCD_Init()初始化失败的解决方法

文章目录 断码屏驱动补充MX_LCD_Init()驱动初始化失败 断码屏驱动补充 已经有大神写过较详细的教程&#xff1a;https://blog.csdn.net/CSDN_Gao_16/article/details/115463499&#xff0c;但这篇博文仍然比较抽象&#xff0c;我看了好多遍才看明白-_-||&#xff0c;为了节省和…

【Java基础教程】(四十四)IO篇 · 上:解析Java文件操作——File类、字节流与字符流,分析字节输出流、字节输入流、字符输出流和字符输入流的区别

Java基础教程之IO操作 上 &#x1f539;本节学习目标1️⃣ 文件操作类&#xff1a;File2️⃣ 字节流与字符流2.1 字节输出流&#xff1a;OutputStream2.2 字节输入流&#xff1a;InputStream2.3 字符输出流&#xff1a;Writer2.4 字符输入流&#xff1a;Reader2.5 字节流与字符…

电脑新装系统优化,win10优化,win10美化

公司发了新的笔记本&#xff0c;分为几步做 1.系统优化,碍眼的关掉。防火墙关掉、页面美化 2.安装必备软件及驱动 3.数据迁移 4.开发环境配置 目录 目录复制 这里写目录标题 目录1.系统优化关掉底部菜单栏花里胡哨 2.安装必备软件及驱动新电脑安装360 1.系统优化 关掉底部菜单…

es通过rest接口_search、_delete_by_query查询与删除数据

1、rest接口查询数据 rest查询: http://localhost:9200/index_name/_search 查询表达式&#xff1a; {"query": {"wildcard": {"accountID": {"value": "v*"}}} }postman请求截图&#xff1a; 2、使用Rest接口删除数据 …

re学习(23)BUUCTF 刮开有奖(中间变量的获取)

INT_PTR __stdcall DialogFunc(HWND hDlg, UINT a2, WPARAM a3, LPARAM a4) {const char *v4; // esiconst char *v5; // ediint v7[2]; // [esp8h] [ebp-20030h] BYREF 虽然看名称不连续&#xff0c;但是通过看偏移地址&#xff0c;可知&#xff0c;这些变量在内存中是连续的&…

iOS--属性关键字

定义 chat&#xff1a; 在iOS开发中&#xff0c;属性关键字是用于声明类的属性&#xff08;实例变量&#xff09;的修饰符。属性关键字可以影响属性的访问权限、内存管理和生成相关的getter和setter方法。 属性关键字有哪些&#xff1f; 分类属性关键字原子性atomic、nonato…

Android ConstraintLayout使用攻略

原文链接 Android ConstraintLayout使用攻略 ConstraintLayout是新一代的布局&#xff0c;它汲取了众家之长&#xff0c;把布局的概念进行了大统一&#xff0c;灵活且强大&#xff0c;基本上可以干掉以前所有的常用布局&#xff08;LinearLayout, RelativeLayout和FrameLayout…

【重点:单例模式】特殊类设计

请设计一个类&#xff0c;只能在堆上创建对象 方式如下&#xff1a; 将构造函数设置为私有&#xff0c;防止外部直接调用构造函数在栈上创建对象。向外部提供一个获取对象的static接口&#xff0c;该接口在堆上创建一个对象并返回。将拷贝构造函数设置为私有&#xff0c;并且…