先看效果
1、WebSocket服务建立
1.1 引入包
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
1.2 新建配置类
package com.ruoyi.web.core.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration public class WebSocketConfig { /** * ServerEndpointExporter 作用 * <p> * 这个Bean会自动注册使用@ServerEndpoint注解声明的websocket endpoint * * @return */ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
1.3 新建服务类
package com.ruoyi.web.core.websocket.service; import com.alibaba.fastjson2.JSONObject; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; /** * websocket核心代码 * // 接口路径 ws://localhost:8080/webSocket/userId; */ @Component @Slf4j @ServerEndpoint("/webSocket/{userId}") public class WebSocketServer { /** * 与某个客户端的连接会话,需要通过它来给客户端发送数据 */ private Session session; /** * 用户ID */ private String userId; /** * concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。 * 虽然@Component默认是单例模式的,但springboot还是会为每个websocket连接初始化一个bean,所以可以用一个静态set保存起来。 * 注:底下WebSocket是当前类名 */ private static CopyOnWriteArraySet<WebSocketServer> webSockets = new CopyOnWriteArraySet<>(); /** * 用来存在线连接用户信息 */ private static ConcurrentHashMap<String, Session> sessionPool = new ConcurrentHashMap<String, Session>(); private static ApplicationContext applicationContext; public static void setApplicationContext(ApplicationContext context) { applicationContext = context; } /** * 链接成功调用的方法 */ @OnOpen public void onOpen(Session session, @PathParam(value = "userId") String userId) { try { this.session = session; this.userId = userId; webSockets.add(this); sessionPool.put(userId, session); log.info("【websocket消息】有新的连接,总数为:" + webSockets.size()); } catch (Exception e) { } } /** * 链接关闭调用的方法 */ @OnClose public void onClose() { try { webSockets.remove(this); sessionPool.remove(this.userId); log.info("【websocket消息】连接断开,总数为:" + webSockets.size()); } catch (Exception e) { } } /** * 收到客户端消息后调用的方法 * * @param message */ @OnMessage public void onMessage(String message) { log.info("【websocket消息】收到客户端消息:" + message); // JSONObject jsonObject = JSONObject.parseObject(message); } /** * 发送错误时的处理 * * @param session * @param error */ @OnError public void one rror(Session session, Throwable error) { log.error("用户错误,原因:" + error.getMessage()); error.printStackTrace(); } /** * 此为广播消息 * * @param message */ public void sendAllMessage(String message) { log.info("【websocket消息】广播消息:" + message); for (WebSocketServer webSocket : webSockets) { try { if (webSocket.session.isOpen()) { webSocket.session.getAsyncRemote().sendText(message); } } catch (Exception e) { e.printStackTrace(); } } } /** * 此为单点消息 * * @param userId * @param message */ public void sendOneMessage(String userId, String message) { Session session = sessionPool.get(userId); if (session != null && session.isOpen()) { try { log.info("【websocket消息】 单点消息:" + message); session.getAsyncRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } } /** * 此为单点消息(多人) * * @param userIds * @param message */ public void sendMoreMessage(String[] userIds, String message) { for (String userId : userIds) { Session session = sessionPool.get(userId); if (session != null && session.isOpen()) { try { log.info("【websocket消息】 单点消息:" + message); session.getAsyncRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } } } }
1.4 WebSocket 服务端已经完成,启动项目测试以下是否正常,下面顺便放一个html网页版测试工具
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>wsClient</title> <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script> <style> .btn-group{ display: inline-block; } </style> </head> <body> <input type='text' value='ws://localhost:8080/webSocket/userId' class="form-control" style='width:390px;display:inline' id='wsaddr' /> <div class="btn-group" > <button type="button" class="btn btn-default" onclick='addsocket();'>连接</button> <button type="button" class="btn btn-default" onclick='closesocket();'>断开</button> <button type="button" class="btn btn-default" onclick='$("#wsaddr").val("")'>清空</button> <button type="button" class="btn btn-default" onclick='restore()'>还原</button> </div> <div id="output" style="border:1px solid #ccc;height:365px;overflow: auto;margin: 20px 0;"></div> <input type="text" id='message' class="form-control" style='width:810px' placeholder="待发信息" onkeydown="en(event);"> <span class="input-group-btn"> <button class="btn btn-default" type="button" onclick="doSend();">发送</button> </span> </div> <script> /*组织时间*/ function formatDate(now) { var year = now.getFullYear(); var month = now.getMonth() + 1; var date = now.getDate(); var hour = now.getHours(); var minute = now.getMinutes(); var second = now.getSeconds(); return year + "-" + (month = month < 10 ? ("0" + month) : month) + "-" + (date = date < 10 ? ("0" + date) : date) + " " + (hour = hour < 10 ? ("0" + hour) : hour) + ":" + (minute = minute < 10 ? ("0" + minute) : minute) + ":" + ( second = second < 10 ? ("0" + second) : second); } var output; var websocket; function init() { output = document.getElementById("output"); } /*连接按钮*/ function addsocket() { var wsaddr = $("#wsaddr").val(); if (wsaddr == '') { alert("请填写websocket的地址"); return false; } StartWebSocket(wsaddr); } /*断开按钮*/ function closesocket() { websocket.close(); } /*还原按钮*/ function restore(){ $("#wsaddr").val('ws://192.168.0.154:8080/'); } function en(event) { var evt = evt ? evt : (window.event ? window.event : null); if (evt.keyCode == 13) { doSend() } } /*发送按钮*/ function doSend() { var message = $("#message").val(); if (message == '') { alert("请先填写发送信息"); $("#message").focus(); return false; } if (typeof websocket === "undefined") { alert("websocket还没有连接,或者连接失败,请检测"); return false; } if (websocket.readyState == 3) { alert("websocket已经关闭,请重新连接"); return false; } console.log(websocket); $("#message").val(''); writeToScreen('<span style="color:green">你发送的信息 ' + formatDate(new Date()) + '</span><br/>' + message); websocket.send(message); } /*书写内容*/ function StartWebSocket(wsUri) { websocket = new WebSocket(wsUri); websocket.onopen = function(evt) { onOpen(evt) }; websocket.onclose = function(evt) { onClose(evt) }; websocket.onmessage = function(evt) { onMessage(evt) }; websocket.onerror = function(evt) { one rror(evt) }; } function onOpen(evt) { writeToScreen("<span style='color:red'>连接成功,现在你可以发送信息啦!!!</span>"); } function onClose(evt) { writeToScreen("<span style='color:red'>websocket连接已断开!!!</span>"); websocket.close(); } function onMessage(evt) { writeToScreen('<span style="color:blue">服务端回应 ' + formatDate(new Date()) + '</span><br/><span class="bubble">' + evt.data + '</span>'); } function one rror(evt) { writeToScreen('<span style="color: red;">发生错误:</span> ' + evt.data); } function writeToScreen(message) { var div = "<div class='newmessage'>" + message + "</div>"; var d = $("#output"); var d = d[0]; var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight; $("#output").append(div); if (doScroll) { d.scrollTop = d.scrollHeight - d.clientHeight; } } </script> </body> </html>
2、Vue 前端实现
2.1 首先项目中安装speak-tts语音播报插件
npm install speak-tts
2.2创建一个全局的 speech.js文件,文件中引入插件并初始化后导出。因为可能会一直读多条消息,防止初始化多个Speech对象,在全局api中初始化一个对象,方便播报的时候调用。
import Speech from 'speak-tts' const speech=new Speech() export default speech
2.3 在项目点击登录按钮后调用全局的语音播报方法。
由于浏览器之间有安全限制,用户不主动触发语音播报方法, 语音播报不会主动发出声音,故在项目的登录处触发方法。
2.3.1 在登录页面 引入封装好得js文件,并初始化方法
import Speech from '@/utils/speech' initSpeech(){ Speech.setLanguage('zh-CN') Speech.init({ volume: 0.6, // 音量0-1 lang: "zh-CN", // 语言 rate: 2, // 语速1正常语速,2倍语速就写2 pitch: 1, // 音调 voice: "Microsoft Yaoyao - Chinese (Simplified, PRC)", }) }
3 在登录后的页面入口文件处编写弹框样式及告警信息的接收等功能(我项目是elementUI 所以是AppMain.vue 页面 在layout下面)
<template> <section class="app-main"> <transition name="fade-transform" mode="out-in"> <keep-alive :include="cachedViews"> <router-view v-if="!$route.meta.link" :key="key" /> </keep-alive> </transition> <iframe-toggle /> <div class="alarmmodel" v-if="popupList.length > 0"> <el-card class="box-card" shadow="always" v-for="(item, index) in popupList" :key="index" > <div slot="header" class="clearfix"> <span style="color: green; font-size: 25px" v-if="item.notifyType == 'newOrder'" >{{ index + 1 }}、订单提醒</span > <span style="color: red; font-size: 25px" v-else-if="item.notifyType == 'refundOrder'" >{{ index + 1 }}、取消订单提醒</span > <el-button style="float: right; padding: 3px 0" type="text" v-if="item.notifyType == 'newOrder'" @click="popupSubmit(item, index, item.notifyType)" >接单</el-button > <el-button style="float: right; padding: 3px 0" type="text" v-else @click="popupSubmit(item, index, item.notifyType)" >确定</el-button > </div> <div class="orderInfo"> <p> <span class="orderInfo_title">名 称:</span> {{ item.goodsName }} </p> <p><span class="orderInfo_title">数 量:</span> {{ item.goodsNum }}</p> <p> <span class="orderInfo_title">金 额:</span> {{ item.payPrice }} 元 </p> <p><span class="orderInfo_title">备 注:</span> {{ item.remark }}</p> </div> </el-card> </div> </section> </template> <script> import iframeToggle from "./IframeToggle/index"; import Speech from "@/utils/speech"; import { updateOrderStatus } from "@/api/orderInfo"; export default { name: "AppMain", components: { iframeToggle }, data() { return { heartbeatTimer: null, // 监测心跳 popupList: [], // 存储弹框数据 pathpopup: window._CONFIG['WebSocketUrl'], // websocket链接地址 socketpopup: null, // 初始化websocket对象 }; }, computed: { cachedViews() { return this.$store.state.tagsView.cachedViews; }, key() { return this.$route.path; }, }, mounted() { this.popupList = []; this.initPopupsoket(); }, beforeDestroy() { this.speech.cancel(); // 取消播放 this.speech = null; this.socketpopup.onclose = this.closePopup; }, methods: { initPopupsoket() { if (typeof WebSocket === "undefined") { alert("您的浏览器不支持socket"); } else { //获取当前登录用户ID const uid = this.$store.getters.id; console.log("当前登录用户信息", uid); if (uid == undefined) { console.log("未获取到商家用户ID,无法实时推送订单消息"); } else { this.socketpopup = new WebSocket(this.pathpopup + uid); this.socketpopup.onopen = this.openPopup; this.socketpopup.onerror = this.errorPopup; this.socketpopup.onmessage = this.getMessagepopup; } } }, openPopup() { console.log("socketpopup连接成功"); this.startHeartbeat(); // 添加心跳监测,用来防止websocket断开 }, startHeartbeat() { // 发送心跳消息 var _this = this; if (_this.heartbeatTimer == null) { _this.heartbeatTimer = setInterval(function () { console.log("监测心跳"); _this.socketpopup.send("ping"); }, 10000); } }, stopHeartbeat() { // 停止心跳 if (this.heartbeatTimer !== null) { clearInterval(this.heartbeatTimer); this.heartbeatTimer = null; } }, errorPopup() { console.log("1连接错误"); }, getMessagepopup(msg) { const returnMsg = JSON.parse(msg.data); console.log("接受消息数据1", returnMsg); console.log("接受消息数据2", returnMsg.message); this.popupList.push(returnMsg); // 将推送的单条数据存起来显示多个弹框,在用户点击确定后消除此条弹框 this.startSpeech(returnMsg.message); // 将数据中的告警传给播报的对象 }, closePopup() { console.log("socketpopup已经关闭"); //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。 window.onbeforeunload = function () { this.socketpopup.close(); }; }, startSpeech(text) { Speech.speak({ text: text, listeners: { //开始播放 onstart: () => { console.log("Start utterance"); }, //判断播放是否完毕 onend: () => { console.log("End utterance"); }, //恢复播放 onresume: () => { console.log("Resume utterance"); }, }, }).then(() => { console.log("读取成功", this.popupList.length); }); }, popupSubmit(item, index, notifyType) { if (notifyType == "newOrder") { //接单 updateOrderStatus({ id: item.orderId, orderStatus: '4' }).then((response) => { this.$message.success("接单成功!"); this.popupList.splice(index, 1); this.$router.push({ path: "/order_manage/dl_order_info" }).catch(() => {}); }); } else { this.popupList.splice(index, 1); } }, }, }; </script> <style lang="scss" scoped> .app-main { /* 50= navbar 50 */ min-height: calc(100vh - 50px); width: 100%; position: relative; overflow: hidden; } .fixed-header + .app-main { padding-top: 50px; } .hasTagsView { .app-main { /* 84 = navbar + tags-view = 50 + 34 */ min-height: calc(100vh - 84px); } .fixed-header + .app-main { padding-top: 84px; } } </style> <style lang="scss"> // fix css style bug in open el-dialog .el-popup-parent--hidden { .fixed-header { padding-right: 6px; } } ::-webkit-scrollbar { width: 6px; height: 6px; } ::-webkit-scrollbar-track { background-color: #f1f1f1; } ::-webkit-scrollbar-thumb { background-color: #c0c0c0; border-radius: 3px; } .alarmmodel { position: fixed; /* 使div固定在页面上的某个位置 */ bottom: 10px; /* 距离顶部10像素 */ right: 10px; /* 距离右侧10像素 */ z-index: 1000; /* 确保div在其他内容之上 */ width: 280px; /* 弹窗宽度 */ height: 400px; /* 弹窗高度 */ overflow-y: scroll; background-color: rgb(201, 194, 194); color: #fff; /* 文字颜色 */ text-align: center; /* 文字居中 */ } .orderInfo { text-align: left; } .orderInfo_title { font-weight: bolder; } </style>
标签:function,VUE,websocket,log,tt,session,WebSocket,message,evt From: https://www.cnblogs.com/bin521/p/18662000