WebSocket是一种在Web浏览器和Web服务器之间创建持久性连接的通信协议。它允许客户端和服务器之间进行全双工通信,这意味着服务器可以主动向客户端发送数据,而不需要客户端首先发起请求。通常用于实时数据传输的场景
背景
- 在 websocket 出来前,想实现实时通信、变更推送、服务端信息推送功能,一般方案是使用 ajax 短轮询、长轮询两种方式
- 短轮询(short polling)是客户端定时向服务器发出请求,询问服务器是否有新的数据。如果服务器有新的数据,就立即返回给客户端;如果没有,就返回一个空的响应。这种方式的问题是如果数据变化的频率比较低,会产生大量无效请求,浪费带宽。
- 长轮询(long polling)也是客户端向服务器发出请求,但服务器并不立即返回响应,而是等到有新的数据时才返回。这样就可以减少无效请求,降低带宽的消耗。但长轮询可能导致服务器需要维持大量未完成的请求,增加了服务器的负担。
通信过程以及原理
建立链接
- WebSocket 协议属于应用层协议,依赖传输层的 TCP 协议。通过 HTTP/1.1 协议的101状态码进行握手建立连接
具体过程
- 客户端发送一个 HTTP GET 请求到服务器,请求的路径是 WebSocket 的路径(类似 ws://example.com/socket)。请求中包含一些特殊的头字段,如 Upgrade: websocket 和 Connection: Upgrade,以表明客户端希望升级连接为 WebSocket
- 服务器收到这个请求后,会返回一个 HTTP 101 状态码(协议切换协议)。同样在响应头中包含 Upgrade: websocket 和 Connection: Upgrade,以及一些其他的 WebSocket 特定的头字段,例如 Sec-WebSocket-Accept,用于验证握手的合法性
- 客户端和服务器之间的连接从普通的 HTTP 连接升级为 WebSocket 连接。之后,客户端和服务器之间的通信就变成了 WebSocket 帧的传输,而不再是普通的 HTTP 请求和响应
// 客户端请求
GET ws://localhost:8888/ HTTP/1.1
Host: localhost:8888
Connection: Upgrade
Upgrade: websocket
Origin: http://localhost:63342
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,ja;q=0.8,en;q=0.7
Sec-WebSocket-Key: b7wpWuB9MCzOeQZg2O/yPg==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
// 服务端响应
HTTP/1.1 101 Web Socket Protocol Handshake
Connection: Upgrade
Date: Wed, 22 Nov 2023 08:15:00 GMT
Sec-WebSocket-Accept: Q4TEk+qOgJsKy7gedijA5AuUVIw=
Server: TooTallNate Java-WebSocket
Upgrade: websocket
优缺点
优点
- 实时性
- 减少网络延迟:与短轮询和长轮询相比
- 较小的数据传输开销:WebSocket 的数据帧相比于 HTTP 请求报文较小
- 较低的服务器资源占用
- 跨域通信:与一些其他跨域通信方法相比,WebSocket 更容易实现跨域通信
缺点
- 连接状态保持
- 不适用于所有场景
- 复杂性
适用场景
- 实时聊天应用
- 在线协作和协同编辑
- 实时数据展示
- 在线游戏
- 推送服务
封装一个 Socket 类
class Socket {
constructor (email) {
this.email = email;
this.instance = null;
this.ws = null;
this.autoConnectTimes = 0;
this.aliveTimer = null;
this.handlers = {};
}
static getInstance(email) {
if(!this.instance) {
this.instance = new Socket(email);
};
return this.instance;
}
connect () {
//无效
if(!this.email) return;
//创建
if('WebSocket' in window) {
//协议类型 http: / https:
const protocalType = window.location.protocol;
const wsProtocol = protocalType == 'https:' ? 'wss:' : 'ws:'
//实例话WebSocket对象
let connectUrl = '';
if(process.env.NODE_ENV == 'development' || process.env.DEPLOY_ENV == 'dev' || process.env.DEPLOY_ENV == 'qa') {
connectUrl = '';
} else if(process.env.DEPLOY_ENV == 'pre') {
connectUrl = '';
} else {
connectUrl = '';
};
this.ws && this.ws.close();
this.ws = new WebSocket(connectUrl);
this.ws.onopen = () => {
this.onOpen.apply(this);
};
this.ws.onmessage = (evt) => {
this.onMeessage.apply(this, [evt])
};
this.ws.onclose = (evt) => {
this.onClose.apply(this, [evt]);
};
this.ws.onerror = () => {
this.onError.apply(this);
};
} else {
Vue.prototype.$toast({
type: 'error',
content: '您的浏览器不支持 WebSocket,请选择Chrome、Firefox等登录!'
});
};
//关闭
let that = this;
window.onbeforeunload = function() {
try {
if(that.ws) {
that.ws.onclose = function () {};
that.ws.close();
};
} catch (error) {
//
};
};
}
//建立连接后的回调
onOpen () {
this.sendMessage({'model': 'callcenter', 'type': 'seatstatus'});
//ping && pang
this.keepWsAlive();
this.autoConnectTimes = 0; //重置
}
//收到消息回调
onMeessage (evt) {
let data = JSON.parse(evt.data);
// 处理各种消息类型
this.dispatchEvent('message', data);
}
//连接断开回调
onClose (evt) {
//打印断开原因
let flag = evt.wasClean || false;
//关闭心跳
if(this.aliveTimer !== null) {
clearInterval(this.aliveTimer);
};
//非手动断开状态状态,断开连接后自动连接
if(!flag) {
this.autoConnect();
};
}
//通信错误时触发
onError () {
//断开连接后自动连接
// this.autoConnect();
}
//发送消息 @params {name: params, type: Obejct}
sendMessage (params) {
let str = JSON.stringify(params);
try {
if(this.ws.readyState == 1) {
this.ws.send(str);
} else {
console.log(this.ws.readyState);
};
} catch (error) {
console.log(error);
};
}
//心跳
keepWsAlive () {
if(this.aliveTimer !== null) {
clearInterval(this.aliveTimer);
};
this.aliveTimer = setInterval(() => {
this.sendMessage({'model': 'ping'});
}, 10000);
}
autoConnect () {
if(this.autoConnectTimes < 20) {
this.autoConnectTimes++;
setTimeout(() => {
this.connect();
}, 2000);
} else {
// 异常断开
this.dispatchEvent('ccBreakLine', {});
}
}
//添加订阅者
addEventListener (type='message', handler) {
// 首先判断handlers内有没有type事件容器,没有则创建一个新数组容器
if (!(type in this.handlers)) {
this.handlers[type] = [];
};
// 将事件存入
this.handlers[type].push(handler);
}
// 触发事件
dispatchEvent (type='message', ...params) {
// 若没有注册该事件则抛出错误
if (!(type in this.handlers)) {
return new Error('未注册该事件')
};
// 便利触发
this.handlers[type].forEach(handler => {
handler(...params);
});
}
removeEventListener (type, handler) {
// 无效事件抛出
if (!(type in this.handlers)) {
return new Error('无效事件');
}
if (!handler) {
// 直接移除事件
delete this.handlers[type];
} else {
const idx = this.handlers[type].findIndex(ele => ele === handler);
// 抛出异常事件
if (idx === undefined) {
return new Error('无该绑定事件');
}
// 移除事件
this.handlers[type].splice(idx, 1)
if (this.handlers[type].length === 0) {
delete this.handlers[type];
};
};
}
};
export default Socket;