webSocket 笔记

news2024/12/23 17:31:09

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/887308.html

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

相关文章

全网首发 | 科学计算与系统建模仿真系列课程上线啦!

当前&#xff0c;信息物理融合系统&#xff08;CPS&#xff09;、基于模型的系统工程&#xff08;MBSE&#xff09;、数字孪生、数字化工程等新型技术快速发展&#xff0c;推动装备研制从信息化时代步入到数字化时代&#xff0c;并且呈现数字化与智能化相融合的新时代特点。MWO…

Flink的常用算子以及实例

1.map 特性&#xff1a;接收一个数据&#xff0c;经过处理之后&#xff0c;就返回一个数据 1.1. 源码分析 我们来看看map的源码 map需要接收一个MapFunction<T,R>的对象&#xff0c;其中泛型T表示传入的数据类型&#xff0c;R表示经过处理之后输出的数据类型我们继续往…

vue2+Spring Boot2.7 大文件分片上传

之前我们文章 手把手带大家实现 vue2Spring Boot2.7 文件上传功能 将了上传文件 但如果文件很大 就不太好处理了 按正常情况甚至因为超量而报错 这里 我弄了个足够大的文件 我们先搭建 Spring Boot2.7 环境 首先 application.yml 代码编写如下 server:port: 80 upload:path:…

机器学习、cv、nlp的一些前置知识

为节省篇幅&#xff0c;不标注文章来源和文章的问题场景。大部分是我的通俗理解。 文章目录 向量关于向量的偏导数&#xff1a;雅可比矩阵二阶导数矩阵&#xff1a;海森矩阵随机变量随机场伽马函数beta分布数学术语坐标上升法协方差训练集&#xff0c;验证集&#xff0c;测试集…

2023年7月京东冰箱行业品牌销售排行榜(京东运营数据分析)

作为日常使用的大家电之一&#xff0c;如今我国冰箱产业已渐趋饱满&#xff0c;市场增长有限。今年上半年&#xff0c;冰箱市场整体销额同比去年来看勉强保持小幅增长。不过&#xff0c;7月份&#xff0c;冰箱大盘的销售表现就略显萧条了。 根据鲸参谋电商数据分析平台的相关数…

css3-grid:grid 布局 / 基础使用

一、理解 grid 二、理解 css grid 布局 CSS Grid布局是一个二维的布局系统&#xff0c;它允许我们通过定义网格和网格中每个元素的位置和尺寸来进行页面布局。CSS Grid是一个非常强大的布局系统&#xff0c;它不仅可以用于构建网格布局&#xff0c;还可以用于定位元素&#xf…

cpu和io的关系

在说io的五中模型之前,先说说Io把文件从哪里移到了哪里 自己的理解: 根据工作或者遇到的业务. 文件不可能存在缓存或在内存中,因为缓存和内存不能永久性储存东西, 文件需要被永久性储存.因此文件都存在电脑的硬盘里, 或者存在云服务器的它们的硬盘里. 我们io文件, 第一…

实现语音识别系统:手把手教你使用STM32C8T6和LD3320(SPI通信版)实现语音识别

本文实际是对LD3320&#xff08;SPI通信版&#xff09;的个人理解&#xff0c;如果单论代码和开发板的资料而言&#xff0c;其实当你购买LD3320的时候&#xff0c;卖家已然提供了很多资料。我在大学期间曾经多次使用LD3320芯片的开发板用于设计系统&#xff0c;我在我的毕业设计…

系统学习Linux-Mariadb高可用MHA

概念 MHA&#xff08;MasterHigh Availability&#xff09;是一套优秀的MySQL高可用环境下故障切换和主从复制的软件。 MHA 的出现就是解决MySQL 单点的问题。 MySQL故障切换过程中&#xff0c;MHA能做到0-30秒内自动完成故障切换操作。 MHA能在故障切换的过程中最大程度上…

IDEA 找不到项目 ‘org.springframework.boot:spring-boot-starter-parent:3.1.2‘

找不到项目 ‘org.springframework.boot:spring-boot-starter-parent:2.6.7’ 这个问题主要是因为ide的缓存导致的&#xff0c;我们直接清理缓存并重启ide 重启之后ide会对pom文件进行编排索引完成之后问题就没有了

去掉鼠标系列之一: 语雀快捷键使用指南

其实应该是系列之二了&#xff0c;因为前面写了一个关于Interlij IDEA的快捷键了。 为什么要写这个了&#xff0c;主要是觉得一会儿用鼠标&#xff0c;一会儿键盘&#xff0c;一点儿不酷&#xff0c;我希望可以一直用键盘&#xff0c;抛开鼠标。后面陆续记录一下各个软件的快捷…

STM32CubeMx之esp8266的at指令使用

AT //返回ok则为正常 ATCWMODE1//设置为设备模式 ATCWLAP//搜索附近可用wifi ATCWJAP"CMCC-5-7","chb513029"//连接热点 ATCIPMUX0//设置wifi为单连接 ATCIPSTART"TCP","192.168.37.1",1001//连接tcp 这里遇到了重重问题 这里我…

VScode搭建Opencv(C++开发环境)

VScode配置Opencv 一、 软件版本二 、下载软件2.1 MinGw下载2.2 Cmake下载2.3 Opencv下载 三、编译3.1 cmake-gui3.2 make3.3 install 四、 VScode配置4.1 launch.json4.2 c_cpp_properties.json4.3 tasks.json 五、测试 一、 软件版本 cmake :cmake-3.27.2-windows-x86_64 Mi…

Go framework-Beego

一、Beego Beego用于在Go中快速开发企业应用程序&#xff0c;包括RESTful API、web应用程序和后端服务。 Beego 源码地址 Beego 官方站点 Beego 官方说明 Beego的特性 RESTful支持MVC架构模块化自动API文档注释路由命名空间开发工具集合Full stack for Web & API Bee…

iTOP-2K1000开发板固态硬盘分区

固态硬盘分区的目的是把固态硬盘分成一个分区并格式化为 ext2&#xff0c;在 linux 系统下&#xff0c;分区主要使用的命令是 fdisk 命令。接下来我们一起看下分区操作。 U 盘启动成功以后进到文件系统&#xff0c;输入命令 fdisk -l 查看当前开发板的固态硬盘的节点&#xff…

易服客工作室:UberMenu WordPress插件 - 网站超级菜单插件

UberMenu WordPress插件是一个用户友好、高度可定制、响应迅速的 Mega Menu WordPress 插件。它与 WordPress 3 菜单系统一起开箱即用&#xff0c;易于上手&#xff0c;但功能强大&#xff0c;足以创建高度定制化和创意的大型菜单配置。 网址: UberMenu WordPress插件 - 网站超…

RS485、MODBUS通信协议详解

前言 MODBUS协议是Modicon公司发表的一种串行通信协议&#xff0c;属于OSI模型中应用层的协议&#xff0c;现广泛应用于工业控制领域&#xff0c;它的主要特点是免费开放、支持多种电气接口&#xff08;如RS-232、RS-485&#xff09;&#xff0c;传输介质可以是双绞线、光纤、无…

java-JVM内存区域JVM运行时内存

一. JVM 内存区域 JVM 内存区域主要分为线程私有区域【程序计数器、虚拟机栈、本地方法区】、线程共享区域【JAVA 堆、方法区】、直接内存。线程私有数据区域生命周期与线程相同, 依赖用户线程的启动/结束 而 创建/销毁(在 HotspotVM 内, 每个线程都与操作系统的本地线程直接映…

Beats:安装及配置 Metricbeat (一)- 8.x

在我之前的文章&#xff1a; Beats&#xff1a;Beats 入门教程 &#xff08;一&#xff09;Beats&#xff1a;Beats 入门教程 &#xff08;二&#xff09; 我详细描述了如何在 Elastic Stack 7.x 安装及配置 Beats。在那里的安装&#xff0c;它通常不带有安全及 Elasticsearc…

机器视觉基础实验-Panorama Stitching

文章目录 1、实验内容2、实验设计&#xff08;略&#xff09;3、实验环境及实验数据集四、实验过程及结果4.1 Harris角点检测器寻找关键点4.2 构建描述算子来描述图中的每个关键点&#xff0c;比较两幅图像的两组描述子&#xff0c;并进行匹配。4.3 根据一组匹配关键点&#xf…