南通专业网站建设,舆情信息报告范文,河北网站建设,网站建设交流推广目录
引言
从 http 到 websocekt 的切换
Sec-WebSocket-Key 与 Sec-WebSocket-Accept
全新的二进制协议
自己实现一个 websocket 服务器
按照协议格式解析收到的Buffer
取出opcode
取出MASK与payload长度
根据mask key读取数据
根据类型处理数据
frame 帧
数据的发…目录
引言
从 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: JkE58n3uIigYDMvCKsBbGZsp1A和请求 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 是 XRfPnS8xl11QWZherej/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
bodyscriptconst 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
bodyscriptconst 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 的理解更深了呢