客户端
<!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>
<button id="connect">连接</button>
<button disabled id="sendMessage">发送</button>
<button disabled id="destroy">关闭</button>
<script type="module">
import { messageCenter } from "./lib/messageCenter.js";
import MyWebSocket from "./lib/webSocket.js";
const name = "zhangsan"; //连接用户名
const connect = document.querySelector("#connect");
const sendMessage = document.querySelector("#sendMessage");
const destroy = document.querySelector("#destroy");
const wsUrl = "ws://127.0.0.1:2048/ws/?name=" + name;
let myWebSocket;
messageCenter.on("changeBtnState", setButtonState); //设置按钮样式
messageCenter.on("reconnect", reconnectWebSocket); //接收重连消息
connect.addEventListener("click", reconnectWebSocket);
sendMessage.addEventListener("click", function (e) {
// 发消息
myWebSocket.sendMsg({
ModeCode: "message",
msg: "hello",
});
});
destroy.addEventListener("click", function (e) {
clear(myWebSocket);
});
function reconnectWebSocket() {
// 入口函数
if (myWebSocket) {
//防止多个websocket同时执行
clear(myWebSocket);
}
connectWebSocket();
}
// 终止连接
function clear(myWebSocket) {
myWebSocket.clear();
myWebSocket = null;
}
function connectWebSocket() {
myWebSocket = new MyWebSocket(wsUrl);
myWebSocket.init(
{
//time:心跳时间间隔 timeout:心跳超时间隔 reconnect:断线重连时间,一般的,断线重连时间等于心跳时间间隔加断线重连时间(忽略超时等待)
time: 10 * 1000,
timeout: 1 * 1000,
reconnect: 5 * 1000,
},
true
);
}
/*
* 设置按钮是否可点击
* @param state:open表示开启状态,close表示关闭状态
*/
function setButtonState(state) {
switch (state) {
case "open":
connect.disabled = true;
sendMessage.disabled = false;
destroy.disabled = false;
break;
case "close":
connect.disabled = false;
sendMessage.disabled = true;
destroy.disabled = true;
break;
}
}
</script>
</body>
</html>
websocket客户端
import { messageCenter } from "./messageCenter.js";
const ModeCode = {
//websocket消息类型
MSG: "message", //普通消息
HEART_BEAT: "heart_beat", //心跳
};
export default class MyWebSocket extends WebSocket {
constructor(url, protocols) {
super(url, protocols);
return this;
}
/*
* 入口函数
* @param heartBeatConfig time:心跳时间间隔 timeout:心跳超时间隔 reconnect:断线重连时间间隔
* @param isReconnect 是否断线重连
*/
init(heartBeatConfig, isReconnect) {
this.onopen = this.openHandler; //连接上时回调
this.onclose = this.closeHandler; //断开连接时回调
this.onmessage = this.messageHandler; //收到服务端消息
this.onerror = this.errorHandler; //连接出错
this.heartBeat = heartBeatConfig;
this.isReconnect = isReconnect;
this.reconnectTimer = null; //断线重连时间器
this.waitingTimer = null; // 超时等待时间器
this.heartTimer = null; // 心跳时间器
this.webSocketState = false; //socket状态 true为已连接
}
openHandler() {
messageCenter.emit("changeBtnState", "open"); //触发事件改变按钮样式
this.webSocketState = true; //socket状态设置为连接,做为后面的断线重连的拦截器
!!this.heartBeat &&
!!this.heartBeat.time &&
this.startHeartBeat(this.heartBeat.time); //是否启动心跳机制
console.log("开启");
}
messageHandler(e) {
const data = this.getMsg(e);
switch (data.ModeCode) {
case ModeCode.MSG: //普通消息
console.log("收到消息" + data.msg);
break;
case ModeCode.HEART_BEAT: //心跳
this.webSocketState = true;
console.log("收到心跳响应" + data.msg);
break;
}
}
closeHandler() {
//socket关闭
messageCenter.emit("changeBtnState", "close"); //触发事件改变按钮样式
this.webSocketState = false; //socket状态设置为断线
console.log("关闭");
}
errorHandler() {
//socket出错
messageCenter.emit("changeBtnState", "close"); //触发事件改变按钮样式
this.webSocketState = false; //socket状态设置为断线
this.reconnectWebSocket(); //重连
console.log("出错");
}
sendMsg(obj) {
this.send(JSON.stringify(obj));
}
getMsg(e) {
return JSON.parse(e.data);
}
/*
* 心跳初始函数
* @param time:心跳时间间隔
*/
startHeartBeat(time) {
this.heartTimer = setTimeout(() => {
this.sendMsg({
ModeCode: ModeCode.HEART_BEAT,
msg: new Date(),
});
this.waitingTimer = this.waitingServer();
}, time);
}
//延时等待服务端响应,通过webSocketState判断是否连线成功
waitingServer() {
this.webSocketState = false;
return setTimeout(() => {
if (this.webSocketState) return this.startHeartBeat(this.heartBeat.time);
console.log("心跳无响应,已断线");
this.reconnectTimer = this.reconnectWebSocket();
}, this.heartBeat.timeout);
}
//重连操作
reconnectWebSocket() {
if (!this.isReconnect) return;
return setTimeout(() => {
messageCenter.emit("reconnect");
}, this.heartBeat.reconnect);
}
// 清除所有定时器
clearTimer() {
clearTimeout(this.reconnectTimer);
clearTimeout(this.heartTimer);
clearTimeout(this.waitingTimer);
}
// 关闭连接
clear(isReconnect = false) {
this.isReconnect = isReconnect;
this.clearTimer();
this.close();
}
}
eventbus
/*
* @Author: Hunter
* @Date: 2022-04-14 15:32:29
* @LastEditTime: 2022-09-15 11:28:03
* @LastEditors: Hunter
* @Description:
* @FilePath: \website\lib\messagecenter.js
* 可以输入预定的版权声明、个性签名、空行等
*/
export class MessageCenter {
events = {};
/**
* 注册事件至调度中心
* @param type 事件类型,特指具体事件名
* @param handler 事件注册的回调
*/
on(type, handler) {
//订阅者
this.checkHandler(type, handler);
if (!this.has(type)) {
//若调度中心未找到该事件的队列,则新建某个事件列表(可以对某个类型的事件注册多个回调函数)
this.events[type] = [];
}
this.events[type].push(handler);
return this;
}
/**
* 触发调度中心的某个或者某些该事件类型下注册的函数
* @param type 事件类型,特指具体事件名
* @param data 发布者传递的参数
*/
emit(type, data) {
//发布者
if (this.has(type)) {
this.runHandler(type, data);
}
return this;
}
//销毁监听
un(type, handler) {
this.unHandler(type, handler);
return this;
}
// 只注册一次监听,执行即销毁
once(type, handler) {
this.checkHandler(type, handler);
const fn = (...args) => {
this.un(type, fn);
return handler(...args);
};
this.on(type, fn);
return this;
}
// 重置调度中心
clear() {
this.events = {};
return this;
}
// 判断事件是否被订阅
has(type) {
return !!this.events[type];
}
// 同一个事件被绑定了多少函数
handlerLength(type) {
return this.events[type]?.length ?? 0;
}
// 监听invoke的消息,若handler中进行了计算或者异步操作,会反馈给invoke
watch(type, handler) {
this.checkHandler(type, handler);
const fn = (...args) => {
this.emit(this.prefixStr(type), handler(...args));
};
this.on(type, fn);
return this;
}
// 触发watch事件,并且接收watch处理结果
invoke(type, data) {
return new Promise((resolve) => {
this.once(this.prefixStr(type), resolve);
this.emit(type, data);
});
}
// 批量执行调度中心中某个函数集
runHandler(type, data) {
for (let i = 0; i < this.events[type].length; i++) {
this.events[type][i] && this.events[type][i](data);
}
}
// 批量销毁调度中心中某个函数集
unHandler(type, handler) {
!handler && (this.events[type] = []);
handler && this.checkHandler(type, handler);
for (let i = 0; i < this.events[type].length; i++) {
if (this.events[type][i] && this.events[type][i] === handler) {
this.events[type][i] = null;
}
}
}
prefixStr(str) {
return `@${str}`;
}
/**
* 检查参数是否符合标准
* @param type 事件名
* @param handler 事件钩子
*/
checkHandler(type, handler) {
if (type?.length === 0) {
throw new Error("type.length can not be 0");
}
if (!handler || !type) {
throw new ReferenceError("type or handler is not defined");
}
if (typeof handler !== "function" || typeof type !== "string") {
throw new TypeError(
`${handler} is not a function or ${type} is not a string`
);
}
}
//返回当前类的实例的单例
static Instance(Fn) {
if (!Fn._instance) {
Object.defineProperty(Fn, "_instance", {
value: new Fn(),
});
}
return Fn._instance;
}
}
export const messageCenter = MessageCenter.Instance(MessageCenter);
export default MessageCenter;
服务端
const http = require("http");
const WebSocketServer = require("./websocket").WebSocketServer;
const port = 2048; //端口
const pathname = "/ws/"; //访问路径
const server = http.createServer();
const clientKey = "name"; //客户端标识符
const webSocketServer = new WebSocketServer(
{ noServer: true },
{
clientKey,
}
);
const initSocket = ({ name, req, socket, head }) => {
webSocketServer.handleUpgrade(req, socket, head, (ws) => {
console.log("shoyan")
ws[clientKey] = name; //添加索引,方便在客户端列表查询某个socket连接
webSocketServer.addClient(ws);
webSocketServer.ws = ws;
});
};
server
.on("upgrade", (req, socket`在这里插入代码片`, head) => {
//通过http.server过滤数据
const url = new URL(req.url, `http://${req.headers.host}`);
const name = url.searchParams.get(clientKey); //获取连接标识
if (!checkUrl(url.pathname, pathname)) {
//未按标准
socket.write("路由未按标准访问");
socket.pipe(socket);
return;
}
initSocket({ name, req, socket, head });
})
.listen(port, () => {
console.log("服务开启");
});
//验证url标准
function checkUrl(url, key) {
//判断url是否包含key
return url.includes(key);
}
websocket 服务端
const WebSocket = require("ws");
const clientKey = "name"; //客户端标识符
exports.WebSocketServer = class extends WebSocket.Server {
constructor(_, opts) {
super(...arguments);
this.clientKey = opts.clientKey ?? clientKey;
this.webSocketClient = {}; //存放已连接的客户端
}
set ws(val) {
//代理当前的ws,赋值时将其初始化
this._ws = val;
val.on("error", this.errorHandler);
val.on("close", this.closeHandler);
val.on("message", this.messageHandler);
}
get ws() {
return this._ws;
}
messageHandler = (e) => {
console.info("接收客户端消息");
const data = JSON.parse(e);
switch (data.ModeCode) {
case "message":
console.log(`收到${this.ws[this.clientKey]}消息${data.msg},已返回`);
this.send(this.ws, data);
break;
case "heart_beat":
console.log(`收到${this.ws[this.clientKey]}心跳${data.msg}`);
this.send(this.ws, data);
break;
}
};
send = (ws, data) => {
ws.send(JSON.stringify(data));
};
errorHandler = (e) => {
this.removeClient(this.ws);
console.info("客户端出错");
};
closeHandler = (e) => {
this.removeClient(this.ws);
console.info("客户端已断开");
};
addClient = (item) => {
//设备上线时添加到客户端列表
if (this.webSocketClient[item[this.clientKey]]) {
console.log(item[this.clientKey] + "客户端已存在");
this.webSocketClient[item[this.clientKey]].close();
}
console.log(item[this.clientKey] + "客户端已添加");
this.webSocketClient[item[this.clientKey]] = item;
};
removeClient = (item) => {
//设备断线时从客户端列表删除
if (!this.webSocketClient[item[this.clientKey]]) {
console.log(item[this.clientKey] + "客户端不存在");
return;
}
console.log(item[this.clientKey] + "客户端已移除");
this.webSocketClient[item[this.clientKey]] = null;
};
};
说明:
1 创建 WebSocketServer 实例时,必须指定 port、server 或 noServer 选项中的一个且只能一个。
2 参数 { noServer: true } 的含义如下:
当设置 noServer: true 时,这表示创建一个不自动绑定到任何底层 HTTP/S 服务器的 WebSocket 服务器实例。
通常情况下,如果不设置这个参数或者设置为 false,创建的 WebSocket 服务器会自动尝试监听一个端口并处理传入的 HTTP 请求以进行 WebSocket 连接的升级。
const http = require('http');
const WebSocket = require('ws');
const server = http.createServer();
const wss = new WebSocket.Server({ noServer: true });
server.on('upgrade', (req, socket, head) => {
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit('connection', ws, req);
});
});
server.listen(8080);
在这个例子中,首先创建了一个普通的 HTTP 服务器 server。然后创建了一个 WebSocket 服务器 wss 并设置 noServer: true。当 HTTP 服务器接收到一个可能的 WebSocket 升级请求时,通过调用 wss.handleUpgrade 来处理这个请求,并将连接升级为 WebSocket 连接。
参考
1https://lonjinup.github.io/WebSocketManager/
2https://juejin.cn/post/6945057379834675230
3https://juejin.cn/post/7371365854012276747#heading-4