我正在参与掘金创作者训练营第5期,点击了解活动详情
前言
WebSocket 是一种双工通讯协议,主要用于客户端—服务器通信。当建立连接之后,客户端和服务器都可以主动向对方发送或接收数据。
WebSocket是建立在TCP之上,跟HTTP一样通过TCP传递数据。
使用 WebSocket 创建的连接是长时间存在的通信通道,但是只要其中一方断开连接,那么将无法通信。
WSS 用于需要加密通道(TCP+TLS)。WS一般默认端口号80,WSS默认端口号443。在https下不能使用 ws:// 。且wss下不支持ip地址的写法(必须写成域名形式)。
实现一个WebSocket连接
关于 WebSocket Api 基础语法, 是非常简单的。有不懂的小伙伴先可以去读一下阮一峰博客那一篇 **Websocket教程**
我们先通过 new WebSocket 创建连接并获取到 WebSocket 实例。这里的 ws 接口,是我在晚上找的测试接口,有需要可以 戳一下 。
const ws = new WebSocket('ws://127.0.0.1:8800');
ws.onopen = function () {
console.log('连接成功');
ws.send('shenjilin');
console.log('客户端向服务器发送的消息:shenjilin');
};
ws.onmessage = function (e) {
console.log('客户端接收服务器发送的消息:' + e.data);
};
复制代码
在浏览器上查看ws请求
以谷歌为例,启动开发者工具,具体操作如下图所示
WebSocket 建立连接之后,会在浏览器上显示一个连接。如果同一个连接创建多次,查看最近一次连接信息。
Message 这一栏就是客户端和服务器传输的信息,信息格式一般是用 字符串或JSON字符串
项目中使用websocket, 如何封装
在实际项目开发过程中,一般不会这么简单的使用,而且使用起来也不是很方便。 所以这时候要将我们 ws 请求进行封装。这里使用 vite+vue3+pinia 来写,还用到的一个 依赖 mitt(是一个体积极小的第三方消息发布/订阅式JavaScript库)
完整代码代码地址: github.com/shenjilina/… 如果以上对您有帮助,欢迎star
1. 实现一个基础ws类
我们现在src目录下创建一个 utils/socket.js 文件
先使用 websocket api 封装一个class类
我们这个类实现创建 ws 实例、创建四个监听事件(onopen, onerror, onclose, onmessage)、重新连接、关闭、销毁等方法。
class Ws {
// websocket 接口地址
url;
// WebSocket 实例
ws;
// 重连中
isReconnectionLoading = false;
// 延时重连的 id
timeId = null;
// 用户手动关闭连接
isCustomClose = false;
// 错误消息队列
errorStack = [];
constructor(url) {
this.url = url;
this.createWebSocket();
}
// 创建一个 webSocket 连接
createWebSocket() {
if ("WebSocket" in window) {
if (flag) return;
flag = true;
// 实例化WebSocket
this.ws = new WebSocket(this.url);
// 监听事件
this.onopen();
this.onerror();
this.onclose();
this.onmessage();
} else {
console.log("你的浏览器不支持 WebSocket");
}
}
// 监听成功
onopen() {
this.ws.onopen = () => {
console.log("onopen: 连接成功了");
// 连接成功。 检查之前发送失败的消息,如果有就直接再次发送
this.errorStack.forEach((message) => {
this.send(message);
});
this.errorStack = [];
this.isReconnectionLoading = false;
};
}
// 监听错误
onerror() {
this.ws.onerror = (err) => {
this.reconnection();
this.isReconnectionLoading = false;
};
}
// 监听关闭
onclose() {
this.ws.onclose = () => {
// 如果是用户手动关闭的,直接返回
if (this.isCustomClose) return;
// 重新连接
this.reconnection();
this.isReconnectionLoading = false;
};
}
// 接收 WebSocket 消息
onmessage() {
this.ws.onmessage = (event) => {
try {
const data = event.data;
console.log("接收到消息:",data)
} catch (error) {
console.log(error, "error");
}
};
}
// 重新链接
reconnection() {
// 防止重复重新链接
if (this.isReconnectionLoading) return;
this.isReconnectionLoading = true;
flag = null;
clearTimeout(this.timeId);
this.timeId = setTimeout(() => {
this.createWebSocket();
}, 3000);
}
// 发送消息
send(message) {
// 连接失败时的处理
if (this.ws.readyState !== 1) {
this.errorStack.push(message);
return;
}
this.ws.send(message);
}
// 手动关闭
close() {
flag = null;
this.isCustomClose = true;
this.ws.close();
}
// 手动开启
start() {
this.isCustomClose = false;
this.reconnection();
}
// 销毁
destroy() {
this.close();
this.ws = null;
this.errorStack = null;
}
}
export default Ws;
复制代码
2. 在ws这个类上添加mitt(发布/订阅)
首先安装依赖 pnpm i mitt
- 创建文件 utils/mitt.js
import mitt from 'mitt';
export const emitter = mitt();
复制代码
- 我们再改造一下上面 ws 类
先在 utils/socket.js 中导入 emitter
// 消息管理中心
import { emitter } from "./mitt";
复制代码
- 然后在ws类中创建两个方法。 分别是订阅和取消订阅,订阅方法是在我们创建实例后订阅服务器的消息,取消订阅在我们关闭或者销毁这个连接时调用
// 订阅
subscribe(eventName, cb) {
emitter.on(eventName, cb);
}
// 取消订阅
unsubscribe(eventName, cb) {
emitter.off(eventName, cb);
}
复制代码
- onmessage 是我们接收服务器发送给客户端的消息监听事件,这里的参数就是服务器发送过来的消息体。我们针对不同的消息类型有不同的处理方式,这里我们使用mitt(事件发布订阅库)这个依赖,将我们服务器发送过来的消息体根据不同类型发布。如果我们想在某个组件中使用,提前订阅这个类型就好
onmessage() {
this.ws.onmessage = (event) => {
try {
const data = event.data;
// 发布消息到消息中心
emitter.emit(data.type, data);
} catch (error) {
console.log(error, "error");
}
};
}
复制代码
- 当我们连接销毁的时候也要关闭我们所有的订阅消息。在销毁方法添加移除订阅
// 销毁
destroy() {
this.close();
this.ws = null;
this.errorStack = null;
// 移除所有订阅
emitter.all.clear();
}
复制代码
在pinia中使用 WS 类
安装依赖 pnpm i pinia
创建文件 store/socket.js
我们通过 pinia 二次封装,这里我们写了 初始化、订阅、发送消息以及销毁方法 。初始化的时候会调用消息订阅方法
import Ws from '@/utils/socket';
export const useSocket = defineStore('socket', () => {
// socket 实例
const instance = ref(null);
// socket 消息
const socketData = ref(null);
/**
* @Author: shenjilin
* @Date: 2022-07-29 15:55:38
* @description: socket 初始化
* @param {*} url
* @return {*} socket 实例
*/
const wsInit = async (url) => {
if (!instance.value) {
const ws = new Ws(url);
instance.value = ws;
// 订阅type = notify 的消息
wsSubscribe("notify");
}
return instance.value;
};
/**
* @Author: shenjilin
* @Date: 2022-07-29 15:43:45
* @description: 订阅websocket消息
* @param {string} type 消息类型
* @return {*}
*/
const wsSubscribe = (type) => {
instance.value.subscribe(type, (data) => {
console.log('接收服务器消息: ', data);
// 每次接收到消息都会重置 socketData
socketData.value = data;
});
};
/**
* @Author: shenjilin
* @Date: 2022-07-29 15:46:14
* @description: 发送消息
* @param {*} data 消息体
* @return {*}
*/
const sendScoket = (data) => {
instance.value.send(JSON.stringify(data));
},
/**
* @Author: shenjilin
* @Date: 2022-07-29 15:49:03
* @description: 销毁
* @return {*}
*/
const destroyScoket = () => {
if (instance.value) {
// 销毁socket
instance.value.destroy();
instance.value = null;
}
},
return {
instance,
socketData,
wsInit,
wsSubscribe,
sendScoket,
destroyScoket
};
});
复制代码
在组件中使用
最后就可以在组件中使用,通过实例化 pinia useSocket 获取 socketData, wsInit, sendScoket, 因为我们在初始化
import { watch } from "vue";
import { useSocket } from "@/store/socket";
const { socketData, wsInit, sendScoket } = useSocket();
// 初始化ws
wsInit("ws://127.0.0.1:8800");
// 获取服务器发给客户端的最新数据
watch(
() => socketData,
(data) => {
console.log("ws: ", data);
},
{
immediate: true,
}
);
// 主动向服务器发送数据
const onsend = () => {
sendScoket("shenjilin");
};
复制代码
websocket 心跳
我们封装 ws 这个类的时候已经添加 监听错误事件,并在监听到错误时开始重新连接ws。但是导致websocket 断开连接有很多种,比如断网以及客户端和服务器长时间未通信(nginx 代理服务器在一定事件内没有传输任何数据,连接将被关闭, 可以配置nginx的 proxy_read_timeout 读取超时配置,proxy_send_timeout 写入超时配置)。
这里我们可以使用心跳的方式解决以上出现的问题, 先来封装一个方法
utils/heartCheck.js
//心跳检测
export default {
timeout: 1000 * 6, //6秒
timeoutObj: null,
serverTimeoutObj: null,
reset: function () {
this.timeoutObj && clearTimeout(this.timeoutObj);
this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
return this;
},
start: function (socket) {
var self = this;
this.timeoutObj = setTimeout(function () {
//这里发送一个心跳,后端收到后,返回一个心跳消息,
//onmessage拿到返回的心跳就说明连接正常
socket.send("ping");
//如果超过一定时间还没重置,说明后端主动断开了
self.serverTimeoutObj = setTimeout(function () {
//如果onclose会执行reconnect,我们执行ws.close()就行了.
//如果直接执行reconnect 会触发onclose导致重连两次
socket.close();
}, self.timeout);
}, this.timeout);
},
};
复制代码
心跳开启是在我们监听 ws 连接创建成功的时候。心跳方法肯定是每过一段时间就会执行一次,所以我们这里是和后端协商。当后端接收到前端发送的心跳消息(ping),后端返回一个pong。然后 onmessage 这个事件中判断接收到消息(任何类型的消息)的时候,会重置心跳消息的时间。
在 ws 类中的两个方法 onopen 和 onmessage,添加心跳
onopen() {
this.ws.onopen = () => {
console.log("onopen: 连接成功了");
// 连接成功。 检查之前发送失败的消息,如果有就直接再次发送
this.errorStack.forEach((message) => {
this.send(message);
});
this.errorStack = [];
this.isReconnectionLoading = false;
// 重置心跳时间, 开启心跳
heartCheck.reset().start(this.ws);
};
}
onmessage() {
this.ws.onmessage = (event) => {
try {
// const data = JSON.parse(event.data);
const data = event.data;
// 接到消息重置心跳时间, 开启新的心跳
heartCheck.reset().start(this.ws);
if (data.data === "pong") return;
// 发布消息到消息中心
emitter.emit("notify", data);
// emitter.emit(data.type, data);
} catch (error) {
console.log(error, "error");
}
};
}
复制代码
最后
至此我们客户端websocket封装完成,以上实现思路以及代码上的问题,还望指正!
代码地址: github.com/shenjilina/… 如果以上对您有帮助,欢迎star