本文代码:本文代码:
1.HTTP的架构模式
1.1. HTTP的特点
2. 双向通信
2.1 轮询
2.2 长轮询
2.3 iframe流
2.4 EventSource流
2.4.1 浏览器端
2.4.2 服务端
3.websocket
3.1 websocket 优势
3.2 websocket实战
3.2.1 服务端
3.2.2 客户端
3.3 如何建立连接
3.3.1 客户端:申请协议升级
3.3.2 服务端:响应协议升级
3.3.3 Sec-WebSocket-Accept的计算
3.3.4 Sec-WebSocket-Key/Accept的作用
3.4 数据帧格式
3.4.1 数据帧格式
3.4.2 掩码算法
3.4.3 服务器实战
1. HTTP的架构模式
Http是客户端/服务器模式中请求-响应所用的协议,在这种模式中,客户端(一般是web浏览器)向服务器提交HTTP请求,服务器响应请求的资源。
1.1. HTTP的特点
2. 双向通信
Comet是一种用于web的推送技术,能使服务器能实时地将更新的信息传送到客户端,而无须客户端发出请求,目前有三种实现方式:轮询(polling) 长轮询(long-polling)和iframe流(streaming)。
2.1 轮询
**index.html** Document
**app.js** let express = require("express"); let app = express(); // http://localhost:8000/ app.use(express.static(__dirname)); app.get("/clock", function (req, res) { res.send(new Date().toLocaleString()); }); app.listen(8000);
2.2 长轮询
**index.html** Document
**app.js** let express = require("express"); let app = express(); // http://localhost:8000/ app.use(express.static(__dirname)); app.get("/clock", function (req, res) { let $timer = setInterval(function () { let date = new Date(); let seconds = date.getSeconds(); if (seconds % 5 === 0) { res.send(date.toLocaleString()); clearInterval($timer); } }, 1000); }); app.listen(8000);
2.3 iframe流
通过在HTML页面里嵌入一个隐藏的iframe,然后将这个iframe的src属性设为对一个长连接的请求,服务器端就能源源不断地往客户推送数据。
**index.html** Document
**app.js** let express = require("express"); let app = express(); // http://localhost:8000/ app.use(express.static(__dirname)); app.get("/clock", function (req, res) { res.header("Content-Type", "text/html"); setInterval(function () { res.write(` `); }, 1000); }); app.listen(9001);
2.4 EventSource流
2.4.1 浏览器端 #
**index.html** Document
2.4.2 服务端 #
事件流的对应MIME格式为text/event-stream,而且其基于HTTP长连接。针对HTTP1.1规范默认采用长连接,针对HTTP1.0的服务器需要特殊设置。
event-source必须编码成utf-8的格式,消息的每个字段使用"\n"来做分割,并且需要下面4个规范定义好的字段:
Event: 事件类型
Data: 发送的数据
ID: 每一条事件流的ID
Retry: 告知浏览器在所有的连接丢失之后重新开启新的连接等待的时间,在自动重新连接的过程中,之前收到的最后一个事件流ID会被发送到服务端
**app.js** let express = require("express"); let app = express(); app.use(express.static(__dirname)); app.get("/clock", function (req, res) { res.header("Content-Type", "text/event-stream"); let counter = 0; let $timer = setInterval(function () { res.write( `id:${counter++}\nevent:abc\ndata:${new Date().toLocaleTimeString()}\n\n` ); }, 1000); res.on("close", function () { clearInterval($timer); }); }); app.listen(7777);
**app2.js** 使用ssestream库 let express = require("express"); let app = express(); app.use(express.static(__dirname)); //passThrough 通过流,它是一转换流 const SseStream = require("ssestream"); app.get("/clock", function (req, res) { let counter = 0; const sseStream = new SseStream(req); sseStream.pipe(res); const pusher = setInterval(function () { sseStream.write({ id: counter++, event: "message", retry: 2000, data: new Date().toString(), }); // 他内部会帮你转换成:event:message\nid:0\nretry:2000\ndata:2019年3月23日17:45:51\n\n }, 1000); res.on("close", function () { clearInterval(pusher); sseStream.unpipe(res); }); }); app.listen(7777);
3. websocket
3.1 websocket优势
3.2 websocket实战
3.2.1 服务端
**app.js** let express = require("express"); let app = express(); app.use(express.static(__dirname)); app.listen(3000); let websocketServer = require("ws").Server; let server = new websocketServer({ port: 8888 }); server.on("connection", (socket) => { console.log("2.服务器监听到了客户端请求"); socket.on("message", (message) => { console.log("4.客户端连接过来的消息", message); socket.send("5.服务器说" + message); }); });
3.2.2 客户端
**index.html** Document
3.3. 如何建立连接
WebSocket复用了HTTP的握手通道。具体指的是,客户端通过HTTP请求与WebSocket服务端协商升级协议。协议升级完成后,后续的数据交换则遵照WebSocket的协议。
3.3.1 客户端:申请协议升级
首先,客户端发起协议升级请求。可以看到,采用的是标准的HTTP报文格式,且只支持GET方法。
请求头:
GET ws://localhost:8888/ HTTP/1.1
Host: localhost:8888
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: IHfMdf8a0aQXbwQO1pkGdA==
3.3.2 服务端:响应协议升级
服务端返回内容如下,状态代码101表示协议切换。到此完成协议升级,后续的数据交互都按照新的协议来。
响应头:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: aWAY+V/uyz5ILZEoWuWdxjnlb7E=
3.3.3 Sec-WebSocket-Accept的计算
Sec-WebSocket-Accept根据客户端请求首部的Sec-WebSocket-Key计算出来。 计算公式为:
const crypto = require('crypto'); const CODE = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; const webSocketKey = 'IHfMdf8a0aQXbwQO1pkGdA=='; let websocketAccept = require('crypto').createHash('sha1').update(webSocketKey + CODE ).digest('base64'); console.log(websocketAccept);//aWAY+V/uyz5ILZEoWuWdxjnlb7E=
3.3.4 Sec-WebSocket-Key/Accept的作用
3.4 数据帧格式
WebSocket客户端、服务端通信的最小单位是帧,由1个或多个帧组成一条完整的消息(message)。
3.4.1 数据帧格式
单位是比特。比如FIN、RSV1各占据1比特,opcode占据4比特
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | Extended payload length | Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+
3.4.2 掩码算法
掩码键(Masking-key)是由客户端挑选出来的32位的随机数。掩码操作不会影响数据载荷的长度。掩码、反掩码操作都采用如下算法:
function maskOrUnmask(buffer, mask) { const length = buffer.length; for (let i = 0; i < length; i++) { buffer[i] ^= mask[i % 4]; } return buffer; } const mask = Buffer.from([0x12, 0x34, 0x56, 0x78]); // 随机写的字节数组 const buffer = Buffer.from([0x68, 0x65, 0x6c, 0x6c, 0x6f]); // 随机写的字节数组 const masked = maskOrUnmask(buffer, mask); // 掩码 console.log(masked); // const unmasked = maskOrUnmask(buffer, mask); // 反掩码 console.log(unmasked); // ,和buffer一致
3.4.3 服务器实战
**app2.js** let net = require("net"); // net模块用于创建tcp服务 let CODE = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; let crypto = require("crypto"); // 实现http协议升级到ws协议。模拟服务端响应头返回 /** * GET ws://localhost:8888/ HTTP/1.1/r/n Connection: Upgrade/r/n Upgrade: websocket/r/n Sec-WebSocket-Version: 13/r/n Sec-WebSocket-Key: O/SldTn2Th7GfsD07IxrwQ==/r/n /r/n */ /** * HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: H8BlFmSUnXVpM4+scTXjZIwFjzs= */ let server = net.createServer((socket) => { socket.once("data", (data) => { // 建立连接,使用once data = data.toString(); // buffer转字符串 if (data.match(/Connection: Upgrade/)) { let rows = data.split("\r\n"); // rows格式: // 'GET / HTTP/1.1', // 'Host: localhost:9999', // 'Connection: Upgrade', // 'Pragma: no-cache', // 'Cache-Control: no-cache', // 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36', // 'Upgrade: websocket', // 'Origin: null', // 'Sec-WebSocket-Version: 13', // 'Accept-Encoding: gzip, deflate, br, zstd', // 'Accept-Language: zh-CN,zh;q=0.9', // 'Sec-WebSocket-Key: MwGApjw5wYjUrCrC2Rr1Cg==', // 'Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits', // '', // '' rows = rows.slice(1, -2); let headers = {}; rows.reduce((memo, item) => { let [key, value] = item.split(": "); memo[key] = value; return memo; }, headers); if (headers["Sec-WebSocket-Version"] == "13") { let secWebSocketKey = headers["Sec-WebSocket-Key"]; let secWebSocketAccept = crypto .createHash("sha1") .update(secWebSocketKey + CODE) .digest("base64"); let response = [ "HTTP/1.1 101 Switching Protocols", "Upgrade: websocket", "Connection: Upgrade", `Sec-WebSocket-Accept: ${secWebSocketAccept}`, "\r\n", ].join("\r\n"); socket.write(response); //后面所有的格式都是基于websocket协议的 socket.on("data", (buffers) => { // 通讯,使用on // data默认是一个Buffer let fin = buffers[0] & (0b10000000 === 0b10000000); // 结束位是true还是false,第0个字节第一位 let opcode = buffers[0] & 0b00001111; // 操作码,第0个字节后4位 let isMask = buffers[1] & (0b10000000 == 0b10000000); // 是否进行了掩码 let payloadLength = buffers[1] & 0b01111111; // 获得第1个字节后7位 let mask = buffers.slice(2, 6); // 掩码键,这里假设payloadLength是7位,下面根据这个来写代码 let payload = buffers.slice(6); // 携带的真实数据 payload = maskOrUnmask(payload, mask); let response = Buffer.alloc(2 + payload.length); response[0] = 0b10000000 | opcode; response[1] = payloadLength; payload.copy(response, 2); // 将 payload 的内容复制到 response 的第二个字节开始的位置,等于把客户端的消息又传了回去 socket.write(response); }); } } }); }); server.listen(9999); function maskOrUnmask(buffer, mask) { const length = buffer.length; for (let i = 0; i < length; i++) { buffer[i] ^= mask[i % 4]; } return buffer; }
**index.html** Document
上一篇:Renesa Version Board开发RT-Thread 之超声波测距模块(HC-SR04)
下一篇:使用API Monitor探测QQ安装包在创建桌面快捷方式时都调用了哪些API及COM接口,去解决C++程序安装包中的问题