webSocket 开发

news2024/9/21 8:03:17

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.coloradmin.cn/o/902450.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

这些Linux基础命令你总得掌握吧

B站|公众号&#xff1a;啥都会一点的研究生 写在前面 很多深度学习/机器学习/数据分析等领域&#xff08;或者说大多数在Python环境下进行操作的领域&#xff09;的初学者入门时是在Windows上进行学习&#xff0c;也得益于如Anaconda等工具把环境管理做的如此友善 但如果想在…

阿里网盘海外使用速度很慢

小虎最近在HK使用阿里云盘&#xff0c;速度突然变得很慢&#xff0c;但是百度的没问题。查了发现是阿里的DNS做的不好&#xff0c;所以换了一个DNS速度就上来了。 解决方案 在这个网站&#xff1a;[原创工具] DNS优选(挑选最合适的DNS服务器,拒绝DNS劫下载DNS推荐工具&#x…

如何取消订阅IEEE membership的email

最近小虎开了一个IEEE Student Member&#xff0c;邮箱都快被IEEE给爆箱了。所以想办法取消订阅其邮件&#xff0c;但是保留其member身份。 方法 在profile界面选择communication preferences and policies, Uncheck所有communications&#xff0c;选择I only want to recei…

Flink内核源码解析--Flink中重要的工作组件和机制

Flink内核源码 1、掌握Flink应用程序抽象2、掌握Flink核心组件整体架构抽象3、掌握Flink Job三种运行模式4、理解Flink RPC网络通信框架Akka详解5、理解TaskManager为例子&#xff0c;分析Flink封装Akka Actor的方法和整个调用流程6、理解Flink高可用服务HighAvailabilityServ…

Spring Cloud Alibaba -微服务架构(二)

1. 微服务架构介绍 微服务架构&#xff0c; 简单的说就是将单体应用进一步拆分&#xff0c;拆分成更小的服务&#xff0c;每个服务都是一个可以独立运行的项目。 1.1 微服务架构的常见问题 一旦采用微服务系统架构&#xff0c;就势必会遇到这样几个问题&#xff1a; 这么多小…

最长回文子序列——力扣516

动态规划 int longestPalindromeSubseq(string s){int n=s.length();vector<vector<int>>

聊聊智慧城市的发展

目录 1.智慧城市应该是什么样子 2.智慧城市的实现方案 3.智慧城市会给人们造成的影响 1.智慧城市应该是什么样子 智慧城市是一种基于信息和通信技术的先进城市管理模式&#xff0c;旨在提高城市的运行效率、居民生活质量和可持续发展。智慧城市整合了各种智能设备、传感器、…

segment anything in high quality

致力于解决细节分割不好的情况&#xff0c;可以理解为sam的精细分割的微调版本&#xff0c;但是对原始的分割能力也没有丢失&#xff0c;有点像目标检测中的小目标检测优化算法。总的来说&#xff0c;在原始的sam上增加了hq-features和hq output token以及mlp&#xff0c;来做h…

【MySQL面试题(66道)】

文章目录 MySQL面试题(66道)基础1.什么是内连接、外连接、交叉连接、笛卡尔积呢&#xff1f;2.那 MySQL 的内连接、左连接、右连接有有什么区别&#xff1f;3.说一下数据库的三大范式&#xff1f;4.varchar 与 char 的区别&#xff1f;5.blob 和 text 有什么区别&#xff1f;6.…

【三次握手】TCP三次握手由入门到精通(完整版)

⬜⬜⬜ &#x1f430;&#x1f7e7;&#x1f7e8;&#x1f7e9;&#x1f7e6;&#x1f7ea;(*^▽^*)欢迎光临 &#x1f7e7;&#x1f7e8;&#x1f7e9;&#x1f7e6;&#x1f7ea;&#x1f430;⬜⬜⬜ ✏️write in front✏️ &#x1f4dd;个人主页&#xff1a;陈丹宇jmu &am…

EndNote(三)【阅读+批注、插入文献】

将文献导入EndNote了&#xff0c;右侧就能看当前文献的内容了,比如preview、pdf查看等&#xff1a; 当然&#xff0c;如果你觉得这样看有点不大气哈哈&#xff0c;你可以双击&#xff1a;&#xff08;这是第一种方法&#xff09; 他就会弹出一个窗口&#xff1a; &#xff08;这…

要跟静音开关说再见了!iPhone15新变革,Action按钮引领方向

有很多传言称iPhone 15 Pro会有很多变化&#xff0c;但其中一个变化可能意味着iPhone体验从第一天起就有的一项功能的终结。我说的是静音开关&#xff0c;它可以让你轻松地打开或关闭iPhone的铃声。 根据越来越多的传言&#xff0c;iPhone 15 Pro和iPhone 15 Pro Max将拆除静音…

C++------利用C++实现二叉搜索树【数据结构】

文章目录 二叉搜索树概念二叉搜索树的操作查找插入删除 二叉搜索树的应用 二叉搜索树 概念 什么是二叉搜索树&#xff0c;二叉搜索树就是指左孩子永远比根小右孩子永远比根大。这个规则适用于所有的子树。 上面的就是一棵二叉搜索树&#xff0c;我们还可以发现这棵树走一个中…

C语言之整数_数据存储篇(1)

目录 数据类型 整形家族 浮点型家族 构造类型 指针类型 空类型 整形在内存中的存储&#xff08;原反补&#xff09; NO1. NO2. NO3. NO4. NO5. NO6. 大端小端字节序 NO.1 NO.2 NO.3 NO.4 练习题 NO1. NO2. NO3. NO4. NO5. NO6. 总结 数据类型 …

Unity 之 变量修饰符public 与private 以及默认

文章目录 publicprivate默认情况的成员变量 public 当在Unity中使用public修饰符时&#xff0c;它将变量声明为公共变量&#xff0c;这意味着该变量可以在Unity编辑器中进行设置&#xff0c;并且可以从其他脚本中访问和修改。公共变量在Unity中广泛用于在脚本之间共享数据&…

4.Linux下Cmake交叉编译Qt项目到Jetson Orin Nano(arm)

由于3&#xff1a;Ubuntu上配置QT交叉编译环境并编译QT程序到Jetson Orin Nano&#xff08;ARM&#xff09;_月上林梢的博客-CSDN博客 这一篇文章只用手动配置&#xff0c;一直在点、点、点。比较 LOW&#xff0c;现在在Ubuntu上使用Cmake实现交叉编译QT程序到Jetson Orin Nano…

电脑技巧:电脑关机、休眠、睡眠之间如何选择,看完你就懂了

目录 一、关机、休眠、睡眠的区别&#xff1f; 1.关机 2.休眠 休眠的优点 休眠的缺点 3.睡眠 睡眠的优点 睡眠的缺点 二、什么时候关机/休眠/睡眠&#xff1f; 什么时候需要关机&#xff1f; 什么情况下使用休眠模式&#xff1f; 什么情况下使用睡眠模式&…

Linux之维护基本存储空间

目录 维护基本存储空间 1.查看磁盘信息&#xff08;块设备&#xff09;信息 2.创建分区 (1)MBR分区 标准MBR结构如下 为什么MBR最多只能有4个主分区 (2)GPT分区 优点 3.分区工具 1.使用fdisk管理MBR分区 语法格式 参数及作用 2.使用gdisk管理GPT分区 操作步骤 3.使用pa…

Java项目之基于ssm框架的社区生活超市管理系统(附源码)

基于ssm框架的社区生活超市管理系统设计与实现&#xff08;程序源码毕业论文&#xff09; 大家好&#xff0c;今天给大家介绍基于ssm框架的社区生活超市管理系统设计与实现&#xff0c;本论文只截取部分文章重点&#xff0c;文章末尾附有本毕业设计完整源码及论文的获取方式。更…

右值及右值引用

右值引用主要是为了优化。 在函数返回值没有打开-fno-elide-constructors时&#xff0c;函数返回值会调用拷贝构造函数 class X { public:X(){cout << "X ctor" << endl;}X(const X& x){cout << "X copy ctor" << endl;}~X()…