一、为什么需要 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
}
}
}