当前位置:   article > 正文

WebSocket使用及优化(心跳机制与断线重连)_websocket timeout

websocket timeout

WebSocket在2008年被提出,其通信协议于2011被制定为标准
与http不同,websocket支持全双工通信(即:在客户端和服务之间双向通信)在websocket问世之前,客户端与服务器通常采用http轮询和Comet等方式保持长链接
然而,这么做无疑会对服务端造成资源消耗,因为HTTP请求包含较长的头文件,只传递了少许的有用信息,十分消耗资源。
于是websocket便诞生了,它不仅节省资源和带宽,更是能实现长链接作用,只需客户端主动与服务端握手一次,即可进行实时通信,实现推送技术。

之前我也写过相关的文章:Socket聊天室使用JS+socket.io+WebRTC+nodejs+express搭建一个简易版远程视频聊天,但是用到的模块都是socket.io,而且没有深入优化,在平时工作上真正用到时发现事情并不简单。有时前端或者后端会断线而对方不知道,像弱网或者后端服务器重启时,前端并不能保证一直连接
所以这篇文章,我们就来使用websocket做一个简单的demo,并且加上心跳和断线重连功能

首先是服务端,采用node+ws模块搭建websocket服务,在server文件夹下新建server.js,并在npm初始化后,下载ws模块

  1. npm init -y
  2. npm i ws

引入ws模块,并搭建一个简单的websocket服务

  1. const WebSocket = require('ws');
  2. const port = 1024//端口
  3. const pathname = '/ws/'//访问路径
  4. new WebSocket.Server({port}, function () {
  5. console.log('websocket服务开启')
  6. }).on('connection', connectHandler)
  7. function connectHandler (ws) {
  8. console.log('客户端连接')
  9. ws.on('error', errorHandler)
  10. ws.on('close', closeHandler)
  11. ws.on('message', messageHandler)
  12. }
  13. function messageHandler (e) {
  14. console.info('接收客户端消息')
  15. this.send(e)
  16. }
  17. function errorHandler (e) {
  18. console.info('客户端出错')
  19. }
  20. function closeHandler (e) {
  21. console.info('客户端已断开')
  22. }

前端部分也搭建个ws访问客户端

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>
  6. Title</title>
  7. </head>
  8. <body>
  9. <script type="module">
  10. const name = 'test'//连接用户名
  11. let wsUrl = 'ws://127.0.0.1:1024/ws/?name=' + name
  12. const ws = new WebSocket(wsUrl)
  13. ws.onopen = function (e) {
  14. console.log('开启')
  15. ws.send(JSON.stringify({
  16. ModeCode: "message",
  17. msg: 'hello'
  18. }))
  19. }//连接上时回调
  20. ws.onclose = function (e) {
  21. console.log('关闭')
  22. }//断开连接时回调
  23. ws.onmessage = function (e) {
  24. let data = JSON.parse(e.data)
  25. console.log('收到消息' + data.msg)
  26. ws.close()
  27. }//收到服务端消息
  28. ws.onerror = function (e) {
  29. console.log('出错')
  30. }//连接出错
  31. </script>
  32. </body>
  33. </html>

前端打印结果:

服务端打印结果:

有以上效果说明一个最简单的ws连接就实现了,下面,我们优化一下,为了降低耦合,我们先引入eventBus发布订阅,然后新建一个websocket类继承自原生WebSocket,因为,我们要在里面做心跳和重连
在服务端,我们先把server完善一下,通过http的upgrade过滤验证ws连接
在原有的服务端增加http服务并做好路径验证

  1. const http = require('http');
  2. const server = http.createServer()
  3. server.on("upgrade", (req, socket, head) => {//通过http.server过滤数据
  4. let url = new URL(req.url, `http://${req.headers.host}`)
  5. let name = url.searchParams.get('name')//获取连接标识
  6. if(!checkUrl(url.pathname, pathname)) {//未按标准
  7. socket.write('未按照标准访问');
  8. socket.destroy();
  9. return;
  10. }
  11. })
  12. server.listen(port, () => {
  13. console.log('服务开启')
  14. })
  15. //验证url标准
  16. function checkUrl (url, key) {//判断url是否包含key
  17. return - ~ url.indexOf(key)
  18. }

完成httpServer后,我们再完善一下websocket服务,将每一个连接的用户都通过代理保存并实现增删,得到以下完整的服务端

  1. const http = require('http');
  2. const WebSocket = require('ws');
  3. const port = 1024//端口
  4. const pathname = '/ws/'//访问路径
  5. const server = http.createServer()
  6. class WebSocketServer extends WebSocket.Server {
  7. constructor () {
  8. super(...arguments);
  9. this.webSocketClient = {}//存放已连接的客户端
  10. }
  11. set ws (val) {//代理当前的ws,赋值时将其初始化
  12. this._ws = val
  13. val.t = this;
  14. val.on('error', this.errorHandler)
  15. val.on('close', this.closeHandler)
  16. val.on('message', this.messageHandler)
  17. }
  18. get ws () {
  19. return this._ws
  20. }
  21. messageHandler (e) {
  22. console.info('接收客户端消息')
  23. let data = JSON.parse(e)
  24. switch(data.ModeCode) {
  25. case 'message':
  26. console.log('收到消息' + data.msg)
  27. this.send(e)
  28. break;
  29. case 'heart_beat':
  30. console.log(`收到${this.name}心跳${data.msg}`)
  31. this.send(e)
  32. break;
  33. }
  34. }
  35. errorHandler (e) {
  36. this.t.removeClient(this)
  37. console.info('客户端出错')
  38. }
  39. closeHandler (e) {
  40. this.t.removeClient(this)
  41. console.info('客户端已断开')
  42. }
  43. addClient (item) {//设备上线时添加到客户端列表
  44. if(this.webSocketClient[item['name']]) {
  45. console.log(item['name'] + '客户端已存在')
  46. this.webSocketClient[item['name']].close()
  47. }
  48. console.log(item['name'] + '客户端已添加')
  49. this.webSocketClient[item['name']] = item
  50. }
  51. removeClient (item) {//设备断线时从客户端列表删除
  52. if(!this.webSocketClient[item['name']]) {
  53. console.log(item['name'] + '客户端不存在')
  54. return;
  55. }
  56. console.log(item['name'] + '客户端已移除')
  57. this.webSocketClient[item['name']] = null
  58. }
  59. }
  60. const webSocketServer = new WebSocketServer({noServer: true})
  61. server.on("upgrade", (req, socket, head) => {//通过http.server过滤数据
  62. let url = new URL(req.url, `http://${req.headers.host}`)
  63. let name = url.searchParams.get('name')//获取连接标识
  64. if(!checkUrl(url.pathname, pathname)) {//未按标准
  65. socket.write('未按照标准访问');
  66. socket.destroy();
  67. return;
  68. }
  69. webSocketServer.handleUpgrade(req, socket, head, function (ws) {
  70. ws.name = name//添加索引,方便在客户端列表查询某个socket连接
  71. webSocketServer.addClient(ws);
  72. webSocketServer.ws = ws
  73. });
  74. })
  75. server.listen(port, () => {
  76. console.log('服务开启')
  77. })
  78. //验证url标准
  79. function checkUrl (url, key) {//判断url是否包含key
  80. return - ~ url.indexOf(key)
  81. }

当连接断开时,只有客户端主动访问服务端才能实现重连,所以客户端的功能要比服务端更多一些,我们把客户端的websocket完善优化一下,添加一些简单的控制功能(连接,发消息,断开)的按钮,这里有一点需要注意:在下次连接之前一定要先关闭当前连接,否则会导致多个客户端同时连接,消耗性能

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>
  6. Title</title>
  7. </head>
  8. <body>
  9. <button id="connect">
  10. 连接
  11. </button>
  12. <button disabled
  13. id="sendMessage">
  14. 发送
  15. </button>
  16. <button disabled
  17. id="destroy">
  18. 关闭
  19. </button>
  20. <script type="module">
  21. const name = 'test'//连接用户名
  22. let connect = document.querySelector('#connect'),//连接按钮
  23. sendMessage = document.querySelector('#sendMessage'),//发送按钮
  24. destroy = document.querySelector('#destroy'),//关闭按钮
  25. wsUrl = 'ws://127.0.0.1:1024/ws/?name=' + name,//连接地址
  26. ws;
  27. connect.addEventListener('click', connectWebSocket)
  28. sendMessage.addEventListener('click', function (e) {
  29. ws.send(JSON.stringify({
  30. ModeCode: "message",
  31. msg: 'hello'
  32. }))
  33. })
  34. destroy.addEventListener('click', function (e) {
  35. ws.close()
  36. ws = null
  37. })
  38. function connectWebSocket () {
  39. if(!ws) {//第一次执行,初始化或ws断开时可执行
  40. ws = new WebSocket(wsUrl)
  41. initWebSocket()
  42. }
  43. }
  44. function initWebSocket () {
  45. ws.onopen = function (e) {
  46. setButtonState('open')
  47. console.log('开启')
  48. }//连接上时回调
  49. ws.onclose = function (e) {
  50. setButtonState('close')
  51. console.log('关闭')
  52. }//断开连接时回调
  53. ws.onmessage = function (e) {
  54. let data = JSON.parse(e.data)
  55. console.log('收到消息' + data.msg)
  56. }//收到服务端消息
  57. ws.onerror = function (e) {
  58. setButtonState('close')
  59. console.log('出错')
  60. }//连接出错
  61. }
  62. /*
  63. * 设置按钮是否可点击
  64. * @param state:open表示开启状态,close表示关闭状态
  65. */
  66. function setButtonState (state) {
  67. switch(state) {
  68. case 'open':
  69. connect.disabled = true
  70. sendMessage.disabled = false
  71. destroy.disabled = false
  72. break;
  73. case 'close':
  74. connect.disabled = false
  75. sendMessage.disabled = true
  76. destroy.disabled = true
  77. break;
  78. }
  79. }
  80. </script>
  81. </body>
  82. </html>

效果如下:

到了这一步,我们websocket的demo已经可以手动运行,在此基础上,我们将其封装一下,并且通过eventBus对外进行通信就可以用了,具体流程将与接下来的心跳一起实现
websocket心跳机制:顾名思义,就是客户端每隔一段时间向服务端发送一个特有的心跳消息,每次服务端收到消息后只需将消息返回,此时,若二者还保持连接,则客户端就会收到消息,若没收到,则说明连接断开,此时,客户端就要主动重连,完成一个周期
心跳的实现也很简单,只需在第一次连接时用回调函数做延时处理,此时还需要设置一个心跳超时时间,若某时间段内客户端发送了消息,而服务端未返回,则认定为断线。下面,我就来实现一下心跳

  1. //this.heartBeat ---> time:心跳时间间隔 timeout:心跳超时间隔
  2. /*
  3. * 心跳初始函数
  4. * @param time:心跳时间间隔
  5. */
  6. function startHeartBeat (time) {
  7. setTimeout(() => {
  8. this.sendMsg({
  9. ModeCode: ModeCode.HEART_BEAT,
  10. msg: new Date()
  11. })
  12. this.waitingServer()
  13. }, time)
  14. }
  15. //延时等待服务端响应,通过webSocketState判断是否连线成功
  16. function waitingServer () {
  17. this.webSocketState = false//在线状态
  18. setTimeout(() => {
  19. if(this.webSocketState) {
  20. this.startHeartBeat(this.heartBeat.time)
  21. return
  22. }
  23. console.log('心跳无响应,已断线')
  24. this.close()
  25. //重连操作
  26. }, this.heartBeat.timeout)
  27. }
'
运行

心跳实现完成后,只需要在ws.onopen中调用即可,效果如下:


然后是重连部分,其实只需要新建一个延时回调,与心跳相似,只不过它是在连接失败时使用的,这里就不多做说明。以下是完整版的代码:
websocket部分:

  1. import eventBus
  2. from "./eventBus.js"
  3. const ModeCode = {//websocket消息类型
  4. MSG: 'message',//普通消息
  5. HEART_BEAT: 'heart_beat'//心跳
  6. }
  7. export default class MyWebSocket extends WebSocket {
  8. constructor (url, protocols) {
  9. super(url, protocols);
  10. return this
  11. }
  12. /*
  13. * 入口函数
  14. * @param heartBeatConfig time:心跳时间间隔 timeout:心跳超时间隔 reconnect:断线重连时间间隔
  15. * @param isReconnect 是否断线重连
  16. */
  17. init (heartBeatConfig, isReconnect) {
  18. this.onopen = this.openHandler//连接上时回调
  19. this.onclose = this.closeHandler//断开连接时回调
  20. this.onmessage = this.messageHandler//收到服务端消息
  21. this.onerror = this.errorHandler//连接出错
  22. this.heartBeat = heartBeatConfig
  23. this.isReconnect = isReconnect
  24. this.reconnectTimer = null//断线重连时间器
  25. this.webSocketState = false//socket状态 true为已连接
  26. }
  27. openHandler () {
  28. eventBus.emitEvent('changeBtnState', 'open')//触发事件改变按钮样式
  29. this.webSocketState = true//socket状态设置为连接,做为后面的断线重连的拦截器
  30. this.heartBeat && this.heartBeat.time ? this.startHeartBeat(this.heartBeat.time) : ""//是否启动心跳机制
  31. console.log('开启')
  32. }
  33. messageHandler (e) {
  34. let data = this.getMsg(e)
  35. switch(data.ModeCode) {
  36. case ModeCode.MSG://普通消息
  37. console.log('收到消息' + data.msg)
  38. break;
  39. case ModeCode.HEART_BEAT://心跳
  40. this.webSocketState = true
  41. console.log('收到心跳响应' + data.msg)
  42. break;
  43. }
  44. }
  45. closeHandler () {//socket关闭
  46. eventBus.emitEvent('changeBtnState', 'close')//触发事件改变按钮样式
  47. this.webSocketState = false//socket状态设置为断线
  48. console.log('关闭')
  49. }
  50. errorHandler () {//socket出错
  51. eventBus.emitEvent('changeBtnState', 'close')//触发事件改变按钮样式
  52. this.webSocketState = false//socket状态设置为断线
  53. this.reconnectWebSocket()//重连
  54. console.log('出错')
  55. }
  56. sendMsg (obj) {
  57. this.send(JSON.stringify(obj))
  58. }
  59. getMsg (e) {
  60. return JSON.parse(e.data)
  61. }
  62. /*
  63. * 心跳初始函数
  64. * @param time:心跳时间间隔
  65. */
  66. startHeartBeat (time) {
  67. setTimeout(() => {
  68. this.sendMsg({
  69. ModeCode: ModeCode.HEART_BEAT,
  70. msg: new Date()
  71. })
  72. this.waitingServer()
  73. }, time)
  74. }
  75. //延时等待服务端响应,通过webSocketState判断是否连线成功
  76. waitingServer () {
  77. this.webSocketState = false
  78. setTimeout(() => {
  79. if(this.webSocketState) {
  80. this.startHeartBeat(this.heartBeat.time)
  81. return
  82. }
  83. console.log('心跳无响应,已断线')
  84. try {
  85. this.close()
  86. } catch(e) {
  87. console.log('连接已关闭,无需关闭')
  88. }
  89. this.reconnectWebSocket()
  90. }, this.heartBeat.timeout)
  91. }
  92. //重连操作
  93. reconnectWebSocket () {
  94. if(!this.isReconnect) {
  95. return;
  96. }
  97. this.reconnectTimer = setTimeout(() => {
  98. eventBus.emitEvent('reconnect')
  99. }, this.heartBeat.reconnect)
  100. }
  101. }

index.html部分:

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>
  6. Title</title>
  7. </head>
  8. <body>
  9. <button id="connect">
  10. 连接
  11. </button>
  12. <button disabled
  13. id="sendMessage">
  14. 发送
  15. </button>
  16. <button disabled
  17. id="destroy">
  18. 关闭
  19. </button>
  20. <script type="module">
  21. import eventBus
  22. from "./js/eventBus.js"
  23. import MyWebSocket
  24. from './js/webSocket.js'
  25. const name = 'test'//连接用户名
  26. let connect = document.querySelector('#connect')
  27. let sendMessage = document.querySelector('#sendMessage')
  28. let destroy = document.querySelector('#destroy')
  29. let myWebSocket,
  30. wsUrl = 'ws://127.0.0.1:1024/ws/?name=' + name
  31. eventBus.onEvent('changeBtnState', setButtonState)//设置按钮样式
  32. eventBus.onEvent('reconnect', reconnectWebSocket)//接收重连消息
  33. connect.addEventListener('click', reconnectWebSocket)
  34. sendMessage.addEventListener('click', function (e) {
  35. myWebSocket.sendMsg({
  36. ModeCode: "message",
  37. msg: 'hello'
  38. })
  39. })
  40. destroy.addEventListener('click', function (e) {
  41. myWebSocket.close()
  42. })
  43. function reconnectWebSocket () {
  44. if(!myWebSocket) {//第一次执行,初始化
  45. connectWebSocket()
  46. }
  47. if(myWebSocket && myWebSocket.reconnectTimer) {//防止多个websocket同时执行
  48. clearTimeout(myWebSocket.reconnectTimer)
  49. myWebSocket.reconnectTimer = null
  50. connectWebSocket()
  51. }
  52. }
  53. function connectWebSocket () {
  54. myWebSocket = new MyWebSocket(wsUrl);
  55. myWebSocket.init({//time:心跳时间间隔 timeout:心跳超时间隔 reconnect:断线重连时
  56. time: 30 * 1000,
  57. timeout: 3 * 1000,
  58. reconnect: 10 * 1000
  59. }, true)
  60. }
  61. /*
  62. * 设置按钮是否可点击
  63. * @param state:open表示开启状态,close表示关闭状态
  64. */
  65. function setButtonState (state) {
  66. switch(state) {
  67. case 'open':
  68. connect.disabled = true
  69. sendMessage.disabled = false
  70. destroy.disabled = false
  71. break;
  72. case 'close':
  73. connect.disabled = false
  74. sendMessage.disabled = true
  75. destroy.disabled = true
  76. break;
  77. }
  78. }
  79. </script>
  80. </body>
  81. </html>

最终实现的效果如下,即使后端服务关闭,或者是断网状态,客户端都能保持重连状态

最后,感谢你看到了这里,文章有任何问题欢迎大佬指出与讨论
附上源码:Gitee

 
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/寸_铁/article/detail/964132
推荐阅读
相关标签
  

闽ICP备14008679号