这是我参与8月更文挑战的第17天,活动详情查看:8月更文挑战
1,为什么需要 WebSocket?
WebSocket是HTML5出的协议,有人会问,我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?
答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。而且,HTTP 协议做不到服务器主动向客户端推送信息。这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。
轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的
2,webSocket简介
WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。
它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。 其他特点包括:
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏
蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是`ws`(如果加密,则为`wss`),服务器网址就是 URL。
3,webSocket优点
首先,Websocket是一个持久化的协议,相对于HTTP这种非持久的协议来说。 传统的HTTP的生命周期是通过Request来界定,也就是一个Request 一个Response,那么在HTTP1.0中,这次HTTP请求就结束了。
在HTTP1.1中进行了改进,使得有一个keep-alive,也就是说,在一个HTTP连接中,可以发送多个Request,接收多个Response。
但是请记住 Request = Response , 在HTTP中永远是这样,也就是说一个request只能有一个response。而且这个response也是被动的,不能主动发起
而Websocket是基于HTTP协议的,或者说借用了HTTP的协议来完成一部分握手,在握手阶段是一样的
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
上面就是Websocket的核心了,告诉Apache、Nginx等服务器:注意啦,本次发起的是Websocket协议,请用webSocket协议处理
首先,Sec-WebSocket-Key 是一个Base64 encode的值,这个是浏览器随机生成的,告诉服务器:我要验证是不是
真的是Websocket协议。
然后,Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议
最后,Sec-WebSocket-Version 是告诉服务器所使用的Websocket协议版本,
然后服务器会返回下列东西,表示已经接受到请求, 成功建立Websocket啦!**
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
这里开始就是HTTP最后负责的区域了,告诉客户,我已经成功切换协议啦~,告诉客户端即将升级的是Websocket协议, 至此,HTTP已经完成它所有工作了,接下来就是完全按照Websocket协议进行了
4,webSocket API
(1),新建 WebSocket 实例
var ws = new WebSocket('ws://localhost:8080');
(2) webSocket.readyState
WebSocket的四种状态
CONNECTING:值为0,表示正在连接。
OPEN:值为1,表示连接成功,可以通信了。
CLOSING:值为2,表示连接正在关闭。
CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
(3) webSocket.onopen
指定连接成功后的回调函数。
ws.onopen = function () {
ws.send('Hello Server!');
}
(4) webSocket.onclose
指定连接关闭后的回调函数
ws.onclose = function(event) {
var code = event.code;
var reason = event.reason;
操作关闭事件
};
(5) webSocket.onmessage
指定收到服务器数据后的回调函数
ws.onmessage = function(event) {
var data = event.data;
// 处理数据
};
(6) webSocket.send
用于向服务器发送数据。
ws.send('your message');
(7) webSocket.onerror
用于指定报错时的回调函数。
socket.onerror = function(event) {
// handle error event
};
5,webSocket实战封装
const Base64 = window.Base64
let timeoutObj = null // 心跳倒计时
let serverTimeoutObj = null // 心跳倒计时
let reconnetNum = 0 // 重连次数
let lockReconnect = false // 是否重新建立连接
let timeoutnum = null // 断开 重连倒计时
let receiveNews = [] // 接受到的消息
let isOrReceive = false // 是否在45秒内收到消息
let webSocketUrl = '' // websocket创建地址
let websocket = null
var wsModuleObj = '' // 所属模块
let wsSystem = '' // 所属系统
let messageId = '' // 消息ID
const sendObj = {
load: {},
path: 'internal/heart_beat',
protocolId: 'PROTO_HEART_BEAT',
sourceType: 'request',
taskId: ''
}
/**
* @description 心跳消息动态生成16位字符串
*/
function uuid() {
function S4() {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1)
}
return (S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4())
}
// 重新连接
function reconnect(wsUrls) {
addLog(JSON.stringify('链接已经断开,' + new Date().toString()), 2004)
Message({message: 'websocket链接已断开',type: 'error'})
if (lockReconnect){
return
}
lockReconnect = true
// 没连接上会一直重连,设置延迟避免请求过多
timeoutnum && clearTimeout(timeoutnum)
timeoutnum = setTimeout(function () {
if (reconnetNum < 4) {
reconnetNum++
websocket = null
createWebSocket(wsUrls)
lockReconnect = false
} else {
timeoutObj && clearInterval(timeoutObj)
timeoutnum && clearTimeout(timeoutnum)
websocket.close()
messageBox()
return
}
}, 20000)
}
// 创建两个定时器
function heartCheck() {
// 第一个定时器每隔20S给后端发送一个消息
timeoutObj && clearInterval(timeoutObj)
timeoutObj = setInterval(() => {
websocketsend(sendObj)
}, 20000)
serverTimeoutObj && clearInterval(serverTimeoutObj)
// 第二个定时器每隔45S监测 后端是否给我发送了心跳
serverTimeoutObj = setInterval(() => {
if(isOrReceive) {
isOrReceive = false
} else {
reconnect(webSocketUrl)
}
},45000)
}
// 连接成功
function wsOnopen() {
reconnetNum = 0
heartCheck() // 开启心跳监测
}
// 发生异常
function wsOnerror(e) {
reconnect(webSocketUrl) // 发生异常,重新链接
}
// 关闭链接
export function websocketclose(e) {
websocket.close()
}
/**
* @description 接收服务器推送的信息
* @param {消息事件参数} e
* @param {所属模块} moduleObj
* @param {所属系统} system
*/
export function websocketonmessage(e, moduleObj, system) {
if (e && e.data) { // 发送消息的模块
const tempData = JSON.parse(e.data)
const redata = tempData.load
if (tempData.path === 'internal/heart_beat') { // 如果是心跳消息
isOrReceive = true // 告诉45s定时器,收到了心跳消息
sendObj.load = redata
} else {
for (let i = 0; i < moduleObj.length; i++) {
if (tempData.path !== 'internal/heart_beat' ) {
const logInfo = { 'messageId:': redata.msgId, 'content': redata.content, 'all': redata}
receiveNews.unshift(redata)
if (messageId !== redata.messageId) {
moduleObj[i].val(redata)
messageId = redata.messageId
}
receiveNews = []
}
}
}
}
}
/**
* @description 向服务器发送信息
* @param {消息内容}} msg
*/
function websocketsend(msg) {
var jsonString = JSON.stringify(msg)
if (websocket.readyState === 1) { // 如果连接正常
websocket.send(jsonString)
}
}
/**
* @description webSocket 对外注册方法
* @param {环境变量} env
* @param {所属模块} moduleObj
* @param {所属系统} system
* @param {UM账号} um
* @param {令牌身份}} token
*/
export function register(env, moduleObj, system, um) {
addLog(JSON.stringify('初始化长链接:'+ um + ',' + system + ',' + moduleObj + ',' + env + ',' +
new Date().toString()), 2002)
initWebSocket(env, moduleObj, system, um)
}
// 初始化创建webSocketUrl
function initWebSocket(env, moduleObj, system, umId) {
wsModuleObj = moduleObj
wsSystem = system || window.localStorage.getItem('systemId')
let wsUrls
const um = umId || window.localStorage.getItem('umId')
const base64 = new Base64(um.toUpperCase())
let token = base64.encode(um)
if (env === 'stg') {
wsUrls = 'ws://xxxxx'
} else if (env === 'prd') {
wsUrls = 'ws://xxxxx'
} else if (env === 'dev') {
wsUrls = 'ws://xxxxx'
}
wsUrls = wsUrls + '?token=' + token + '&system=' + wsSystem
webSocketUrl = wsUrls
createWebSocket(webSocketUrl)
}
/**
* @description 创建长链接,并初始化方法
* @param {长链接地址} url
*/
function createWebSocket(url) {
if ('WebSocket' in window) { // 判断当前浏览器是否支持WebSocket
if(!websocket) {
websocket = new WebSocket(url) // 初始化webSocket
init() // 初始化websocket的方法
}
} else {
alert('当前浏览器 Not support websocket')
}
}
/**
* @description 初始化websocket事件
*/
function init() {
sendObj.taskId = uuid() // 初始化taskId
websocket.onerror = wsOnerror // 链接错误
websocket.onclose = websocketclose // 链接关闭
websocket.onopen = wsOnopen // 链接成功
websocket.onmessage = (e) => { websocketonmessage(e, wsModuleObj, wsSystem) } // 接受信息
}