一、了解websocket
websocket是一个长链接,基于http三次握手,实时双工通讯技术(简单点说就是服务器可以给你发送消息,不需要你请求再响应)
刚开始基于http三次握手时,请求头如下:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket // 升级成【websocket】
Connection: Upgrade // 是否升级
Sec-WebSocket-Key: x3JJHMbDL1EzLksadBhXDw== // 浏览器随机生成的base64 encode值,用于验证对方是否是websocket,实现双向通讯
Sec-WebSocket-Protocol: chat, superchat // 用户定义的字符串,一般用来传tocken
Sec-WebSocket-Version: 13 // ws的版本号
Origin: http://example.com // 请求源地址
告诉服务器请给我升级成websocket,不要在用传统的http。
----- 到此,三次握手完成辽。。。
二、与http的区别和联系
联系
- 都是基于TCP协议;
- websocket是基于http的他们的兼容性都很好;
- 在连接的建立过程中对错误的处理方式相同;
- 都使用 Request/Response模型进行连接的建立;
- 都可以在网络中传输数据。
区别
- websocket是持久连接,http 是短连接(http可以通过Ajax一直发送请求和长轮询保持一段时间内的连接,但本质上还是短连接);
- websocket的协议是以 ws/wss 开头,http 对应的是 http/https;
- websocket是有状态的双向连接,http 是无状态的单向连接;
- websocket连接建立之后,数据的传输使用帧来传递,不再需要Request消息;
- websocket是可以跨域的。
三、WebSocket都有哪些属性、方法和事件
属性
- Socket.readyState:这是个只读属性,y用来表示连接状态
- 0:未连接 1:连接已建立 2.连接z正在关闭 3.连接已关闭或打不开连接
- Socket.bufferedAmount:也是只读属性。主要是计算还没有被send()发出的UTF-8文本字节数。
方法
- Socket.send():向服务器发送数据
- Socket.close():关闭连接
事件
- Socket.onopen:连接建立时触发
- Socke.onmessage:客户端接受服务端数据时触发
- Socket.onerror:通信错误时触发
- Socket.onclose:连接关闭时触发
四、WebSocket 使用
类模式
import clog from '@/utils/clog' // 相当于chalk.js 控制台输出有颜色的字符串
export class Websocket {
#pingstartTime
#pingendTime
#networkDelay
constructor() {
this.wsUrl = ''
this.token = ''
this.userCode = ''
this.webscoketAgent = void (0)
this.retryTimes = 5 // websocket重试次数
this.retryInteval = 5 // websocket重试间隔时间 秒
this.heartInterval = 30 // ws心跳检测时间 秒
this.heartCount = 0
this.heartClientTimeout = 0
this.onMessage = (message) => {}
this.delayCallback = (delayTimes) => {}
this.onNetDisconnect = () => {}
}
// 重置重试次数
retryTimesReset() {
this.retryTimes = 5
}
/**
* 重置心跳检测
* @returns {SeewinWebsocket}
*/
heartReset () {
this.heartCount = 0
clearInterval(this.heartClientTimeout)
return this
}
/**
* 开始心跳检测
*/
heartPingStart () {
const ctx = this
ctx.heartClientTimeout = setInterval(() => {
if (this.heartCount < 3 && typeof this.webscoketAgent !== 'undefined' && this.webscoketAgent.readyState === 1) {
this.sendMessage('Ping', { userCode: this.userCode })
this.pingstartTime = Date.now()
this.heartCount++
} else {
this.webscoketAgent.close()
this.heartReset()
}
}, this.heartInterval * 1000)
}
/**
* 发送消息
* @param sign
* @param data
*/
sendMessage (sign = '', data = {}) {
if (sign === '') { return }
// console.log('%c' + `发送时间戳${Date.now()}`, 'color:#cacaca;')
this.sendRawMessage(JSON.stringify({ sign, data }))
}
/**
* 发送ws raw消息
* @param message
*/
sendRawMessage (message = '') {
if (typeof this.webscoketAgent === 'undefined') {
clog.error('系统websocket', '发送消息失败,ws未连接')
return
}
this.webscoketAgent.send(message)
}
/**
* 连接ws
* @param wsUrl ws地址
* @param token ws连接token
* @param userCode 用户userCode
* @param onMessage ws消息处理回调
* @returns {Promise<unknown>}
*/
connect(wsUrl, token, userCode, onMessage = (message) => {}, onNetDisconnect = (message) => {}) {
const ctx = this
return new Promise((resolve, reject) => {
if (wsUrl === '' || token === '' || userCode === '') {
reject('ws地址不正确,请重新登录')
}
ctx.wsUrl = wsUrl
ctx.token = token
ctx.userCode = userCode
this.pingstartTime = Date.now()
ctx.webscoketAgent = new WebSocket(this.wsUrl, this.token);
ctx.onMessage = onMessage
ctx.onNetDisconnect = onNetDisconnect
// 打开websocket
ctx.webscoketAgent.addEventListener('open', (event) => {
clog.success('系统websocket', '已连接', { uri: wsUrl, token: token })
ctx.retryTimesReset()
ctx.pingendTime = Date.now()
ctx.networkDelay = ctx.pingendTime - ctx.pingstartTime
ctx.delayCallback(ctx.networkDelay)
// console.log(`%c[延迟]${this.networkDelay} ms`, 'color:blue;')
ctx.heartPingStart()
})
// 监听websocket消息
ctx.webscoketAgent.addEventListener('message', (event) => {
if (typeof event.data === 'undefined' || event.data === '') { return }
try {
const message = JSON.parse(event.data)
clog.info('系统websocket', '接收消息', message)
if (message.msgType === 'PingSuccess') {
this.pingendTime = Date.now()
this.networkDelay = this.pingendTime - this.pingstartTime
this.delayCallback(this.networkDelay)
// console.log(`%c[延迟]${this.networkDelay} ms`, 'color:blue;')
}
ctx.onMessage(message)
} catch (err) {
clog.error('系统websocket', '解析消息失败', event.data)
}
});
// websocket错误
this.webscoketAgent.addEventListener('error', (event) => {
clog.error('系统websocket', '错误', event)
ctx.heartReset()
setTimeout(() => {
ctx.reload()
}, ctx.retryInteval * 1000)
});
// 监听websocket消息
this.webscoketAgent.addEventListener('close', (event) => {
clog.warn('系统websocket', '已关闭', event)
ctx.heartReset()
if (ctx.retryTimes > 0) {
setTimeout(() => {
ctx.reload()
}, ctx.retryInteval * 1000)
}
});
resolve()
})
}
/**
* 断开websocket
*/
disconnect() {
if (typeof this.webscoketAgent === 'undefined') { return }
this.wsUrl = ''
this.token = ''
this.userCode = ''
this.retryTimes = 0
console.log('退出ws', this.retryTimes)
this.webscoketAgent.close()
}
/**
* 重连
*/
reload () {
if (this.retryTimes <= 0) {
clog.error('系统websocket', '重连全部完成')
this.onNetDisconnect()
return
}
clog.error('系统websocket', '正在尝试重连中...')
this.retryTimes = this.retryTimes - 1
this.connect(this.wsUrl, this.token, this.userCode, this.onMessage).catch(() => {})
}
setDelayCallback(event = (delayTimes) => {}) {
this.delayCallback = event
}
/**
* 获取延迟
*/
getNetworkDelay() {
return this.networkDelay
}
}
五、携带token
三种方式
- 通过send携带
var ws = new WebSocket("ws://" + url + "/webSocketServer");
ws.onopen=function(){
ws.send(token)
}
- 在请求地址中携带
var ws = new WebSocket("ws://" + url?token + "/webSocketServer");
var wss = new WebSocket("wss://" + url?token + "/webSocketServer");
- 基于协议头携带
websocket请求头中可以包含Sec-WebSocket-Protocol这个属性,该属性是一个自定义的子协议。它从客户端发送到服务器并返回从服务器到客户端确认子协议。我们可以利用这个属性添加token。
var ws = new WebSocket("ws://" + url+ "/webSocketServer",[Protocol]); //传输组
var ws = new WebSocket("ws://" + url+ "/webSocketServer",token); //传单个字符串
注意:如果传递了token参数,后端响应的时候,也必须携带这个token响应,否则前端收不到数据。