本文由作者“阿宝哥”分享,原题“你不知道的 WebSocket”,即时通讯网有修订和改动,感谢作者的分享。
cover-opti.png (13.27 KB, 下载次数: 1182)
下载附件 保存到相册
3 年前 上传
1-1.png (46.7 KB, 下载次数: 1172)
1-2.png (53.64 KB, 下载次数: 1123)
ws://echo.websocket.org wss://echo.websocket.org
1-3.png (35.93 KB, 下载次数: 1173)
2-1.png (26.11 KB, 下载次数: 1123)
const myWebSocket = new WebSocket(url [, protocols]);
2-2.png (15.36 KB, 下载次数: 1243)
2-3.png (13.57 KB, 下载次数: 1159)
// const socket = new WebSocket("ws://echo.websocket.org"); // const sendMsgContainer = document.querySelector("#sendMessage"); function send() { const message = sendMsgContainer.value; if (socket.readyState !== WebSocket.OPEN) { console.log("连接未建立,还不能发送消息"); return; } if (message) socket.send(message); }
// const socket = new WebSocket("ws://echo.websocket.org"); // const receivedMsgContainer = document.querySelector("#receivedMessage"); socket.addEventListener("message", function (event) { console.log("Message from server ", event.data); receivedMsgContainer.value = event.data; });
2-4.png (35.71 KB, 下载次数: 1222)
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>WebSocket 发送普通文本示例</title> <style> .block { flex: 1; } </style> </head> <body> <h3>WebSocket 发送普通文本示例</h3> <div style="display: flex;"> <div class="block"> <p>即将发送的数据:<button>发送</button></p> <textarea id="sendMessage" rows="5" cols="15"></textarea> </div> <div class="block"> <p>接收的数据:</p> <textarea id="receivedMessage" rows="5" cols="15"></textarea> </div> </div> <script> const sendMsgContainer = document.querySelector("#sendMessage"); const receivedMsgContainer = document.querySelector("#receivedMessage"); const socket = new WebSocket("ws://echo.websocket.org"); // 监听连接成功事件 socket.addEventListener("open", function (event) { console.log("连接成功,可以开始通讯"); }); // 监听消息 socket.addEventListener("message", function (event) { console.log("Message from server ", event.data); receivedMsgContainer.value = event.data; }); function send() { const message = sendMsgContainer.value; if (socket.readyState !== WebSocket.OPEN) { console.log("连接未建立,还不能发送消息"); return; } if (message) socket.send(message); } </script> </body> </html>
const socket = new WebSocket("ws://echo.websocket.org"); socket.onopen = function () { // 发送UTF-8编码的文本信息 socket.send("Hello Echo Server!"); // 发送UTF-8编码的JSON数据 socket.send(JSON.stringify({ msg: "我是阿宝哥" })); // 发送二进制ArrayBuffer const buffer = new ArrayBuffer(128); socket.send(buffer); // 发送二进制ArrayBufferView const intview = new Uint32Array(buffer); socket.send(intview); // 发送二进制Blob const blob = new Blob([buffer]); socket.send(blob); };
2-5.png (43.54 KB, 下载次数: 1172)
Blob(Binary Large Object)表示二进制类型的大对象。在数据库管理系统中,将二进制数据存储为一个单一个体的集合。Blob 通常是影像、声音或多媒体文件。在 JavaScript 中 Blob 类型的对象表示不可变的类似文件对象的原始数据。
2-6.png (18.5 KB, 下载次数: 1154)
// const socket = new WebSocket("ws://echo.websocket.org"); // const sendMsgContainer = document.querySelector("#sendMessage"); function send() { const message = sendMsgContainer.value; if (socket.readyState !== WebSocket.OPEN) { console.log("连接未建立,还不能发送消息"); return; } const blob = new Blob([message], { type: "text/plain" }); if (message) socket.send(blob); console.log(`未发送至服务器的字节数:${socket.bufferedAmount}`); }
// const socket = new WebSocket("ws://echo.websocket.org"); // const receivedMsgContainer = document.querySelector("#receivedMessage"); socket.addEventListener("message", async function (event) { console.log("Message from server ", event.data); const receivedData = event.data; if (receivedData instanceof Blob) { receivedMsgContainer.value = await receivedData.text(); } else { receivedMsgContainer.value = receivedData; } });
2-7.png (44.51 KB, 下载次数: 1222)
<!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>WebSocket 发送二进制数据示例</title> <style> .block { flex: 1; } </style> </head> <body> <h3>WebSocket 发送二进制数据示例</h3> <div style="display: flex;"> <div class="block"> <p>待发送的数据:<button>发送</button></p> <textarea id="sendMessage" rows="5" cols="15"></textarea> </div> <div class="block"> <p>接收的数据:</p> <textarea id="receivedMessage" rows="5" cols="15"></textarea> </div> </div> <script> const sendMsgContainer = document.querySelector("#sendMessage"); const receivedMsgContainer = document.querySelector("#receivedMessage"); const socket = new WebSocket("ws://echo.websocket.org"); // 监听连接成功事件 socket.addEventListener("open", function (event) { console.log("连接成功,可以开始通讯"); }); // 监听消息 socket.addEventListener("message", async function (event) { console.log("Message from server ", event.data); const receivedData = event.data; if (receivedData instanceof Blob) { receivedMsgContainer.value = await receivedData.text(); } else { receivedMsgContainer.value = receivedData; } }); function send() { const message = sendMsgContainer.value; if (socket.readyState !== WebSocket.OPEN) { console.log("连接未建立,还不能发送消息"); return; } const blob = new Blob([message], { type: "text/plain" }); if (message) socket.send(blob); console.log(`未发送至服务器的字节数:${socket.bufferedAmount}`); } </script> </body> </html>
3-1.png (40.74 KB, 下载次数: 1175)
GET ws://echo.websocket.org/ HTTP/1.1 Host: echo.websocket.org Origin: file:// Connection: Upgrade Upgrade: websocket Sec-WebSocket-Version: 13 Sec-WebSocket-Key: Zx8rNEkBE4xnwifpuh8DHQ== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
HTTP/1.1 101 Web Socket Protocol Handshake ① Connection: Upgrade ② Upgrade: websocket ③ Sec-WebSocket-Accept: 52Rg3vW4JQ1yWpkvFlsTsiezlqw= ④
const http = require("http"); const port = 8888; const { generateAcceptValue } = require("./util"); const server = http.createServer((req, res) => { res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" }); res.end("大家好,我是阿宝哥。感谢你阅读“你不知道的WebSocket”"); }); server.on("upgrade", function (req, socket) { if (req.headers["upgrade"] !== "websocket") { socket.end("HTTP/1.1 400 Bad Request"); return; } // 读取客户端提供的Sec-WebSocket-Key const secWsKey = req.headers["sec-websocket-key"]; // 使用SHA-1算法生成Sec-WebSocket-Accept const hash = generateAcceptValue(secWsKey); // 设置HTTP响应头 const responseHeaders = [ "HTTP/1.1 101 Web Socket Protocol Handshake", "Upgrade: WebSocket", "Connection: Upgrade", `Sec-WebSocket-Accept: ${hash}`, ]; // 返回握手请求的响应信息 socket.write(responseHeaders.join("\r\n") + "\r\n\r\n"); }); server.listen(port, () => console.log(`Server running at http://localhost:${port}`) );
// util.js const crypto = require("crypto"); const MAGIC_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; function generateAcceptValue(secWsKey) { return crypto .createHash("sha1") .update(secWsKey + MAGIC_KEY, "utf8") .digest("base64"); }
3-2.png (33.6 KB, 下载次数: 1208)
WebSocket connection to 'ws://localhost:8888/' failed: Error during WebSocket handshake: Incorrect 'Sec-WebSocket-Accept' header value
// 从请求头中读取子协议 const protocol = req.headers["sec-websocket-protocol"]; // 如果包含子协议,则解析子协议 const protocols = !protocol ? [] : protocol.split(",").map((s) => s.trim()); // 简单起见,我们仅判断是否含有JSON子协议 if (protocols.includes("json")) { responseHeaders.push(`Sec-WebSocket-Protocol: json`); }
3-3.png (42.06 KB, 下载次数: 1140)
3-4.png (76.99 KB, 下载次数: 1161)
j = i MOD 4 transformed-octet-i = original-octet-i XOR masking-key-octet-j
E6 88 91 E6 98 AF E9 98 BF E5 AE 9D E5 93 A5
let uint8 = new Uint8Array([0xE6, 0x88, 0x91, 0xE6, 0x98, 0xAF, 0xE9, 0x98, 0xBF, 0xE5, 0xAE, 0x9D, 0xE5, 0x93, 0xA5]); let maskingKey = new Uint8Array([0x08, 0xf6, 0xef, 0xb1]); let maskedUint8 = new Uint8Array(uint8.length); for (let i = 0, j = 0; i < uint8.length; i++, j = i % 4) { maskedUint8[i ] = uint8[i ] ^ maskingKey[j]; } console.log(Array.from(maskedUint8).map(num=>Number(num).toString(16)).join(' '));
ee 7e 7e 57 90 59 6 29 b7 13 41 2c ed 65 4a
3-5.png (42.87 KB, 下载次数: 1217)
Client: FIN=1, opcode=0x1, msg="hello" Server: (process complete message immediately) Hi. Client: FIN=0, opcode=0x1, msg="and a" Server: (listening, new message containing text started) Client: FIN=0, opcode=0x0, msg="happy new" Server: (listening, payload concatenated to previous message) Client: FIN=1, opcode=0x0, msg="year!" Server: (process complete message) Happy new year to you too!
function parseMessage(buffer) { // 第一个字节,包含了FIN位,opcode, 掩码位 const firstByte = buffer.readUInt8(0); // [FIN, RSV, RSV, RSV, OPCODE, OPCODE, OPCODE, OPCODE]; // 右移7位取首位,1位,表示是否是最后一帧数据 const isFinalFrame = Boolean((firstByte >>> 7) & 0x01); console.log("isFIN: ", isFinalFrame); // 取出操作码,低四位 /** * %x0:表示一个延续帧。当 Opcode 为 0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片; * %x1:表示这是一个文本帧(text frame); * %x2:表示这是一个二进制帧(binary frame); * %x3-7:保留的操作代码,用于后续定义的非控制帧; * %x8:表示连接断开; * %x9:表示这是一个心跳请求(ping); * %xA:表示这是一个心跳响应(pong); * %xB-F:保留的操作代码,用于后续定义的控制帧。 */ const opcode = firstByte & 0x0f; if (opcode === 0x08) { // 连接关闭 return; } if (opcode === 0x02) { // 二进制帧 return; } if (opcode === 0x01) { // 目前只处理文本帧 let offset = 1; const secondByte = buffer.readUInt8(offset); // MASK: 1位,表示是否使用了掩码,在发送给服务端的数据帧里必须使用掩码,而服务端返回时不需要掩码 const useMask = Boolean((secondByte >>> 7) & 0x01); console.log("use MASK: ", useMask); const payloadLen = secondByte & 0x7f; // 低7位表示载荷字节长度 offset += 1; // 四个字节的掩码 let MASK = []; // 如果这个值在0-125之间,则后面的4个字节(32位)就应该被直接识别成掩码; if (payloadLen <= 0x7d) { // 载荷长度小于125 MASK = buffer.slice(offset, 4 + offset); offset += 4; console.log("payload length: ", payloadLen); } else if (payloadLen === 0x7e) { // 如果这个值是126,则后面两个字节(16位)内容应该,被识别成一个16位的二进制数表示数据内容大小; console.log("payload length: ", buffer.readInt16BE(offset)); // 长度是126, 则后面两个字节作为payload length,32位的掩码 MASK = buffer.slice(offset + 2, offset + 2 + 4); offset += 6; } else { // 如果这个值是127,则后面的8个字节(64位)内容应该被识别成一个64位的二进制数表示数据内容大小 MASK = buffer.slice(offset + 8, offset + 8 + 4); offset += 12; } // 开始读取后面的payload,与掩码计算,得到原来的字节内容 const newBuffer = []; const dataBuffer = buffer.slice(offset); for (let i = 0, j = 0; i < dataBuffer.length; i++, j = i % 4) { const nextBuf = dataBuffer[i ]; newBuffer.push(nextBuf ^ MASK[j]); } return Buffer.from(newBuffer).toString(); } return ""; }
server.on("upgrade", function (req, socket) { socket.on("data", (buffer) => { const message = parseMessage(buffer); if (message) { console.log("Message from client:" + message); } else if (message === null) { console.log("WebSocket connection closed by the client."); } }); if (req.headers["upgrade"] !== "websocket") { socket.end("HTTP/1.1 400 Bad Request"); return; } // 省略已有代码 });
Server running at http://localhost:8888 isFIN: true use MASK: true payload length: 15 Message from client:我是阿宝哥
function constructReply(data) { const json = JSON.stringify(data); const jsonByteLength = Buffer.byteLength(json); // 目前只支持小于65535字节的负载 const lengthByteCount = jsonByteLength < 126 ? 0 : 2; const payloadLength = lengthByteCount === 0 ? jsonByteLength : 126; const buffer = Buffer.alloc(2 + lengthByteCount + jsonByteLength); // 设置数据帧首字节,设置opcode为1,表示文本帧 buffer.writeUInt8(0b10000001, 0); buffer.writeUInt8(payloadLength, 1); // 如果payloadLength为126,则后面两个字节(16位)内容应该,被识别成一个16位的二进制数表示数据内容大小 let payloadOffset = 2; if (lengthByteCount > 0) { buffer.writeUInt16BE(jsonByteLength, 2); payloadOffset += lengthByteCount; } // 把JSON数据写入到Buffer缓冲区中 buffer.write(json, payloadOffset); return buffer; }
server.on("upgrade", function (req, socket) { socket.on("data", (buffer) => { const message = parseMessage(buffer); if (message) { console.log("Message from client:" + message); // 新增以下👇代码 socket.write(constructReply({ message })); } else if (message === null) { console.log("WebSocket connection closed by the client."); } }); });
3-6.png (37.58 KB, 下载次数: 986)
const http = require("http"); const port = 8888; const { generateAcceptValue, parseMessage, constructReply } = require("./util"); const server = http.createServer((req, res) => { res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" }); res.end("大家好,我是阿宝哥。感谢你阅读“你不知道的WebSocket”"); }); server.on("upgrade", function (req, socket) { socket.on("data", (buffer) => { const message = parseMessage(buffer); if (message) { console.log("Message from client:" + message); socket.write(constructReply({ message })); } else if (message === null) { console.log("WebSocket connection closed by the client."); } }); if (req.headers["upgrade"] !== "websocket") { socket.end("HTTP/1.1 400 Bad Request"); return; } // 读取客户端提供的Sec-WebSocket-Key const secWsKey = req.headers["sec-websocket-key"]; // 使用SHA-1算法生成Sec-WebSocket-Accept const hash = generateAcceptValue(secWsKey); // 设置HTTP响应头 const responseHeaders = [ "HTTP/1.1 101 Web Socket Protocol Handshake", "Upgrade: WebSocket", "Connection: Upgrade", `Sec-WebSocket-Accept: ${hash}`, ]; // 返回握手请求的响应信息 socket.write(responseHeaders.join("\r\n") + "\r\n\r\n"); }); server.listen(port, () => console.log(`Server running at http://localhost:${port}`) );
const crypto = require("crypto"); const MAGIC_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; function generateAcceptValue(secWsKey) { return crypto .createHash("sha1") .update(secWsKey + MAGIC_KEY, "utf8") .digest("base64"); } function parseMessage(buffer) { // 第一个字节,包含了FIN位,opcode, 掩码位 const firstByte = buffer.readUInt8(0); // [FIN, RSV, RSV, RSV, OPCODE, OPCODE, OPCODE, OPCODE]; // 右移7位取首位,1位,表示是否是最后一帧数据 const isFinalFrame = Boolean((firstByte >>> 7) & 0x01); console.log("isFIN: ", isFinalFrame); // 取出操作码,低四位 /** * %x0:表示一个延续帧。当 Opcode 为 0 时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片; * %x1:表示这是一个文本帧(text frame); * %x2:表示这是一个二进制帧(binary frame); * %x3-7:保留的操作代码,用于后续定义的非控制帧; * %x8:表示连接断开; * %x9:表示这是一个心跳请求(ping); * %xA:表示这是一个心跳响应(pong); * %xB-F:保留的操作代码,用于后续定义的控制帧。 */ const opcode = firstByte & 0x0f; if (opcode === 0x08) { // 连接关闭 return; } if (opcode === 0x02) { // 二进制帧 return; } if (opcode === 0x01) { // 目前只处理文本帧 let offset = 1; const secondByte = buffer.readUInt8(offset); // MASK: 1位,表示是否使用了掩码,在发送给服务端的数据帧里必须使用掩码,而服务端返回时不需要掩码 const useMask = Boolean((secondByte >>> 7) & 0x01); console.log("use MASK: ", useMask); const payloadLen = secondByte & 0x7f; // 低7位表示载荷字节长度 offset += 1; // 四个字节的掩码 let MASK = []; // 如果这个值在0-125之间,则后面的4个字节(32位)就应该被直接识别成掩码; if (payloadLen <= 0x7d) { // 载荷长度小于125 MASK = buffer.slice(offset, 4 + offset); offset += 4; console.log("payload length: ", payloadLen); } else if (payloadLen === 0x7e) { // 如果这个值是126,则后面两个字节(16位)内容应该,被识别成一个16位的二进制数表示数据内容大小; console.log("payload length: ", buffer.readInt16BE(offset)); // 长度是126, 则后面两个字节作为payload length,32位的掩码 MASK = buffer.slice(offset + 2, offset + 2 + 4); offset += 6; } else { // 如果这个值是127,则后面的8个字节(64位)内容应该被识别成一个64位的二进制数表示数据内容大小 MASK = buffer.slice(offset + 8, offset + 8 + 4); offset += 12; } // 开始读取后面的payload,与掩码计算,得到原来的字节内容 const newBuffer = []; const dataBuffer = buffer.slice(offset); for (let i = 0, j = 0; i < dataBuffer.length; i++, j = i % 4) { const nextBuf = dataBuffer[i ]; newBuffer.push(nextBuf ^ MASK[j]); } return Buffer.from(newBuffer).toString(); } return ""; } function constructReply(data) { const json = JSON.stringify(data); const jsonByteLength = Buffer.byteLength(json); // 目前只支持小于65535字节的负载 const lengthByteCount = jsonByteLength < 126 ? 0 : 2; const payloadLength = lengthByteCount === 0 ? jsonByteLength : 126; const buffer = Buffer.alloc(2 + lengthByteCount + jsonByteLength); // 设置数据帧首字节,设置opcode为1,表示文本帧 buffer.writeUInt8(0b10000001, 0); buffer.writeUInt8(payloadLength, 1); // 如果payloadLength为126,则后面两个字节(16位)内容应该,被识别成一个16位的二进制数表示数据内容大小 let payloadOffset = 2; if (lengthByteCount > 0) { buffer.writeUInt16BE(jsonByteLength, 2); payloadOffset += lengthByteCount; } // 把JSON数据写入到Buffer缓冲区中 buffer.write(json, payloadOffset); return buffer; } module.exports = { generateAcceptValue, parseMessage, constructReply, };
4-1.png (54.45 KB, 下载次数: 1019)
4-2.png (45.99 KB, 下载次数: 997)
Socket 的英文原义是“孔”或“插座”:作为 BSD UNIX 的进程通信机制,取后一种意思。通常也称作”套接字“,用于描述IP地址和端口,是一个通信链的句柄,可以用来实现不同虚拟机或不同计算机之间的通信。 在Internet 上的主机一般运行了多个服务软件,同时提供几种服务。每种服务都打开一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务。Socket 正如其英文原义那样,像一个多孔插座。一台主机犹如布满各种插座的房间,每个插座有一个编号,有的插座提供 220 伏交流电, 有的提供 110 伏交流电,有的则提供有线电视节目。 客户软件将插头插到不同编号的插座,就可以得到不同的服务。
4-3.png (26.14 KB, 下载次数: 974)
来源:即时通讯网 - 即时通讯开发者社区!
轻量级开源移动端即时通讯框架。
快速入门 / 性能 / 指南 / 提问
轻量级Web端即时通讯框架。
详细介绍 / 精编源码 / 手册教程
移动端实时音视频框架。
详细介绍 / 性能测试 / 安装体验
基于MobileIMSDK的移动IM系统。
详细介绍 / 产品截图 / 安装体验
一套产品级Web端IM系统。
详细介绍 / 产品截图 / 演示视频
引用此评论
精华主题数超过100个。
连续任职达2年以上的合格正式版主
为论区做出突出贡献的开发者、版主等。
Copyright © 2014-2024 即时通讯网 - 即时通讯开发者社区 / 版本 V4.4
苏州网际时代信息科技有限公司 (苏ICP备16005070号-1)
Processed in 0.128911 second(s), 37 queries , Gzip On.