这篇文章给学校组织上课的一部分,同时也是为了帮我补充以前没注意的问题。
以前都没怎么了解过 Websocket,最多也是用过 pusher 之类的成套解决方案,所以这里就对 Websocket 发生的整个过程做一个了解。这里使用原生搭建,因为 nodejs 的 WebSocket 很原始,需要自己做很多事情。
1. 服务端创建一个Net服务器
// 引入net模块
const net = require('net')
// 使用net模块创建服务器,返回的是一个原始的socket对象
const server = net.createServer((socket) => {
})
server.listen(8002, () => {
console.log('Runing at: ws://localhost:8002')
})
在你的页面的 script 标签中写上
const ws = new WebSocket('ws://localhost:8080/')
2. Web端创建一个WebSocket链接
创建一个 WebSocket 连接,此时控制台的 Network 模块可以看到一个处于pending状态的HTTP连接。
这个连接是一个 HTTP 请求,与普通HTTP请求的请求你头相比,增加了以下内容:
- Sec-WebSocket-Extensions: permessage-deflate; clientmaxwindow_bits // 扩展信息
- Sec-WebSocket-Key: D27bMuu0x12Q2VJ7iE0ANw== // 发送一个Key到服务端,用于校验服务端是否支持WebSocket
- Sec-WebSocket-Version: 13 // WebSocket 版本
- Upgrade: websocket // 告知服务器通信协议将会升级到 WebSocket 若服务器支持则继续下一步
3. 服务端使用socket.once,触发一次data事件处理HTTP请求头数据
socket.once('data', (buffer) => {
// 接收到HTTP请求头数据
const str = buffer.toString()
console.log(str)
})
打印结果如下:
Runing at: ws://localhost:8002
GET / HTTP/1.1
Host: localhost:8002
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/80.0.3987.132 Safari/537.36 Edg/80.0.361.66
Upgrade: websocket
Origin: http://127.0.0.1:5500
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Cookie: xxx
Sec-WebSocket-Key: p9koDI+fzMQI/LAka5jzng==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
和以前的一样我们将换行符转换之后就成了这样
Runing at: ws://localhost:8002\r\nGET / HTTP/1.1\r\nHost: localhost:8002\r\n...
就可以通过以下的函数得到 header 信息
const parseHeader = str => {
// 将请求头数据按回车符切割为数组,得到每一行数据
let arr = str.split('\r\n').filter(item => item)
// 第一行数据为GET / HTTP/1.1,可以丢弃。
arr.shift()
let headers = {} // 存储最终处理的数据
arr.forEach((item) => {
// 需要用":"将数组切割成key和value
let [name, value] = item.split(':')
// 去除无用的空格,将属性名转为小写
name = name.toLowerCase()
value = value.trim()
// 获取所有的请求头属性
headers[name] = value
})
return headers
}
5. 根据请求头参数,判断是否WebSocket请求
- 根据
headers['upgrade'] !== 'websocket'
,判断该HTTP连接是否可升级为 WebSocket,若可以升级,表示为WebSocket 请求。 - 根据
headers['sec-websocket-version'] !== '13'
,判断WebSocket的版本是否为 13,以免因为版本不同出现兼容问题。
socket.once('data', buffer => {
// 接收到HTTP请求头数据
const str = buffer.toString()
// 将请求头数据转为对象
const headers = parseHeader(str)
// 判断请求是否为WebSocket连接
if (headers['upgrade'] !== 'websocket') {
// 若当前请求不是WebSocket连接,则关闭连接
console.log('非 WebSocket 连接')
socket.end()
} else if (headers['sec-websocket-version'] !== '13') {
// 判断WebSocket版本是否为13,防止是其他版本,造成兼容错误
console.log('WebSocket 版本错误')
socket.end()
} else {
// 请求为WebSocket连接时,进一步处理
}
})
6. 校验Sec-WebSocket-Key,完成连接
根据协议规定的方式,向前端返回一个请求头,完成建立 WebSocket 连接的过程。
若客户端校验结果正确,在控制台的 Network 模块可以看到HTTP请求的状态码变为101 Switching Protocols,同时客户端的 ws.onopen 事件被触发。
// 校验Sec-WebSocket-Key,完成连接
/*
协议中规定的校验用GUID,可参考如下链接:
https://tools.ietf.org/html/rfc6455#section-5.5.2
https://stackoverflow.com/questions/13456017/what-does-258eafa5-e914-47da-95ca-c5ab0dc85b11-means-in-websocket-protocol
*/
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
const key = headers['sec-websocket-key']
const hash = crypto.createHash('sha1') // 创建一个签名算法为sha1的哈希对象
hash.update(`${key}${GUID}`) // 将key和GUID连接后,更新到hash
const result = hash.digest('base64') // 生成base64字符串
const header = `HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-Websocket-Accept: ${result}\r\n\r\n` // 生成供前端校验用的请求头
socket.write(header) // 返回HTTP头,告知客户端校验结果,HTTP状态码101表示切换协议:https://httpstatuses.com/101。
// 若客户端校验结果正确,在控制台的 Network 模块可以看到HTTP请求的状态码变为 101 Switching Protocols,同时客户端的ws.onopen事件被触发。
// 将建立连接的客户端推到数组中
list.push(socket)
console.log(list.length)
// 处理聊天数据
7. 建立连接后,通过data事件接收客户端的数据并处理
连接开始后,可以在控制台的 Network 模块看到,该连接会一直保留在 pending 状态,直到连接断开。
因为这里使用的是原生,所以数据帧也要我们自己解析,有兴趣的同学可以稍微看一下(我反正只能说抱歉了)
此时可以通过 data 事件处理客户端的数据,但此时双方通信的数据为二进制,需要按照其格式进行处理后才可以正常使用。
处理收到的数据:
// 处理收到的数据
function decodeWsFrame(data) {
let start = 0;
let frame = {
isFinal: (data[start] & 0x80) === 0x80,
opcode: data[start++] & 0xF,
masked: (data[start] & 0x80) === 0x80,
payloadLen: data[start++] & 0x7F,
maskingKey: '',
payloadData: null
};
if (frame.payloadLen === 126) {
frame.payloadLen = (data[start++] << 8) + data[start++];
} else if (frame.payloadLen === 127) {
frame.payloadLen = 0;
for (let i = 7; i >= 0; --i) {
frame.payloadLen += (data[start++] << (i * 8));
}
}
if (frame.payloadLen) {
if (frame.masked) {
const maskingKey = [
data[start++],
data[start++],
data[start++],
data[start++]
];
frame.maskingKey = maskingKey;
frame.payloadData = data
.slice(start, start + frame.payloadLen)
.map((byte, idx) => byte ^ maskingKey[idx % 4]);
} else {
frame.payloadData = data.slice(start, start + frame.payloadLen);
}
}
return frame;
}
处理发出的数据:
function encodeWsFrame(data) {
const isFinal = data.isFinal !== undefined ? data.isFinal : true,
opcode = data.opcode !== undefined ? data.opcode : 1,
payloadData = data.payloadData ? Buffer.from(data.payloadData) : null,
payloadLen = payloadData ? payloadData.length : 0;
let frame = [];
if (isFinal) frame.push((1 << 7) + opcode);
else frame.push(opcode);
if (payloadLen < 126) {
frame.push(payloadLen);
} else if (payloadLen < 65536) {
frame.push(126, payloadLen >> 8, payloadLen & 0xFF);
} else {
frame.push(127);
for (let i = 7; i >= 0; --i) {
frame.push((payloadLen & (0xFF << (i * 8))) >> (i * 8));
}
}
frame = payloadData ? Buffer.concat([Buffer.from(frame), payloadData]) : Buffer.from(frame);
return frame;
}
发送给客户端 (这里的广播处理尽量简单)
const data = decodeWsFrame(buffer)
// opcode为8,表示客户端发起了断开连接
if (data.opcode === 8) {
socket.end() // 与客户端断开连接
} else {
// 接收到客户端数据时的处理,此处默认为返回接收到的数据。
// 更新那些客户端还在
list = list.filter(item => !item.destroyed)
list.forEach(item => {
item.write(encodeWsFrame({ payloadData: `服务端接收到的消息为:${data.payloadData ? data.payloadData.toString() : ''}` }))
})
}
完整代码
// 引入net模块
const net = require('net')
const crypto = require('crypto')
// 处理收到的数据
function decodeWsFrame(data) {
let start = 0;
let frame = {
isFinal: (data[start] & 0x80) === 0x80,
opcode: data[start++] & 0xF,
masked: (data[start] & 0x80) === 0x80,
payloadLen: data[start++] & 0x7F,
maskingKey: '',
payloadData: null
};
if (frame.payloadLen === 126) {
frame.payloadLen = (data[start++] << 8) + data[start++];
} else if (frame.payloadLen === 127) {
frame.payloadLen = 0;
for (let i = 7; i >= 0; --i) {
frame.payloadLen += (data[start++] << (i * 8));
}
}
if (frame.payloadLen) {
if (frame.masked) {
const maskingKey = [
data[start++],
data[start++],
data[start++],
data[start++]
];
frame.maskingKey = maskingKey;
frame.payloadData = data
.slice(start, start + frame.payloadLen)
.map((byte, idx) => byte ^ maskingKey[idx % 4]);
} else {
frame.payloadData = data.slice(start, start + frame.payloadLen);
}
}
return frame;
}
// 处理发出的数据
function encodeWsFrame(data) {
const isFinal = data.isFinal !== undefined ? data.isFinal : true,
opcode = data.opcode !== undefined ? data.opcode : 1,
payloadData = data.payloadData ? Buffer.from(data.payloadData) : null,
payloadLen = payloadData ? payloadData.length : 0;
let frame = [];
if (isFinal) frame.push((1 << 7) + opcode);
else frame.push(opcode);
if (payloadLen < 126) {
frame.push(payloadLen);
} else if (payloadLen < 65536) {
frame.push(126, payloadLen >> 8, payloadLen & 0xFF);
} else {
frame.push(127);
for (let i = 7; i >= 0; --i) {
frame.push((payloadLen & (0xFF << (i * 8))) >> (i * 8));
}
}
frame = payloadData ? Buffer.concat([Buffer.from(frame), payloadData]) : Buffer.from(frame);
return frame;
}
const parseHeader = str => {
// 将请求头数据按回车符切割为数组,得到每一行数据
let arr = str.split('\r\n').filter(item => item)
// 第一行数据为GET / HTTP/1.1,可以丢弃。
arr.shift()
let headers = {} // 存储最终处理的数据
arr.forEach((item) => {
// 需要用":"将数组切割成key和value
let [name, value] = item.split(':')
// 去除无用的空格,将属性名转为小写
name = name.toLowerCase()
value = value.trim()
// 获取所有的请求头属性
headers[name] = value
})
return headers
}
// 在内存里面存储一个连接服务器的列表
let list = [];
// 使用net模块创建服务器,返回的是一个原始的socket对象,与Socket.io的socket对象不同。
const server = net.createServer((socket) => {
socket.once('data', buffer => {
// 接收到HTTP请求头数据
const str = buffer.toString()
// 将请求头数据转为对象
const headers = parseHeader(str)
// 判断请求是否为WebSocket连接
if (headers['upgrade'] !== 'websocket') {
// 若当前请求不是WebSocket连接,则关闭连接
console.log('非 WebSocket 连接')
socket.end()
} else if (headers['sec-websocket-version'] !== '13') {
// 判断WebSocket版本是否为13,防止是其他版本,造成兼容错误
console.log('WebSocket 版本错误')
socket.end()
} else {
// 校验Sec-WebSocket-Key,完成连接
/*
协议中规定的校验用GUID,可参考如下链接:
https://tools.ietf.org/html/rfc6455#section-5.5.2
https://stackoverflow.com/questions/13456017/what-does-258eafa5-e914-47da-95ca-c5ab0dc85b11-means-in-websocket-protocol
*/
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
const key = headers['sec-websocket-key']
const hash = crypto.createHash('sha1') // 创建一个签名算法为sha1的哈希对象
hash.update(`${key}${GUID}`) // 将key和GUID连接后,更新到hash
const result = hash.digest('base64') // 生成base64字符串
const header = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${result}`,
'\r\n'
].join('\r\n'); // 生成供前端校验用的请求头
socket.write(header) // 返回HTTP头,告知客户端校验结果,HTTP状态码101表示切换协议:https://httpstatuses.com/101。
// 若客户端校验结果正确,在控制台的 Network 模块可以看到HTTP请求的状态码变为 101 Switching Protocols,同时客户端的ws.onopen事件被触发。
// 将建立连接的客户端推到数组中
list.push(socket)
console.log(list.length)
// 7. 建立连接后,通过data事件接收客户端的数据并处理
socket.on('data', (buffer) => {
const data = decodeWsFrame(buffer)
// opcode为8,表示客户端发起了断开连接
if (data.opcode === 8) {
socket.end() // 与客户端断开连接
} else {
// 接收到客户端数据时的处理,此处默认为返回接收到的数据。
// 更新那些客户端还在
list = list.filter(item => !item.destroyed)
list.forEach(item => {
item.write(encodeWsFrame({ payloadData: `服务端接收到的消息为:${data.payloadData ? data.payloadData.toString() : ''}` }))
})
}
})
}
})
})
server.listen(8002, () => {
console.log('Runing at: ws://localhost:8002')
})
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<input type="text" id="input" value="test message"><br />
<input type="button" value="发送消息" id="send"><br />
<h3>接收到的消息:</h3>
<input type="button" value="关闭聊天" id="close"><br />
<script>
const ws = new WebSocket('ws://localhost:8002/')
// 连接打开的事件,连接开始后,可以在控制台的Network模块看到,该连接会一直保留在pending状态,直到连接断开。
ws.onopen = function () {
console.log('连接已建立')
}
// 接收消息的事件
ws.onmessage = function (response) {
console.log(response)
document.querySelector('h3').insertAdjacentHTML('afterend', `<p>${response.data}<p>`)
}
// 连接正常关闭的事件
ws.onclose = function () {
console.log('连接已关闭')
document.querySelector('p').innerHTML = '连接已关闭'
}
// 连接出错的事件
ws.onerror = function () {
console.log('连接出错')
}
</script>
<script>
document.querySelector('#send').addEventListener('click', function () {
ws.send(document.querySelector('#input').value)
})
document.querySelector('#close').addEventListener('click', function () {
ws.close()
})
</script>
</body>
</html>