1、项目场景

监控某工程状态变化,将后台推送信息解析并实时展示,推送数据量较大。

2、使用技术

vue+websocket+protobuf

3、技术介绍

Websocket

通信协议,基于tcp。与http协议不同,http是短连接,浏览器给服务端发送请求时开始,服务端接收处理响应后关闭,而且http协议只能由浏览器端发起,服务端无法直接推送。而ws是一个长连接,在vue的单个页面中,从页面创建(或者调用ws初始化方法)时开始,在当前页面关闭时结束。如果想要在整个项目中通用一个ws,可以将ws的触发放在app.vue上。ws可以实现浏览器端和服务端的双向通信,消息可以是文本也可以是二进制流数据(blob),不存在跨域问题。使用ws就可以实现后台主动向前端推送数据,保证数据的实时渲染。

protobuf

protobuf是一种与语言无关、平台无关、可扩展的序列化数据结构,可以用于数据通信、数据存储。序列化是指将数据结构或者对象转换成能够被存储和传输的格式,同时保证序列化的结果在另一种环境中能够被重建回原来的数据或者对象。相比于XML、JSON,protobuf更加高效,适用于数据量较大的场景。

4、前端websocket实现

这里提供两种ws实现方式:原生ws、借助stompJS实现ws,两者差距在于后台ws的使用。如果后台使用的是Spring底层及的Websocket API,就使用原生的ws,如果后台启用了SockJS通信,就使用借助stompJS实现ws。需要注意的是,原生的ws在由浏览器端向后台发送数据时,后台支持接收的数据类型有String,ArrayBuffer、Blob、ArrayBufferView,但是借助stompJS的实现方式只支持发送String。
原生ws实现

<template>
  <div></div>
</template>

<script>
export default {
  name: "",
  components: {},
  props: [],
  data() {
    return {
      path: "ws://127.0.0.1:8085/binaryHandler", // socket连接地址
      socket: "", // socket对象
    };
  },
  watch: {},
  computed: {},
  methods: {
    init() {
      // 判断浏览器是否支持socket
      if (typeof WebSocket === "undefined") {
        this.$message({
          message: "您的浏览器不支持socket",
          type: "warn"
        });
      } else {
        // 实例化socket
        this.socket = new WebSocket(this.path);
        // 监听socket连接
        this.socket.onopen = this.open;
        // 监听socket错误信息
        this.socket.onerror = this.error;
        // 监听socket推送消息
        this.socket.onmessage = this.getMessage;
      }
    },
    open() {
      console.log("socket连接成功");
    },
    error() {
      console.log("连接错误");
    },
    getMessage(msg) {
        console.log(msg)
    },
    send() {
      this.socket.send("数据发送");
    },
    close() {
      console.log("socket已关闭");
    }
  },
  created() {},
  mounted() {
      this.init();
  },
  destroyed() {
    this.socket.close = this.close;
  }
};
</script>
<style lang='scss' scoped>
</style>

基于stompJS实现

<template>
  <div></div>
</template>

<script>
import SockJS from "sockjs-client";
import Stomp from "stompjs";
import heartBeat from "@/proto/HeartBeat_pb";
export default {
  name: "SocketLink",
  components: {},
  props: [],
  data() {
    return {
      socketDir: "http://127.0.0.1:8085/gs-guide-websocket", // socket连接地址
      stompClient: null, // STOMP子协议的客户端对象
      sendUrl: "/app/hello", // 发送数据通道
      receiveUrl: "/topic/greetings" // 接收数据通道
    };
  },
  watch: {},
  computed: {},
  methods: {
    // socket创建连接
    initWebSocket() {
      // 建立连接对象
      let socket = new SockJS(this.socketDir);
      this.stompClient = Stomp.over(socket); // 获取STOMP子协议的客户端对象
      this.connection();
    },
    connection() {
      // 定义客户端的认证信息,按需求配置
      let headers = {};
      // 向服务器发起websocket连接
      this.stompClient.connect(
        headers,
        () => {
          this.stompClient.subscribe(
            this.receiveUrl,
            msg => {
              // todo 对数据处理
              let data = msg;
            },
            headers
          );
        },
        err => {
          // 连接发生错误时的处理函数
          console.log(err);
        }
      );
    },
    // 断开连接
    disconnect() {
      if (this.stompClient) {
        this.stompClient.disconnect();
      }
    },
    // 发送数据
    sendMessage() {
      let msg = "aaa";
      // 这里只支持发送string类型
      this.stompClient.send(this.sendUrl, {}, msg);
    }
  },
  created() {},
  mounted() {
      this.initWebSocket();
  }
};
</script>
<style lang='scss' scoped>
</style>

5、前端protobuf数据处理

这里提供两种protobuf数据处理方式,借助google-protobuf、借助protobufJS。借助于StompJS的ws的实现后台不能接收arraybuffer数据类型,这里仅配合后台原生ws的实现。
借助google-protobuf
(1) 使用npm下载 google-protobuf。

npm install google-protobuf -S

(2) 由于是在vue中使用,建议将后台提供的proto转化成js。后台提供的proto文件类似于以下格式:

syntax = "proto3";

 option java_package = "com.train.proto";
 option java_outer_classname = "HeartBeatClass";

message HeartBeat {
    int32 heart =1;
    int32 time =2;
    int32 ids =3;
    int64 requestInfo =4;
}

在命令行执行下面代码,转化成js文件

protoc.exe --js_out=import_style=commonjs,binary:. xxx.proto

(3) google-protobuf提供了序列化和反序列化的方法,可以实现对二进制数据的处理

<template>
  <div></div>
</template>

<script>
import heartBeat from "@/proto/HeartBeat_pb";
export default {
  name: "",
  components: {},
  props: [],
  data() {
    return {
      path: "ws://127.0.0.1:8085/binaryHandler", // socket连接地址
      socket: "",  // socket对象
    };
  },
  watch: {},
  computed: {},
  methods: {
    init() {
      // 判断浏览器是否支持socket
      if (typeof WebSocket === "undefined") {
        this.$message({
          message: "您的浏览器不支持socket",
          type: "warn"
        });
      } else {
        // 实例化socket
        this.socket = new WebSocket(this.path);
        // 监听socket连接
        // 监听socket错误信息
        this.socket.onerror = this.error;
        // 监听socket推送消息
        this.socket.onmessage = this.getMessage;
      }
    },
    open() {
      console.log("socket连接成功");
    },
    error() {
      console.log("连接错误");
    },
    getMessage(msg) {
      // websocket server 返回的是blob
      let result = msg.data;
      if (result instanceof Blob) {
        // 对blob数据进行处理
        let reader = new FileReader();
        reader.readAsArrayBuffer(result);
        reader.onload = ()=>{
          const buf = new Uint8Array(reader.result);
          // 反序列化
          let decodeMsg = heartBeat.HeartBeat.deserializeBinary(buf);
          // 获取数据,使用getXXX();
          let requestInfo = decodeMsg.getRequestinfo()
        }
      }
    },
    send() {
      if(this.socket === "") return;
        // 创建heart对象
         const heart = new heartBeat.HeartBeat();
         // 赋值,使用setXXX
         heart.setHeart(100);
         heart.setTime(150);
         heart.setIds(200);
         heart.setRequestinfo(250);
         // 序列化
         const hearts = heart.serializeBinary();
         this.socket.send(buf);
    },
    close() {
      console.log("socket已关闭");
    }
  },
  created() {},
  mounted() {
      this.init()
  },
  destroyed() {
    this.socket.close = this.close;
  }
};
</script>
<style lang='scss' scoped>
</style>

关于google-protobuf的详细介绍见官网:https://www.npmjs.com/package/google-protobuf
借助于protobufJS
(1) 使用npm下载 protobufJS。

npm install protobufjs -S;

(2) 同样推荐将proto文件转化成js文件
在项目跟目录下执行,A:生成js文件存放位置 B:需要生成js的proto文件

npx pbjs -t json-module -w commonjs -o A B

例如:

npx pbjs -t json-module -w commonjs -o src/proto/HeartBeat.js src/proto/HeartBeat.proto

(3) protobufJS提供方法较多,lookup、encode、decode等

<template>
  <div></div>
</template>

<script>
import heartBeatOth from "@/proto/HeartBeat"
export default {
  name: "",
  components: {},
  props: [],
  data() {
    return {
      path: "ws://127.0.0.1:8085/binaryHandler", // socket连接地址
      socket: "", // socket实例
      heartOther: "", // heartBeatOth对象
    };
  },
  watch: {},
  computed: {},
  methods: {
    init() {
      // 判断浏览器是否支持socket
      if (typeof WebSocket === "undefined") {
        this.$message({
          message: "您的浏览器不支持socket",
          type: "warn"
        });
      } else {
        // 实例化socket
        this.socket = new WebSocket(this.path);
        // 监听socket连接
        this.socket.onopen = this.open;
         // 获取消息类型
        this.heartOther = heartBeatOth.lookup("HeartBeat");
        // 监听socket错误信息
        this.socket.onerror = this.error;
        // 监听socket推送消息
        this.socket.onmessage = this.getMessage;
      }
    },
    open() {
      console.log("socket连接成功");
    },
    error() {
      console.log("连接错误");
    },
    getMessage(msg) {
      // websocket server返回的是blob
      let result = msg.data;
      if (result instanceof Blob) {
        // 对blob进行处理
        let reader = new FileReader();
        reader.readAsArrayBuffer(result);
        reader.onload = ()=>{
          const buf = new Uint8Array(reader.result);
          let decodeMsg = this.heartOther.decode(buf);
          let { requestInfo } = decodeMsg;
        }
      }
    },
    send() {
      if(this.socket === "") return;
      // 传递内容
      let cont = {
        heart: 100,
        time: 150,
        ids: 200,
        requestInfo: 250
      }
      // 验证内容是否有效
      let errMsg = this.heartOther.verify(cont);
      if(errMsg) throw Error(errMsg);
      // 创建消息实体
      let message = this.heartOther.create(cont);
      // 将消息实体编码成Uint8Array 
      let buf = this.heartOther.encode(message).finish();
      this.socket.send(buf);
    },
    close() {
      console.log("socket已关闭");
    }
  },
  created() {},
  mounted() {
      this.init();
  },
  destroyed() {
    this.socket.close = this.close;
  }
};
</script>
<style lang='scss' scoped>
</style>

关于google-protobuf的详细介绍见官网:https://www.npmjs.com/package/protobufjs

补充
(1) 使用axios对接收protobuf数据的处理。需要修改content-type, responseType

import Vue from 'vue'
import axios from 'axios'

const protoSer = axios.create({
    timeout: 60000,
    headers: {
      'X-Requested-With': 'XMLHttpRequest',
      'Content-Type': 'application/x-protobuf;charset=UTF-8'
    },
    responseType: 'arraybuffer'
  })

Vue.prototype.$protoSer = protoSer
export default protoSer

(2) 使用google-protobuf、protobufJS在数据反序列化、序列化、获取数据上的打印结果
google-protobuf反序列化

protobufJS反序列化

google-protobuf序列化

protobufJS序列化

google-protobuf获取数据

protobufJS获取数据