目前各大主流浏览器都已经实现 WebSocket,本文主要是实现 server端的WebSocket服务。
完整代码见文章末尾,代码受到了很多优秀开源项目的影响,比如本次就借用了不知道传了第几手并非原创的发布订阅模式。
所以项目已加上了MIT协议,希望也能对其他人有所帮助。如果看完后觉得能有一点点的帮助,希望能随手点个赞或者star~
既然要手写WebSocket,那么首先需要了解它是什么?
一.WebSocket是什么?
WebSocket是一个通信协议,由IETF定义,标准为RFC 6455。那么我们这时候就去看看它的标准。
RFC 6455标准链接如下:www.rfc-editor.org/rfc/rfc6455…
首先看全文的摘要,摘要非常精炼,一共四句话。
第一句话说明它是一个双向通信协议。
第二句讲安全,是基于基于源的安全模型。
第三句表明它是基于消息帧,基于 TCP。所以我们可以导入http模块,用于创建WebSocket。
import http from 'node:http'
class WebSocket extends EventEmitter {
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
clients: Array<Client>
constructor({ port = 30104 }: { port: number }) {
super()
this.server = http.createServer();
this.server.listen(port);
this.clients = []
}
}
最后一句说明目的,为了解决不使用多个 http 连接,也能实现客户端和服务端的双向通信。
紧接着我们看下目录:
目录也很清晰,从背景,协议概览,握手,挥手等等开始介绍。但我们为了实现一个最基本的WebSocket通信,不用去看那么全,只需要关注通信的最基本三要素即可:
- 握手
- 数据传递
- 挥手
二.握手(Opening Handshake)
找到握手 Opening Handshake 这一章:www.rfc-editor.org/rfc/rfc6455…
这些是客户端的握手部分,可以后面再看,毕竟主流浏览器都已实现,我们要实现的server端WebSocket通信。
server端的握手部分比client端更加简单,只有以下几部分:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
而且这几部分全是固定值
翻译下Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=的意思是:
将字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 使用 SHA-1 加密,取 base64-encoded。
socket.write([
"HTTP/1.1 101 Switching Protocols",
"Upgrade: websocket",
"Connection: Upgrade",
"Sec-WebSocket-Accept: " + crypto.createHash("sha1").update(req.headers['sec-websocket-key'] + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest("base64")
].join("\n") + "\n\n");
其中 "Connection: Upgrade",所以
import http from 'node:http'
class WebSocket extends EventEmitter {
server: http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
clients: Array<Client>
constructor({ port = 30104 }: { port: number }) {
super()
this.server = http.createServer();
this.server.listen(port);
this.clients = []
this.server.on("upgrade", (req, socket) => this.create(req, socket))
}
}
三.数据传递(Data Framing)
我们跳转到 Data Framing 这一章
可以知道
WebSocket是通过帧(frames)来传递数据。- 如果是
client端向server通信,必须设置mask。如果是server端向client通信,不能设置mask。
既然是通过帧来传递,那就必须知道帧的结构
帧的结构文档中都有解释,这里翻译一下:
-
FIN:1bit指这是消息的最后一个片段 -
RSV1,RSV2,RSV3:1 bit each必须为0 -
Opcode:4 bits定义传输的数据内容的解释-
%x0表示延续帧
-
%x1表示文本帧
-
%x2表示二进制帧
-
%x3-7保留给更多的非控制帧
-
%x8表示连接关闭
-
%x9表示ping
-
%xA表示pong
-
%xB-F为进一步的控制帧保留
-
-
Mask:1 bit上面解释过,如果是server端向client端通信,不能设置,即为0。但如果是client向server端通信,需要设置为1 -
Payload length:7 bits, 7+16 bits, or 7+64 bits内容的大小,分三种情况。0-125表示内容小于7 bits,126表示内容大于7 bits但小于16 bits。如果是127,表示内容是64 bits。 -
Masking-key:0 or 4 bytes没有Mask自然也没有Masking-key。如果有的情况下,
将上述标准翻译成代码
const src = Buffer.from(source);
// FIN 只占 1bit,buffer[n]取的是1byte,1byte = 8 bits,所以要和128进行比较,比较第一位
fin: src[0] >= 128,
// opcode 占 4 bits,15等于1111,目的要取后4位,用来判断是结束还是文本还是一些其他信息
opcode: src[0] & 15,
// mask为 1 byte 的第一位
mask: src[1] >= 128
// payloadSize为 1 byte 的后7位,所以要位运算 127 ,取后面7位
payloadSize: src[1] & 127
// metaSize 的大小与 payloadSize 直接相关,多个if else 判断
metaSize
// maskKey 与 mask 和 metaSize 相关
maskKey: masked ? src.subarray(metaSize, metaSize + (masked ? 4 : 0)) : null,
payloadSize,帧大小,metaSize的大小的判断:
if (payloadSize === 127) {
size = src.readUInt32BE(6);
metaSize = 10;
} else if (payloadSize === 126) {
size = src.readUInt16BE(2);
metaSize = 4;
} else {
size = payloadSize;
metaSize = 2;
}
读取元数据,处理帧meta:
const handleFrameMeta = (meta: FrameMeta) => {
const metaLength = meta.metaSize
buf.subarray(0, metaLength)
buf = buf.subarray(metaLength);
return buf
}
成功读取了元数据后,就需要去读取数据的内容,数据的内容就比较简单读取,直接获取帧的大小,从metaSize到size的部分就是帧的内容
const handleFrameContent = (meta: FrameMeta) => {
const size = meta.size
const buffer = buf.subarray(0, size)
data = Buffer.concat([data, this.iMask(buffer, meta.maskKey)]);
// 需要判断下是否是最后一帧,这些信息都记录在`meta`中,如果是的话,说明读取完毕,`emit`数据。
if (meta.fin) {
if (meta.opcode === 8) return client.close();
if (meta.opcode === 9) return client.pong();
client.emit("data", data);
data = Buffer.allocUnsafe(0);
}
buf = buf.subarray(size);
}
四.挥手(Closing Handshake)
挥手比握手更加简单,只需要发送或者接收到一个关闭帧即可。比如发送 Opcode 关闭帧。
五.完整代码
代码地址和路径:github.com/chaxus/ran/…