WebSocket简介
HTML5开始提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。
特点:
- WebSocket可以在浏览器里使用
- 支持双向通信
- 使用很简单
使用
安装ws库
npm i ws
服务端
var app = require('express')();
var WebSocket = require('ws');
var wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', function connection(ws) {
// 如果有客户端连接,就会进入这个回调
console.log('连接成功');
// 如果有客户端发送消息,就会进入这个回调
ws.on('message', (msg) => {
console.log('服务端接收的消息' + msg);
});
// 向客户端发送消息
ws.send('nice to meet you too');
});
app.listen(3000);
客户端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="text">
<button>发送</button>
<script>
var ws = new WebSocket('ws://localhost:8080');
ws.onopen = function () {
console.log('连接成功');
// 向服务端发送消息
ws.send('nice to meet you');
};
ws.onmessage = function (msg) {
// 接收到服务端消息
console.log('客户端接收的消息: ' + msg.data);
};
</script>
</body>
</html>
客户端效果:
服务端效果:
ws模块源码解析
426状态码
426 Upgrade Required 是一种HTTP协议的错误状态代码,表示服务器拒绝处理客户端使用当前协议发送的请求,但是可以接受其使用升级后的协议发送的请求。
这个状态码主要用在WebSockets协议中,表示客户端需要使用WebSockets协议来连接服务器。
什么意思呢?例如我们创建一个HTTP服务如果这么写:
const http = require("http");
const server = http.createServer((req,res)=>{
const body = http.STATUS_CODES[426]
// 设置响应头
res.writeHead(426, {
"Content-Length": body.length,
"Content-Type": "text/plain",
});
// 发送响应体
res.end(body)
})
server.listen(8000)
就是告诉客户端,如果你访问我这边的服务,那么你就要进行升级服务。也就是使用WebSocket对我进行访问!
那有一个问题,如果客户端使用了WebSocket访问,服务端要怎么进行响应呢?
还直接在createServer里面的回调中处理吗?
upgrade事件
在这里面,如果客户端通过WebSocket进行访问服务端,会触发服务端server的upgrade事件,也就是说会进下面的回调函数里。
server.on('upgrade',(req, socket, head) => {
// 固定格式
const key = req.headers['sec-websocket-key'];
const digest = createHash('sha1')
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',)
.digest('base64');
const headers = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${digest}`
];
socket.write(headers.concat('\r\n').join('\r\n'));
// 客户端发送的消息
socket.on('data', (data) => {
console.log(data.toString());
})
// 服务端向客户端发送消息
socket.write('你好')
})
这个回调中,通过socket来进行服务端和客户端之间的双向通信。
转码
但是只有上面的例子,似乎每次拿到的数据都是乱码。这是因为WebSocket之间的通信的报文,不能通过Buffer的toString直接转码。这里提供一下在网上找到的转码方法:
server.on('upgrade', (req, socket, head) => {
const key = req.headers['sec-websocket-key'];
const digest = createHash('sha1')
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',)
.digest('base64');
const headers = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
`Sec-WebSocket-Accept: ${digest}`
];
socket.write(headers.concat('\r\n').join('\r\n'));
socket.on('data',(data) => {
console.log(decodeSocketFrame(data).payloadBuf.toString())
socket.write(encodeSocketFrame({
fin:1,
opcode:1,
payloadBuf:Buffer.from('你好')
}))
})
})
function decodeSocketFrame (bufData){
let bufIndex = 0
const byte1 = bufData.readUInt8(bufIndex++).toString(2)
const byte2 = bufData.readUInt8(bufIndex++).toString(2)
console.log(byte1);
console.log(byte2);
const frame = {
fin:parseInt(byte1.substring(0,1),2),
// RSV是保留字段,暂时不计算
opcode:parseInt(byte1.substring(4,8),2),
mask:parseInt(byte2.substring(0,1),2),
payloadLen:parseInt(byte2.substring(1,8),2),
}
// 如果frame.payloadLen为126或127说明这个长度不够了,要使用扩展长度了
// 如果frame.payloadLen为126,则使用Extended payload length同时为16/8字节数
// 如果frame.payloadLen为127,则使用Extended payload length同时为64/8字节数
// 注意payloadLen得长度单位是字节(bytes)而不是比特(bit)
if(frame.payloadLen==126) {
frame.payloadLen = bufData.readUIntBE(bufIndex,2);
bufIndex+=2;
} else if(frame.payloadLen==127) {
// 虽然是8字节,但是前四字节目前留空,因为int型是4字节不留空int会溢出
bufIndex+=4;
frame.payloadLen = bufData.readUIntBE(bufIndex,4);
bufIndex+=4;
}
if(frame.mask){
const payloadBufList = []
// maskingKey为4字节数据
frame.maskingKey=[bufData[bufIndex++],bufData[bufIndex++],bufData[bufIndex++],bufData[bufIndex++]];
for(let i=0;i<frame.payloadLen;i++) {
payloadBufList.push(bufData[bufIndex+i]^frame.maskingKey[i%4]);
}
frame.payloadBuf = Buffer.from(payloadBufList)
} else {
frame.payloadBuf = bufData.slice(bufIndex,bufIndex+frame.payloadLen)
}
return frame
}
function encodeSocketFrame (frame){
const frameBufList = [];
// 对fin位移七位则为10000000加opcode为10000001
const header = (frame.fin<<7)+frame.opcode;
frameBufList.push(header)
const bufBits = Buffer.byteLength(frame.payloadBuf);
let payloadLen = bufBits;
let extBuf;
if(bufBits>=126) {
//65536是2**16即两字节数字极限
if(bufBits>=65536) {
extBuf = Buffer.allocUnsafe(8);
buf.writeUInt32BE(bufBits, 4);
payloadLen = 127;
} else {
extBuf = Buffer.allocUnsafe(2);
buf.writeUInt16BE(bufBits, 0);
payloadLen = 126;
}
}
let payloadLenBinStr = payloadLen.toString(2);
while(payloadLenBinStr.length<8){payloadLenBinStr='0'+payloadLenBinStr;}
frameBufList.push(parseInt(payloadLenBinStr,2));
if(bufBits>=126) {
frameBufList.push(extBuf);
}
frameBufList.push(...frame.payloadBuf)
return Buffer.from(frameBufList)
}
WebSocketServer的实现
有了上面的基础,基本知道双向通信是怎么做到的了。就来看一下WebSocketServer的实现。
当我们使用的时候,我们是以这种方式:
const WebSocketServer = require('ws')
var wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
})
我们知道,connection是httpServer的回调,为什么在WebSocketServer中可以使用呢?
export default class WebSocketServer {
constructor(port) {
this._server = http.createServer((req, res) => {
const body = http.STATUS_CODES[426];
res.writeHead('426', {
'Content-Type': 'text/align',
'Content-Length': body.length
})
res.end(body)
})
this._server.listen(port);
const connectionEmit = this.emit.bind(this, 'connection');
const closeEmit = this.emit.bind(this, 'close');
// 其他事件,都是http能监听到的;
const map = {
connection: connectionEmit,
close: closeEmit
}
for(let emitName in map) {
this._server.on(emitName, map[emitName])
}
}
}
在WebSocketServer中,如果客户端触发了http的事件时,它便将其转发到WebSocket实例上面。
然后再处理自己的逻辑。
改进后的实现
const http = require('http');
const WebSocket = require('ws');
const EventEmitter = require('events');
class WebSocketServer extends EventEmitter {
constructor(port) {
super();
this._server = http.createServer((req, res) => {
const body = http.STATUS_CODES[426];
res.writeHead(426, {
'Content-Type': 'text/plain',
'Content-Length': body.length
});
res.end(body);
});
this._wss = new WebSocket.Server({ noServer: true });
this._server.on('upgrade', (request, socket, head) => {
this._wss.handleUpgrade(request, socket, head, (ws) => {
this.emit('connection', ws, request);
});
});
this._server.listen(port);
}
}
module.exports = WebSocketServer;
解释
继承 EventEmitter:
- WebSocketServer 继承自 EventEmitter,使得 WebSocketServer 类能够使用事件机制。
创建 HTTP 服务器:
- HTTP 服务器在接收到普通 HTTP 请求时,返回状态码 426,表示需要升级协议。
创建 WebSocket 服务器:
- 使用 ws 模块创建一个 WebSocket 服务器实例,并配置为不绑定任何端口({ noServer: true })。
处理升级请求:
- 监听 HTTP 服务器的 upgrade 事件,该事件在接收到带有 Upgrade 头部的 HTTP 请求时触发。
- 调用 WebSocket 服务器的 handleUpgrade 方法处理 WebSocket 握手过程。
- 当握手成功时,触发 connection 事件,并传递 WebSocket 实例 ws 和 HTTP 请求 request。
事件机制:
- this.emit('connection', ws, request) 将 WebSocket 实例和请求传递到 connection 事件的监听器。