websocket简介及上手

1.websocket初识:

WebSocket 是 HTML5 提供的一种全双工通讯的协议,类似于http,同样建立在TCP上的传输协议,被称为ws,加密传输称为wss。

WebSocket 使得客户端和服务器之间的数据交换变得简单些,服务端可以主动向客户端推送数据,而在传统的http协议中服务端是不能主动推送数据给客户端的,可以保证数据的实时性。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。

在这里插入图片描述
2.websocket优势:

传统的http协议如果想要做数据及时更新应用,那么只能使用轮询的方式不断的发起请求得到数据,而这个发起请求得到数据的过程会浪费很多带宽资源,而且服务端不能主动传数据给客户端,这是因为http协议中,请求是无状态的,即一次发起请求到结束响应后就会和服务端断开连接,每次请求中请求头和响应头都带有很多无用数据,那么这将大大浪费带宽及服务器压力,而websocket只在建立连接的时候会携带不常用信息,之后建立长连接后就可以保持有状态,此时可以由服务器主动推送消息等,现在很多实时更新数据的应用都是利用websocket协议开发的,例如聊天室等应由。

3.前端项目中使用WebSocket:

封装WebSocket类:

前端项目(vue)中使用websocket,window对象提供了WebSocket对象,使用时创建一个对象即可,为了方便起见,将这一部分装为一个类,在vue项目main.js中创建一个实例即可,具体分装如下:

export default class WebSocketClient {
  // 单例模式创建websocket客户端:
  static instance = null

  // 方法前面加get,调用时不用加小括号,判断此类是否被创建了对象,如果创建了的话就直接return
  static get Instance () {
    if (!this.instance) {
      this.instance = new WebSocketClient()
    }
    return this.instance
  }

  // ws为websocket实例对象,下面会赋WebSocket对象寄存器地址给ws
  ws = null

  // 定义一个是否连接成功的标识,来解决当服务没有连接就请求数据报错的bug
  connected = false

  // 记录重连ws服务器的次数(可用于延迟发送请求和延迟掉线重连服务):
  reConnectCount = 0

  //  临时回调函数容器,存储回调函数用来接收数据:以socket匹配名称为key,函数寄存器地址为value
  callBackMap = {}

  // 连接服务器:
  // 掉线重新连接定时器id
  timeId = null
  connect () {
    if (!window.WebSocket) {
      return console.log('浏览器不支持WebSocket')
    }
    // 创建连接,先判断当前浏览器是否支持WebSocket,支持的话创建websocket客户端并将寄存器地址赋值给ws变量
    if ('WebSocket' in window) {
      // 创建websocket客户端对象,并连接到指定服务器:(后端项目所在服务器ip才可以访问到,否则连接失败!)
      this.ws = new WebSocket('ws://127.0.0.1:3001')
      //可以直接绑定域名
      // this.ws = new WebSocket('ws://******.com:3001')
    } else {
      alert('您的浏览器不支持Websocket通信协议,请使用Chrome或者Firefox浏览器!')
    }

    // 监听客户端与服务端连接成功:
    this.ws.onopen = () => {
      // 将是否连接的状态修改为已经连接成功
      this.connected = true
      // 并将掉线重连次数重新赋值为0
      this.reConnectCount = 0
      // 连接成功后将掉线重连定时器停止:
      clearInterval(this.timeId)
      console.log('连接websocket服务器成功!')
    }

    // 监听服务器主动断开或连接失败:
    this.ws.onclose = () => {
      // 当服务器主动断开或连接失败时,将是否连接状态修改为未连接,并将掉线重连次数累加
      this.connected = false
      this.reConnectCount++
      console.log('服务器主动断开了连接')
      // 当监听到与服务端断开后,每隔一段时间调用自己connect尝试重新连接服务器,当连接成功后在监听连接成功的事件中停止定时器
      this.timeId = setTimeout(() => {
        this.connect()
      }, this.reConnectCount * 1000)
    }

    // 监听服务器端发送数据过来:
    this.ws.onmessage = response => {
      const responseData = JSON.parse(response.data)
     // 拿到后端返回数据中的websocket标识符api的值,调用回调函数将数据传给回调函数
      const api = responseData.api
      if (this.callBackMap[api]) {
        this.callBackMap[api].call(this, responseData)
      }
    }
  }
  // 请求的回调函数

  addCallBack (socketType, callBakc) {
    this.callBackMap[socketType] = callBakc
  }
  // 移出存储回调函数中的某个函数:

  removeCallBack (socketType) {
    console.log('移出回调函数')
    this.callBackMap[socketType] = null
  }

  // 定义发送请求数据的方法,request为请求对象格式:{api:'要匹配的接口', body:{请求体对象}}
  send (request) {
    // 因为我的接口中需要token验证,所以我们将每次请求都加上token值
    const token = window.sessionStorage.getItem('token')
    const obj = { token: token, ...request }
    // 当ws服务连接成功后才可以发送请求
    if (this.connected) {
      this.reConnectCount = 0
      this.ws.send(JSON.stringify(obj))
    } else {
      this.reConnectCount++
      // 否则延迟this.reConnectCount * 500ms再次尝试请求数据,这里不写死时间是因为当多次连接是每次将时间拉长可以减少服务压力
      setTimeout(() => {
        this.ws.send(JSON.stringify(obj))
      }, this.reConnectCount * 1000)
    }
  }
}

main.js中创建WebSocket实例并挂载到vue全局:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
// 导入分装的websocket类:
import websocket from './websocket'
import '@/assets/iconfont/iconfont.css'
import './assets/css/global.less'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import axios from 'axios'
axios.interceptors.request.use(config => {
  config.headers.authorization = window.sessionStorage.getItem('token')
  return config
})
Vue.use(ElementUI)
Vue.prototype.$http = axios
// 通过单例模式连接ws服务器:
websocket.Instance.connect()
// 将websocket挂载到vue原型上:
Vue.prototype.$ws = websocket.Instance

Vue.config.productionTip = false
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')

注意: 在vue.config.js中升级ws协议,不升级的话上线后无法连接服务端ws服务,如:

const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  devServer: {
    // 升级ws协议,不配置的话连接不上:
    host: 'localhost',
    port:3001,
    client: {
      webSocketURL: 'ws://0.0.0.0:3001/',
    },
    headers: {
      'Access-Control-Allow-Origin': '*',
    },
    // 配置后端项目反向代理服务器:
    proxy: {
      '/api': {
        target: 'http://127.0.0.1:3000/', // 对应自己的接口地址
        changeOrigin: true,
        ws: true,
        pathRewrite: {
          '/api': '/api'
        }
      },
      '/spublic': {
        target: 'http://127.0.0.1:3000/', // 对应自己的静态图片地址
        changeOrigin: true,
        ws: true,
        pathRewrite: {
          '/spublic': '/spublic'
        }
      }
    }
  }
})

vue组件中使用websocket请求数据:

<script>
export default {
  mounted () {
    // 组件创建或挂载时调用addCallBack方法注册回调,将第一个参数作为key 参数二为回调函数寄存器地址存到WebSocket中callBackMap对象中,当服务端有数据发送过来其中api对应的值和注册时key值相同是调用回调函数接收数据,addCallBack('devicedata', this.sendgetdatamethods)中devicedata是每个请求的唯一标识,可以理解为api匹配url,如:'/api/login'
    this.$ws.addCallBack('devicedata', this.sendgetdatamethods)
  },
  destroyed () {
    // 组件销毁时将注册的回调函数销毁掉:
    this.$ws.removeCallBack('devicedata')
  },
  methods: {
    // 接收数据的回调函数,参数data是服务端返回的数据,服务器发送数据过来,如果唯一标识匹配注册时的唯一标识(devicedata)就会触发调用此回调函数
    sendgetdatamethods (data) {
      console.log(data)
    },
    // 发送数据给服务端获取数据,如果后端支持的话,可以自动推送数据:
    sendMsgHandle () {
      const obj = { api: 'devicedata', body: {id: 1} }
      this.$ws.send(obj)
    }
  }
}
</script>

4.后端node.js项目中使用websocket:

websocket配置: node.js中默认没有websocket服务,需要引入第三方包进行开发,第三方包有好几种,例如:socket.io、nodejs-websocket、ws等,这里我使用ws封装了websocket服务,如下:

// 导入查询数据的方法:
const queryData = require('../serves/websocketserve/websocketapi')
// 引入ws模块:
const Ws = require('ws')

// 导出websocket服务配置:
module.exports = () => {
  // 创建websocket服务:
  const wsServe = new Ws.Server({
    // 必须配置ip地址,否则上线后连接不上
    host: "127.0.0.1",
    port: 3001
  })
  // 监听客户端连接:
  wsServe.on('connection',client => {
    console.log('有客户端接入了')
    // 监听客户端有消息发送过来:
    client.on('message',async msg => {
      console.log('有客户端消息发送过来了')
      console.log(msg)
      const request = JSON.parse(msg)
      try {
        // 调用查询数据的方法从数据库中查询数据:
        const response = await queryData(request)
        // 将查到的数据返回给客户端
        client.send(JSON.stringify(response))
      } catch (err) {
        client.send(JSON.stringify({cod:404,msg:'获取数据失败'}))
      }

      // wsServe.clients中存所有当前连接的客户端,可以用来做广播
      wsServe.clients.forEach(itemClient => {
        itemClient.send('发送数据到当前已连接的所有websocket客户端')
      })
    })
  })
}

查询数据的方法queryData文件websocketapi代码如下:
这个文件可以配置需要进行websocket通信的api接口,如:

// 引入token验证:
const {verifyToken} = require('../../commethods/creattoken')
// 引入数据库配置:
const connection = require('../../config/mysqldbconfig')
// 引入动态数据库配置:
const dataConnection = require('../../config/mysqldbdataconfig')
// 导入时间格式化函数:
const dateFormatter2 = require('../../commethods/dateformat2')

// 导出查询到的数据,参数request.api是前端传过来唯一标识,可以理解为api接口url,即:'/api/login',只是只拿login,当与下面某个条件匹配时,调用匹配的函数查询数据并返回给调用处,之后websocket服务将调用处的数据响应给客户端
module.exports = (request) => {
  // 查询表格实时值请求:
  if (request.api === 'devicedata') {
    return devicedata(request)
  }
  // 查运维数据界面实时值请求:
  if (request.api === 'getinstpr') {
    return getinstpr(request)
  }
}

// 查时刻值方法:(这里做测试,我简单查下数据库数据即可,实际开发中这里可以利用异步查询多个数据在resolve返回)
function devicedata (request) {
  const {token} = request
  let isOk = verifyToken(token)
  const {id} = request.body
  // return 一个Promsie
  return new Promise((resolve,reject) => {
    isOk.then(() => {
      let sql = 'SELECT * FROM channe_name WHERE pid = "'+paramsn+'" LIMIT 1'
      connection.query(sql,(error,data)=>{
        try {
          if (error) {
            throw error
          } else {
            // 响应数据时也要加上唯一标识api:XXXXX,用于前端调用回调函数拿到响应数据:
            resolve({api:'devicedata', cod:200,msg:'获取实时值数据成功',valueArray: data})
          }
        } catch(err){
          console.log('通道名称接口错误:'+err)
        }
      })
    }).catch(() => {
      reject({api:'devicedata', cod:201,msg:'获取数据失败'})
    })
  })
}

// 查运维数据界面实时值请求方法:这里做测试,只是将上面方法复制到这里并修改了需要修改的参数,可加深理解
function getinstpr (request) {
  const {token} = request
  let isOk = verifyToken(token)
  const {id} = request.body
  // return 一个Promsie
  return new Promise((resolve,reject) => {
    isOk.then(() => {
      let sql = 'SELECT * FROM instr_data WHERE pid = "'+paramsn+'" LIMIT 1'
      connection.query(sql,(error,data)=>{
        try {
          if (error) {
            throw error
          }else{
            resolve({api:'getinstpr', cod:200,msg:'获取实时值数据成功',valueArray: data})
          }
        } catch(err){
          console.log('通道名称接口错误:'+err)
        }
      })
    }).catch(() => {
      reject({api:'getinstpr', cod:201,msg:'获取数据失败'})
    })
  })
}

在后端入口文件中开启websocket服务:

const express = require('express')

// 引入websocket配置:
const WebSocketServe = require('./config/websocketconfig')
// 开启websocket服务:
WebSocketServe()


const app = express()
app.listen(3000,'127.0.0.1',()=>{
  console.log('serve is running...');
})

5.配置nginx:

worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;
    client_max_body_size 10m;

    server {
        # 配置websocket代理:
        listen       3001;
        # 下面两项必须配置才可支持websocket服务:
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        location / {
            proxy_pass  http://127.0.0.1:3001; #设置接口代理服务器
        }

    }
}

提示:本文图片等素材来源于网络,若有侵权,请发邮件至邮箱:810665436@qq.com联系笔者 删除。

笔者:苦海

Logo

前往低代码交流专区

更多推荐