webSocket 开发

1 认识webSocket 

WebSocket_ohana!的博客-CSDN博客

一,什么是websocket

  • WebSocket是HTML5下一种新的协议(websocket协议本质上是一个基于tcp的协议)
  • 它实现了浏览器与服务器全双工通信,能更好的节省服务器资源和带宽并达到实时通讯的目的
  • Websocket是一个持久化的协议

WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输

2 技术选型 为什么选择webSocket

WebSocket有以下特点:

  •  是真正的全双工方式,建立连接后客户端与服务器端是完全平等的,可以互相主动请求。而HTTP长连接基于HTTP,是传统的客户端对服务器发起请求的模式。
  •  HTTP长连接中,每次数据交换除了真正的数据部分外,服务器和客户端还要大量交换HTTP header,信息交换效率很低。Websocket协议通过第一个request建立了TCP连接之后,之后交换的数据都不需要发送 HTTP header就能交换数据,这显然和原有的HTTP协议有区别所以它需要对服务器和客户端都进行升级才能实现(主流浏览器都已支持HTML5)

 

3 WebSocket  Demo

前端服务:

npm install websocket // "websocket": "^1.0.32",

新建 websocket.js 文件

node 安装:

npm install ws //     "ws": "^7.3.0",

vue项目中使用WebSocket_vue websocket服务端_weixin_43964779的博客-CSDN博客

开发者API

WebSocket() - Web API 接口参考 | MDN

client:

  <script>
    var app = new Vue({
      el: '#app',
      data: {
        message: '',
        lists: [],
        ws: {},
        name: '',
        isShow: true,
        num: 0,
        roomid: '',
        uid: '',
        handle: {}
      },
      mounted() {
      },
      methods: {
        init() {
          this.ws = new WebSocket('ws://127.0.0.1:3000')
          this.ws.onopen = this.onOpen
          this.ws.onmessage = this.onMessage
          this.ws.onclose = this.onClose
          this.ws.onerror = this.onError
        },
        enter() {
          if (this.name.trim() === '') {
            alert('用户名不得为空')
            return
          }
          this.init()
          this.isShow = false
        },
        onOpen: function () {
          // console.log('open:' + this.ws.readyState);
          //ws.send('Hello fro,m client!')
          // 发起鉴权请求
          //this.ws.send(JSON.stringify({
          //  event: 'auth',
          //  message: //'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIx//MjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNT//E2MjM5MDIyfQ.//XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o'
          //}))
          this.ws.send(JSON.stringify({
            event: 'enter',
            message: this.name,
            roomid: this.roomid,
            uid: this.uid
          }))
        },
        onMessage: function (event) {
          // 当用户未进入聊天室,则不接收消息
          if (this.isShow) {
            return
          }
          // 接收服务端发送过来的消息
          var obj = JSON.parse(event.data)
          switch (obj.event) {
            case 'noauth':
              // 鉴权失败
              // 路由跳转到 /login 重新获取token
              break;
            case 'enter':
              // 当有一个新的用户进入聊天室
              this.lists.push('欢迎:' + obj.message + '加入聊天室!')
              break;
            case 'out':
              this.lists.push(obj.name + '已经退出了聊天室!')
              break;
            case 'heartbeat':
              //this.checkServer() // timeInterval + t
              // 可以注释掉以下心跳状态,主动测试服务端是否会断开客户端的连接
              this.ws.send(JSON.stringify({
                event: 'heartbeat',
                message: 'pong'
              }))
              break
            default:
              if (obj.name !== this.name) {
                // 接收正常的聊天
                this.lists.push(obj.name + ':' + obj.message)
              }
          }
          this.num = obj.num
        },
        onClose: function () {
          // 当链接主动断开的时候触发close事件
          console.log('close:' + this.ws.readyState);
          console.log('已关闭websocket');
          this.ws.close()
        },
        onError: function () {
          // 当连接失败时,触发error事件
          console.log('error:' + this.ws.readyState);
          console.log('websocket连接失败!');
          // 连接失败之后,1s进行断线重连!
          var _this = this
          setTimeout(function () {
            _this.init()
          }, 1000)
        },
        // 发送消息
        send: function () {
          this.lists.push(this.name + ':' + this.message)
          this.ws.send(JSON.stringify({
            event: 'message',
            message: this.message,
            name: this.name
          }))
          this.message = ''
        },
        checkServer: function () {
          var _this = this
          clearTimeout(this.handle)
          this.handle = setTimeout(function () {
            _this.onClose()
            setTimeout(function () {
              _this.init()
            }, 1000)
            // 设置1ms的时延,调试在服务器测未及时响应时,客户端的反应
          }, 30000 + 1000)
        }
      }
    })
  </script>



Server:

package.json:

{
  "name": "server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "nodemon src/index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bluebird": "^3.7.2",
    "jsonwebtoken": "^8.5.1",
    "redis": "^2.8.0",
    "ws": "^7.2.1"
  },
  "devDependencies": {
    "nodemon": "^2.0.2"
  }
}

index.js:

const WebSocket = require('ws')
const wss = new WebSocket.Server({ port: 3000 })
// const jwt = require('jsonwebtoken')
const {getValue, setValue, existKey} = require('./config/RedisConfig')

const timeInterval = 30000
// 多聊天室的功能
// roomid -> 对应相同的roomid进行广播消息
let group = {}

// const run = async () =>{
//   setValue('imooc', 'hello')
//   const result = await getValue('imooc')
//   console.log('TCL: run -> result', result)
// }

// run()
const prefix = 'imooc-'
wss.on('connection', function connection (ws) {
  // 初始的心跳连接状态
  ws.isAlive = true

  console.log('one client is connected');
  // 接收客户端的消息
  ws.on('message', async function(msg) {
    const msgObj = JSON.parse(msg)
    const roomid = prefix + (msgObj.roomid ? msgObj.roomid : ws.roomid)
    if (msgObj.event === 'enter') {
      // 当用户进入之后,需要判断用户的房间是否存在
      // 如果用户的房间不存在,则在redis中创建房间号,用户保存用户信息
      // 主要是用于统计房间里的人数,用于后面进行消息发送
      ws.name = msgObj.message
      ws.roomid = msgObj.roomid
      ws.uid = msgObj.uid
      console.log('TCL: connection -> ws.uid', ws.uid)
      // 判断redis中是否有对应的roomid的键值
      const result = await existKey(roomid)
      if (result === 0) {
        // 初始化一个房间数据
        setValue(roomid, ws.uid)
      } else {
        // 已经存在该房间缓存数据
        const arrStr = await getValue(roomid)
        let arr = arrStr.split(',')
        if (arr.indexOf(ws.uid) === -1) {
          setValue(roomid, arrStr + ',' + ws.uid)
        }
      }
      if (typeof group[ws.roomid] === 'undefined') {
        group[ws.roomid] = 1
      } else {
        group[ws.roomid] ++
      }
    }
    // // 鉴权
    // if (msgObj.event === 'auth') {
    //   jwt.verify(msgObj.message, 'secret', (err, decode) => {
    //     if (err) {
    //       // websocket返回前台鉴权失败消息
    //       ws.send(JSON.stringify({
    //         event: 'noauth',
    //         message: 'please auth again'
    //       })) 
    //       console.log('auth error');
    //       return
    //     } else {
    //       // 鉴权通过
    //       console.log(decode);
    //       ws.isAuth = true
    //       return 
    //     }
    //   })
    //   return
    // }
    // // 拦截非鉴权的请求
    // if (!ws.isAuth) {
    //   return
    // }
    // 心跳检测
    if (msgObj.event === 'heartbeat' && msgObj.message === 'pong') {
      ws.isAlive = true
      return
    }

    // 广播消息
    // 获取房间里所有的用户信息
    const arrStr = await getValue(roomid)
    let users = arrStr.split(',')
    wss.clients.forEach(async (client) => {
      // 判断非自己的客户端
      if (client.readyState === WebSocket.OPEN && client.roomid === ws.roomid) {
        msgObj.name = ws.name
        msgObj.num = group[ws.roomid]
        client.send(JSON.stringify(msgObj))
        // 排队已经发送了消息了客户端 -> 在线
        if (users.indexOf(client.uid) !== -1) {
          users.splice(users.indexOf(client.uid), 1)
        }
        // 消息缓存信息:取redis中的uid数据
        let result = await existKey(ws.uid)
        if (result !== 0) {
          // 存在未发送的离线消息数据
          let tmpArr = await getValue(ws.uid)
          let tmpObj = JSON.parse(tmpArr)
          let uid = ws.uid
          if (tmpObj.length > 0) {
            let i = []
            // 遍历该用户的离线缓存数据
            // 判断用户的房间id是否与当前一致
            tmpObj.forEach((item) => {
              if (item.roomid === client.roomid && uid === client.uid) {
                client.send(JSON.stringify(item))
                i.push(item)
              }
            })
            // 删除已经发送的缓存消息数据
            if (i.length > 0) {
              i.forEach((item) => {
                tmpObj.splice(item, 1)
              })
            }
            setValue(ws.uid, JSON.stringify(tmpObj))
          }
        }
      }
    })

    // 断开了与服务端连接的用户的id,并且其他的客户端发送了消息
    if (users.length> 0 && msgObj.event === 'message') {
      users.forEach(async function(item) {
        const result = await existKey(item)
        if (result !== 0) {
          // 说明已经存在其他房间该用户的离线消息数据
          let userData = await getValue(item)
          let msgs = JSON.parse(userData)
          msgs.push({
            roomid: ws.roomid,
            ...msgObj
          })
          setValue(item, JSON.stringify(msgs))
        } else {
          // 说明先前这个用户一直在线,并且无离线消息数据
          setValue(item, JSON.stringify([{
            roomid: ws.roomid,
            ...msgObj
          }]))
        }
      })
    }
  })

  // 当ws客户端断开链接的时候
  ws.on('close', function() {
    if (ws.name) {
      group[ws.roomid] --
    }
    let msgObj = {}
    // 广播消息
    wss.clients.forEach((client) => {
      // 判断非自己的客户端
      if (client.readyState === WebSocket.OPEN && ws.roomid === client.roomid) {
        msgObj.name = ws.name
        msgObj.num = group[ws.roomid]
        msgObj.event = 'out'
        client.send(JSON.stringify(msgObj))
      }
    })
  })
})

// setInterval(()=> {
//   wss.clients.forEach((ws) => {
//     if (!ws.isAlive && ws.roomid) {
//       group[ws.roomid] --  
//       delete ws['roomid']
//       return ws.terminate()
//     }
//     // 主动发送心跳检测请求
//     // 当客户端返回了消息之后,主动设置flag为在线
//     ws.isAlive = false
//     ws.send(JSON.stringify({
//       event: 'heartbeat',
//       message: 'ping',
//       num: group[ws.roomid]
//     }))
//   })
// }, timeInterval)

4 心跳检测&断线重连

服务器先发->客户端->服务器 ∞  

心跳检测:

服务ping->客户端   服务器端有定时器 如果没有收到 会在下次遍历中关闭与该客户的服务 ws.terminate() //终止发送  退出了本次连接

客户段Clinet:

客户段在定时器中加入 断开重连的代码,在下次服务器发送过来的PIng  的代码中 清除掉上次的定时器,同时就清除了上次心跳检查的断开代码,然后发送pong->服务器,服务器收到后继续大发送ping->客户端,当本次请求一直未收到ping时  心跳检查的定时器没有被清除 ,就会执行close方法,关闭本次连接,并重新Init新的链接,这就是断线重连。

服务器端: 定时器

setInterval(()=> {  //定时器
  wss.clients.forEach((ws) => {
    if (!ws.isAlive && ws.roomid) {  //客户段终止
      group[ws.roomid] --  
      delete ws['roomid']
      return ws.terminate() //终止发送  退出了本次连接

    }
    // 主动发送心跳检测请求
    // 当客户端返回了消息之后,主动设置flag为在线
    ws.isAlive = false
    ws.send(JSON.stringify({
      event: 'heartbeat',
      message: 'ping',
      num: group[ws.roomid]
    }))
  })
}, timeInterval)

客户端 心跳检查与 断线重连:




   //心跳检查
   
  case 'heartbeat':
              //this.checkServer() // timeInterval + t   如果一直接收到ping   那么这次的请求就会删除上次的定时器  定时器不会被执行
              // 可以注释掉以下心跳状态,主动测试服务端是否会断开客户端的连接
              this.ws.send(JSON.stringify({
                event: 'heartbeat',
                message: 'pong'
              }))
              break




     //检查心跳 
        checkServer: function () {
          var _this = this
          clearTimeout(this.handle)  //清除计时器
          this.handle = setTimeout(function () {
            _this.onClose()
            setTimeout(function () {
              _this.init()
            }, 1000)
            // 设置1ms的时延,调试在服务器测未及时响应时,客户端的反应
          }, 30000 + 1000)

        }

springboot整合webSocket(看完即入门)_springboot websocket_hmb↑的博客-CSDN博客

webSocket前端开发实现+心跳检测机制_前端心跳检测_mayuan2011的博客-CSDN博客

为什么要使用webSocket以及心跳检测机制

在使用webSocket的过程中,如果遇到网络断开,服务端并没有触发onclose事件,就会出现此状况:服务端会继续向客户端发送多余的连接,并且这些数据会丢失。
因此就需要一种机制来检测客户端和服务端是否处于正常的连接状态,因此就有了webSocket的心跳检测机制,即如果有心跳则说客户端和服务端的连接还存在,无心跳相应则说明链接已经断开,需要采取重新连接等措施。

5 总结 

WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。


 


 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/82265.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【机器学习】— 2 图神经网络GNN

一、说明 在本文中&#xff0c;我们探讨了图神经网络&#xff08;GNN&#xff09;在推荐系统中的潜力&#xff0c;强调了它们相对于传统矩阵完成方法的优势。GNN为利用图论来改进推荐系统提供了一个强大的框架。在本文中&#xff0c;我们将在推荐系统的背景下概述图论和图神经网…

回归预测 | MATLAB实现PSO-RBF粒子群优化算法优化径向基函数神经网络多输入单输出回归预测(多指标,多图)

回归预测 | MATLAB实现PSO-RBF粒子群优化算法优化径向基函数神经网络多输入单输出回归预测&#xff08;多指标&#xff0c;多图&#xff09; 目录 回归预测 | MATLAB实现PSO-RBF粒子群优化算法优化径向基函数神经网络多输入单输出回归预测&#xff08;多指标&#xff0c;多图&a…

matlab使用教程(19)—曲线拟合与一元方程求根

1.多项式曲线拟合 此示例说明如何使用 polyfit 函数将多项式曲线与一组数据点拟合。您可以按照以下语法&#xff0c;使用 polyfit 求出以最小二乘方式与一组数据拟合的多项式的系数 p polyfit(x,y,n), 其中&#xff1a; • x 和 y 是包含数据点的 x 和 y 坐标的向量 …

深入理解SSO原理,项目实践使用一个优秀开源单点登录项目(附源码)

深入理解SSO原理,项目实践使用一个优秀开源单点登录项目(附源码)。 一、简介 单点登录(Single Sign On),简称为 SSO。 它的解释是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。 ❝ 所谓一次登录,处处登录。同样一处退出,处处退出。 ❞ 二…

Axios使用CancelToken取消重复请求

处理重复请求&#xff1a;没有响应完成的请求&#xff0c;再去请求一个相同的请求&#xff0c;会把之前的请求取消掉 新增一个cancelRequest.js文件 import axios from "axios" const cancelTokens {}export const addPending (config) > {const requestKey …

时序预测 | MATLAB实现基于CNN-BiGRU卷积双向门控循环单元的时间序列预测-递归预测未来(多指标评价)

时序预测 | MATLAB实现基于CNN-BiGRU卷积双向门控循环单元的时间序列预测-递归预测未来(多指标评价) 目录 时序预测 | MATLAB实现基于CNN-BiGRU卷积双向门控循环单元的时间序列预测-递归预测未来(多指标评价)预测结果基本介绍程序设计参考资料 预测结果 基本介绍 MATLAB实现基于…

Vs code 使用中的小问题

1.Java在Vs code 中使用单元测试失败或者如何使用单元测试 创建Java项目&#xff0c;或者将要测试的文件夹添加进工作区 要出现lib包&#xff0c;并有两个测试用的jar包 编写测试文件 public class TestUnit{ public static void main(String[] args) {String str "…

Pycharm与Anaconda Python的开发环境搭建

目录 一&#xff1a;下载 二&#xff1a;安装python 三&#xff1a;设置Pycharm 一&#xff1a;下载 下载Anaconda&#xff1a; Anaconda | The World’s Most Popular Data Science Platform 安装好以后&#xff0c;设置一下环境变量&#xff1a; 打开命令行&#xff0c…

OpenCV-Python中的图像处理-图像特征

OpenCV-Python中的图像处理-图像特征 图像特征Harris角点检测亚像素级精度的角点检测Shi-Tomasi角点检测SIFT(Scale-Invariant Feature Transfrom)SURF(Speeded-Up Robust Features)FAST算法BRIEF(Binary Robust Independent Elementary Features)算法ORB (Oriented FAST and R…

aardio简单网站css或js下载练习

import win.ui; /*DSG{{*/ var winform win.form(text"下载网站css或js";right664;bottom290;maxfalse) winform.add( buttonClose{cls"button";text"退出";left348;top204;right498;bottom262;color14120960;fontLOGFONT(h-14);note" &qu…

SQL Injection

SQL Injection 就是通过把恶意的sql命令插入web表单递交给服务器&#xff0c;或者输入域名或页面请求的查询字符串递交到服务器&#xff0c;达到欺骗服务器&#xff0c;让服务器执行这些恶意的sql命令&#xff0c;从而让攻击者&#xff0c;可以绕过一些机制&#xff0c;达到直…

一种新型的4H-SiC超结共模场效应晶体管(UMOSFET),具有异质结二极管,以提高反向恢复特性

标题&#xff1a;A novel 4H-SiC super junction UMOSFET with heterojunction diode for enhanced reverse recovery characteristics 摘要 摘要—本文提出并通过数值模拟研究了一种新型的碳化硅&#xff08;SiC&#xff09;超结共模场效应晶体管&#xff08;UMOSFET&#xf…

利用POM完成脚本分离实现企业级自动化(POM设计模式+页面的框架封装+测试报告截图)

利用POM完成脚本分离实现企业级自动化&#xff08;POM设计模式页面的框架封装测试报告截图&#xff09; 项目-测试-手工测试 项目-测试-手工测试 1.了解需求&#xff1b; 2.编写测试用例&#xff08;开始&#xff09;——功能测试组会去做的事情 3.执行测试用例——发送测试报…

Kotlin 使用 View Binding

解决的问题&#xff1a; 《第一行代码——Android》第三版 郭霖 P277 视图绑定的问题 描述&#xff1a; kotlin-android-extensions 插件已经弃用 butter knife 已经弃用 解决办法 推荐使用 View Binding 来代替 findViewById 使用方法 1、配置 build.gradle 2、在act…

服务器数据恢复-HP EVA存储常见故障的数据恢复流程

EVA存储原理&#xff1a; EVA系列存储是以虚拟化存储为实现目的的中高端存储设备&#xff0c;内部的结构组成完全不同于其他的存储设备&#xff0c;RAID在EVA内部称之为VRAID。 EVA会在每个物理磁盘&#xff08;PV&#xff09;的0扇区写入签名&#xff0c;签名后PV会被分配到不…

协程框架NtyCo的实现

一、为什么需要协程&#xff1f; 讨论协程之前&#xff0c;我们需要先了解同步和异步。以epoll多路复用器为例子&#xff0c;其主循环框架如下&#xff1a; while (1){int nready epoll_wait(epfd, events, EVENT_SIZE, -1);int i0;for (i0; i<nready; i){int sockfd ev…

Maven高级

目录 一、分模块开发与设计 1. 分模块开发的意义 2. 分模块开发&#xff08;模块拆分&#xff09; &#xff08;1&#xff09;创建Maven模块 &#xff08;2&#xff09;书写模块代码 &#xff08;3&#xff09;通过maven指令安装模块到本地仓库&#xff08;install指令&…

[JavaWeb]【四】web后端开发-SpringBootWeb入门

目录 一 Spring 二 SpringBootWeb入门 2.1 入门需求 2.2 分析 2.3 开始创建SpringBootWeb 2.4 创建类实现需求 2.5 启动程序 2.6 访问 三 HTTP协议 3.1 HTTP-概述 3.2 HTTP-请求协议 3.3 HTTP-响应协议 3.3.1 响应状态码 && 响应类型 3.4 HTTP-协议解析 前言…

孤注一掷——基于文心Ernie-3.0大模型的影评情感分析

孤注一掷——基于文心Ernie-3.0大模型的影评情感分析 文章目录 孤注一掷——基于文心Ernie-3.0大模型的影评情感分析写在前面一、数据直观可视化1.1 各评价所占人数1.2 词云可视化 二、数据处理2.1 清洗数据2.2 划分数据集2.3 加载数据2.4 展示数据 三、RNIE 3.0文心大模型3.1 …

前端基础(Vue的模块化开发)

目录 前言 响应式基础 ref reactive 学习成果展示 Vue项目搭建 总结 前言 前面学习了前端HMTL、CSS样式、JavaScript以及Vue框架的简单适用&#xff0c;接下来运用前面的基础继续学习Vue&#xff0c;运用前端模块化编程的思想。 响应式基础 ref reactive 关于ref和react…