前端代码
class Socket {
constructor({ url = '', token = '', roomId = '', callback = () => {} }) {
this.url = url;
this.callback = callback;
this.token = token;
this.roomId = roomId;
this.ws = {};
this.init();
}
init() {
this.ws = new WebSocket(`wss://${this.url}`);
// WebSocket连接成功时触发
this.ws.onopen = () => this.onOpen(this.token, this.roomId);
// WebSocket接受到信息时触发
this.ws.onmessage = e => this.onMessage(e, this.callback);
// WebSocket连接信息错误时触发
this.ws.onerror = this.onError;
// WebSocket关闭时触发
this.ws.onclose = this.onClose;
}
onOpen(token, roomId) {
// 连接时,根据身份校验事件,传参给服务端设置对应的连接信息
this.send({
event: 'auth',
message: {
token,
roomId
}
});
}
send(data) {
this.ws.send(JSON.stringify(data));
}
onMessage(e, callback) {
const data = JSON.parse(e.data);
switch (data.event) {
case 'noAuth':
// 验证失败,返回登录页
location.href = '/login';
break;
case 'heartbeat':
// 心跳检测,当连接成功,服务端就会定时发起ping请求,需要回复,让服务端知道客户端在线,
this.send({
event: 'heartbeat',
message: 'pong'
});
clearTimeout(this.pingTimeout);
this.pingTimeout = setTimeout(() => {
this.close();
this.onError();
}, 30000 + 1000);
break;
default:
callback(data);
}
}
onError() {
console.log('websocket连接错误');
// 连接出错了,1S后重新连接
setTimeout(() => {
this.init();
}, 1000);
}
onClose() {
console.log('websocket连接断开');
}
close() {
// 关闭websocket连接需要关闭心跳检测定时器,防止断开后原来的定时器还在运行连接
clearTimeout(this.pingTimeout);
this.ws.close();
}
}
后端代码
import WebSocket from "ws";
import fs from "fs";
import https from "https";
import jwt from "jsonwebtoken";
class Socket {
constructor(config) {
this.config = {
port: 8080,
...config,
};
this.wss = {};
this.init();
}
init() {
const server = https.createServer({
cert: fs.readFileSync(
__dirname + "/httpsConfig/5163307_www.pengjianming.top.pem"
),
key: fs.readFileSync(
__dirname + "/httpsConfig/5163307_www.pengjianming.top.key"
),
});
// 由于使用的是https,根据官方的用法,这样设置,如果不是使用https直接传入this.config定义端口就可以
this.wss = new WebSocket.Server({ server });
this.wss.on("connection", (ws) => {
// 连接成功,开启心跳检测
ws.isAlive = true;
this.heartbeat(ws);
ws.on("message", (msg) => this.message(ws, msg));
ws.on("close", () => this.close(ws, this.wss));
});
server.listen(8080);
}
message(ws, msg) {
const data = JSON.parse(msg);
const event = {
// 身份验证完后添加返回验证信息,并添加对应用户信息到ws上
auth: (params) => {
const auth = jwt.verify(params.token, "shared-secret");
if (auth) {
ws.user = auth;
ws.roomId = params.roomId;
} else {
ws.send(
JSON.stringify({
event: "noAuth",
})
);
}
},
// 收到心跳,证明还活着
heartbeat: () => {
ws.isAlive = true;
},
};
event[data.event](data.message);
}
send(user_name, message) {
// 排除掉是多人的客户端,根据对应用户名发送消息
[...this.wss.clients]
.filter((client) => !client.roomId)
.forEach((client) => {
if (
client.readyState === WebSocket.OPEN &&
client.user.user_name === user_name
) {
client.send(JSON.stringify({ event: "tip", message }));
}
});
}
broadcast(roomId, user_id) {
// 广播对应房间号的,排除自己
[...this.wss.clients]
.filter((client) => client.roomId)
.forEach((client) => {
if (
client.readyState === WebSocket.OPEN &&
client.roomId === roomId &&
user_id !== client.user.id
) {
client.send(JSON.stringify({ event: "chat" }));
}
});
}
close(ws) {
// 关闭连接时关闭对应的定时器
clearInterval(ws.interval);
}
heartbeat(ws) {
// 假设心跳停止,定时发送ping测试心跳,如果没有返回pong,则会关闭该websocket
ws.interval = setInterval(() => {
if (ws.isAlive === false) return ws.terminate();
ws.isAlive = false;
ws.send(
JSON.stringify({
event: "heartbeat",
message: "ping",
})
);
}, 30000);
}
}
export default new Socket();
流程说明
初始化
创建对象时,调用init方法,创建websocket实例,连接成功,此时onOpen也会被调用,与后端进行身份校验,与创建连接信息,根据传递的token,可以确定身份,进行单点信息发送,如果传递了roomId,就可以通过广播进行指定roomId的广播,实现多聊天室那样的效果,如果身份校验通过,则设置对应连接信息,如果不通过就会发送event的noAuth事件,来通知客户端身份校验没通过,客户端接收到noAuth事件,变回跳转登录页,进行重新登录
心跳检测
创建websocket实例,连接成功时,后端便会开始心跳检测,连接成功时先设定还活着isAlive = true,当开启心跳检测时,就设定死了,发送heartbeat事件消息给客户端,如果客户端没有在规定时间发送回heartbeat,设定isAlive = true,那么,在第二次心跳检测开始时,就会认为客户端死了直接断开连接了,如果客户端返回对应heartbeat消息,即心跳正常,客户端每次收到心跳,都会清除之前定时器,开启一个新的定时器,如果在规定时间内,服务端没再次进行心跳检测,便会关闭此客户端,重新连接
协议说明
当使用的是http时,协议为ws,如果使用https则协议为wss,并且,需要设定证书文件
广播信息
遍历所有已连接客户端,通过连接时传递的roomId,进行筛选,然后发送信息
单点发送信息
遍历所有已连接客户端,根据之前身份验证的信息,来传递
传递信息注意
传递的信息需要转换为字符换,不转换会拿到传递信息的类型
如何使用
this.socket = new Socket({
url: 'www.pengjianming.top:8080',
token: getToken(),
roomId: this.$route.query.id,
callback: data => {
if (data.event === 'chat') {
this.getTicket();
}
}
});
创建后,每次服务端返回信息,如果事件不是设定的heartbeat与noauth,都会调用callback,可以在这里进行需要的事件通知