WebSocket在服务端和客户端通信demo,支持心跳检测+断线重连

一、为什么需要 WebSocket?

初次接触 WebSocket 的人,都会问同样的问题:我们已经有了 HTTP 协议,为什么还需要另一个协议?它能带来什么好处?

答案很简单,因为 HTTP 协议有一个缺陷:通信只能由客户端发起。

举例来说,我们想了解今天的天气,只能是客户端向服务器发出请求,服务器返回查询结果。HTTP 协议做不到服务器主动向客户端推送信息。

这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。

轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。

它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

其他特点包括:

(1)建立在 TCP 协议之上,服务器端的实现比较容易。

(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

(3)数据格式比较轻量,性能开销小,通信高效。

(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信。

(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL

----------------------------------------------------------------------------------------------------------------------------

以上参考: http://www.ruanyifeng.com/blog/2017/05/websocket.html

下面是可以用于生产环境的demo


服务端:


// npm install nodejs-websocket
const ws = require( "nodejs-websocket")

console. log( "开始建立链接")
let rd, obj, time, intervalObj = null
const server = ws. createServer( conn => {
conn. on( "text", str => {
time = new Date()
time = time. getFullYear() + "-" + ( time. getMonth() + 1) + "-" + time. getDate() + " "
+ time. getHours() + ":" + time. getMinutes() + ":" + time. getSeconds()
console. log( `接受到的信息为: ${ str } , 接受到信息的时间为: ${ time } `)
obj = JSON. parse( str)
// 回应浏览器发送的ping,判断链接是否已经断开
if ( obj. event === "ping") {
conn. sendText( '{"event": "pong"}')
}
// 订阅数据
if ( obj. event === "getData") {
// 模拟数据一直在推送
intervalObj = setInterval(() => {
rd = Math. floor( Math. random( 0, 1) * 10)
conn. sendText( JSON. stringify({ event: 'getData', dt: { num: rd}}))
}, 1000)
}
})
conn. on( "close", function ( code, reason) {
clearInterval( intervalObj)
console. log( "Connection closed")
})
// 必须监控error, 每当浏览器刷新时会断开链接报错
conn. on( "error", function ( error) {
clearInterval( intervalObj)
console. log( "Connection error", error)
})
}). listen( 8001)

客户端

index.html

<!DOCTYPE html >
< html lang= "en" >
< head >
< meta charset= "UTF-8" >
< meta name= "viewport" content= "width=device-width, initial-scale=1.0" >
< meta http-equiv= "X-UA-Compatible" content= "ie=edge" >
< title >Document </ title >
</ head >
< body >
< div id= "message" ></ div >
</ body >
</ html >
< script type= "text/javascript" src= "./webSocket.js" ></ script >
< script type= "text/javascript" src= "./init.js" ></ script >


init.js

const msg = document. querySelector( "#message")
let ws, _getTimer
// 整个站点只要创建一次websocket链接,不同的数据源进行多次订阅即可
ws = new webSocketFn( "ws://127.0.0.1:8001")
// 注册异常调用函数,每个订阅要有一个异常处理函数
let webSocketData = () => { ws. webSocketSend( '{"event": "getData"}') }
let ajaxData = () => {
// 接口请求
console. log( 777)
}
let fail = ws. getSocketFile( webSocketData, ajaxData)
ws. errorCallBackFunArr. push( fail)
// 注册成功的回掉,每个订阅对应的要注册一个回掉
ws. successFn[ 'getData'] = ( data) => {
data && ( msg. innerHTML = data. dt. num)
}
let firstSend = () => {
if ( ws. isConnection()) {
webSocketData()
} else {
// 第一次请求可能websocket还没有链接,过200毫秒再试一次
ajaxData()
setTimeout( webSocketData, 200)
}
}
firstSend()


webSocket.js


class WebSocketClass {
constructor ( wsUrl) {
this. successFn = {} // 成功的回掉函数
this. wsUrl = wsUrl // 请求的url
this. errorCallBackFunArr = [] // 当推送异常时执行的数组
this. isConnection = false // 判断是否支持weosocket
this. isErrorCallBack = false
this. lockReconnect = false // 避免重复连接
this. ping = null
this. sendObj = {}
this. heartCheck()
this. createWebSocket()
}
createWebSocket () {
try {
this. webSocket = new WebSocket( this. wsUrl)
this. initEventHandle()
} catch ( e) {
this. errorCallBackData()
this. reconnect( this. wsUrl)
}
}
initEventHandle () {
this. webSocket. onclose = () => {
this. errorCallBackData()
this. reconnect( this. wsUrl)
}
this. webSocket. onerror = () => {
this. errorCallBackData()
this. reconnect( this. wsUrl)
}
this. webSocket. onopen = () => {
this. isConnection = true
this. isErrorCallBack = false
clearInterval( this. ping)
this. ping = setInterval(() => {
this. send( '{"event": "ping"}')
}, 10000)
// 心跳检测重置
this. heartCheck. start()
}
this. webSocket. onmessage = data => {
// 如果获取到消息,心跳检测重置
// 拿到任何消息都说明当前连接是正常的
this. heartCheck. start()
this. decodeData( data)
}
}
send( cmd) {
// 只有当 webSocket.readyState 为 OPEN才发送订阅
// readyState属性返回实例对象的当前状态,共有四种。
// CONNECTING:值为0,表示正在连接。
// OPEN:值为1,表示连接成功,可以通信了。
// CLOSING:值为2,表示连接正在关闭。
// CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
if (! cmd) {
return
}
// this.sendObj[cmd] && (delete clearTimeout(this.sendObj[cmd]))
if ( this. webSocket. readyState === 1) { // 只有当链接打开时才进行订阅
this. webSocket. send( cmd)
}
// else if (this.webSocket.readyState === 0) { // 如果链接处于正在链接中,则进行延时订阅
// this.sendObj[cmd] = setTimeout(() => {
// this.send(cmd)
// }, 50)
// }

}

reconnect ( url) {
if ( this. lockReconnect) {
return
}
this. lockReconnect = true
// 没连接上会一直重连,设置延迟避免请求过多
setTimeout(() => {
this. createWebSocket( url)
this. lockReconnect = false
}, 2000)
}
errorCallBackData () {
this. isConnection = false
if (! this. isErrorCallBack && this. errorCallBackFunArr. length) {
this. isErrorCallBack = true
for ( let i = 0, len = this. errorCallBackFunArr. length; i < len; i++) {
if (( typeof this. errorCallBackFunArr[ i]) === 'function') {
this. errorCallBackFunArr[ i]()
}
}
}
}
decodeData ( data) {
if ( data. data instanceof Blob) {
let blob = data. data
// js中的blob没有没有直接读出其数据的方法,通过FileReader来读取相关数据
let reader = new FileReader()
reader. readAsArrayBuffer( blob)
// 当读取操作成功完成时调用.
reader. onload = ( evt) => {
if ( evt. target. readyState === FileReader. DONE) {
let result = new Uint8Array( evt. target. result)
// 如果后端进行压缩数据处理(zlib),那么要引入解析zlib的js
result = ( new window. Zlib. RawInflate( result)). decompress()
let strResult = ''
let length = result. length
for ( let i = 0; i < length; i++) {
strResult += String. fromCharCode( result[ i])
}
this. callBackData( JSON. parse( strResult))
}
}
return
}
let d = JSON. parse( data. data)
// 如果后端需要等待,则返回code:10010,过一段时间后重新订阅
if ( d. code === '10010') {
let dt = {}
Object. assign( dt, d)
delete dt. code
delete dt. msg
setTimeout(() => {
this. send( JSON. stringify( dt))
}, 2000)
return
}
this. callBackData( JSON. parse( data. data))
}
callBackData ( data) {
if ( data instanceof Array) {
for ( let i = 0; i < data. length; i++) {
this. doCallback( data[ i])
}
} else if ( data instanceof Object) {
if ( data. hasOwnProperty( 'event') && data. event === 'pong') {
return
}
data. payload && ( data. payload = JSON. parse( data. payload))
this. doCallback( data)
}
}
doCallback ( data) {
if ( data. event) {
let fn = this. successFn[ data. event]
if ( typeof fn === 'function') {
fn( data)
}
}
}
// 心跳检测
heartCheck () {
let that = this
this. heartCheck = {
timeout: 10000, // 10秒
timeoutObj: null,
serverTimeoutObj: null,
reset : function () {
clearTimeout( this. timeoutObj)
clearTimeout( this. serverTimeoutObj)
},
start : function () {
this. reset()
let self = this
this. timeoutObj = setTimeout( function () {
// 这里发送一个心跳,后端收到后,返回一个心跳消息
// onmessage拿到返回的心跳就说明连接正常
that. send( '{"event": "ping"}')
self. serverTimeoutObj = setTimeout( function () { // 如果超过一定时间还没重置,说明后端主动断开了
// 如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
that. webSocket. close()
}, self. timeout)
}, this. timeout)
}
}
}
}
function webSocketFn ( url) {
let wb = new WebSocketClass( url)
return {
webSocketSend: wb. send. bind( wb),
errorCallBackFunArr: wb. errorCallBackFunArr,
successFn: wb. successFn,
isConnection : function () {
return wb. isConnection
},
getSocketFile ( webSocketData, ajaxData) {
let socketFail = ( isFirst) => {
if ( this. isConnection()) {
webSocketData()
} else {
ajaxData()
setTimeout( socketFail, 2000)
}
}
return socketFail
}
}
}

猜你喜欢

转载自blog.csdn.net/zsnpromsie/article/details/80646316