开发背景
工单完成后需要实时向客户端推送消息提醒,即服务端向客户端发送消息
实现效果:弹窗出新消息提示,并且有音乐声。消息提醒数+1
消息存一份,可随时查看,例如
什么是websocket
WebSocket是HTML5新增的协议,它的目的是在浏览器和服务器之间建立一个不受限的双向通信的通道,比如说,服务器可以在任意时刻发送消息给浏览器。
为什么传统的HTTP协议不能做到WebSocket实现的功能?
这是因为HTTP协议是一个请求-响应协议,请求必须先由浏览器发给服务器,服务器才能响应这个请求,再把数据发送给浏览器。换句话说,浏览器不主动请求,服务器是没法主动发数据给浏览器的。
其实在之前有两个替代方案,不过都不能完全满足需求。一个是轮询,一个是comet。简单理解,轮询就是通过js设置一个定时器不断查询接口,但是这样做会造成一个问题,定时器频率太慢相当于延时会很长,频率太快又会给服务器带来很大的压力;而comet可以理解为一次请求如果没有超过预定时间或者没有返回数据,就会一直保持链接状态,在服务器挂起一个线程,这就代表着也要消耗服务器资源,而且,一个HTTP连接在长时间没有数据传输的情况下,链路上的任何一个网关都可能关闭这个连接,而网关是我们不可控的,这就要求Comet连接必须定期发一些ping数据表示连接“正常工作”。
所以综上WebSocket是目前实现实时通讯的最优方案。
实现
前端
jsapi.httpGet(serviceBase + "service/sys/user/getName", function (resultObject) {
// console.log("serviceBase is :"+serviceBase)
vm.$data.username = resultObject.data.name;
userId = resultObject.data.code;
createWebSocket();
});
// 重复锁-避免重复连接
var lockReconnect = false;
var url = serviceBase+"webSocket";
var wsUrl = url.replace('http','ws');
var websocket = null;
var tt = null;
function createWebSocket(){
var is_support = ("WebSocket" in window);
if (is_support) {
console.log("浏览器支持 WebSocket");
}else {
alert("您的浏览器不支持 WebSocket,将无法接收到消息通知。\n解决:升级浏览器版本或更换浏览器");
}
try {
websocket = new WebSocket(wsUrl+"/"+userId);
initWebSocket();
} catch(e) {
console.log('catch');
reconnect(wsUrl);
}
}
function initWebSocket(){
//连接发生错误的回调方法
websocket.onerror = function(){
// console.log("socket连接失败");
//重连
reconnect(wsUrl);
};
//连接成功建立的回调方法
websocket.onopen = function(event){
// console.log("socket连接已打开");
//心跳检测重置
heartCheck.start();
};
//接收到消息的回调方法
websocket.onmessage = function(event){
var received_msg = event.data;
// console.log("收到消息"+received_msg);
//心跳检测重置
heartCheck.start();
if (received_msg!=='心跳'){
// 弹窗提醒,播放音乐
layer.msg(received_msg, {
time: 2000, //2s后自动关闭
offset: ['80px', '80%']
});
controlMp3()
// 刷新统计数
initCountNum()
}
};
//连接关闭的回调方法
websocket.onclose = function(){
// console.log("socket连接已关闭");
//重连
reconnect(wsUrl);
};
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function(){
websocket.close();
};
}
//重连函数
function reconnect(url) {
if(lockReconnect) {
return;
};
lockReconnect = true;
//没连接上会一直重连,设置延迟避免请求过多
tt && clearTimeout(tt);
tt = setTimeout(function () {
createWebSocket(url);
lockReconnect = false;
}, 20000);
}
// 心跳检测
var heartCheck ={
// 每隔几秒检测心跳是否正常
timeout: 50000,
timeoutObj: null,
serverTimeoutObj: null,
start: function (){
// console.log("开始测试心跳");
var self = this;
this.timeoutObj && clearTimeout(this.timeoutObj);
this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj);
this.timeoutObj = setTimeout(function(){
//这里发送一个心跳,后端收到后,返回一个心跳消息,
// console.log('发送消息,测试后台是否运行中...');
//任意发一个消息过去,后台接收,在init()中的onmessage收到消息,说明后台没有挂掉,有心跳
websocket.send("心跳测试",userId);
self.serverTimeoutObj = setTimeout(function() {
// console.log("后台挂掉,没有心跳了....");
// console.log("打印websocket的地址:"+websocket);
websocket.close();
// createWebSocket();
}, self.timeout);
}, this.timeout)
}
}
//关闭连接
function closeWebSocket(){
websocket.close();
}
controlMp3 = function (){
// var audio = document.getElementById('music');
var audio= new Audio("../resources/music/13203.mp3");
audio.play();
}
解析
剩下的就是连接方法和心跳机制,分别都有注解,就不再复述
后端
导入依赖
maven
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>5.2.12.RELEASE</version>
</dependency>
配置类WebSocket
package com.keytop.superpark.ks.mq;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.sql.Time;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CopyOnWriteArraySet;
@Component
@ServerEndpoint("/webSocket/{userCode}")
public class WebSocket {
private static Logger logger = LoggerFactory.getLogger(WebSocket.class);
private Session session;
private String userCode;
private static CopyOnWriteArraySet<WebSocket> webSocketSet = new CopyOnWriteArraySet<>();
@OnOpen
public void onOpen(@PathParam("userCode") String userCode, Session session){
this.session = session;
this.userCode = userCode;
webSocketSet.add(this);
logger.info("【websocket消息】有新的连接,总数:{}",webSocketSet.size());
}
@OnClose
public void onClose(){
webSocketSet.remove(this);
logger.info("【websocket消息】连接断开,总数:{}",webSocketSet.size());
}
@OnMessage
public void onMessage(String message,@PathParam("userCode") String userCode) throws IOException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
logger.info("【websocket消息】收到客户端发来的消息:{},用户:{},时间:{}",message,userCode,sdf.format(new Date()));
if ("心跳测试".equals(message)){
for (WebSocket item : webSocketSet) {
if (item.userCode.equals(userCode)){
synchronized (item.session){
item.session.getBasicRemote().sendText("心跳");
logger.info("【websocket消息】单播消息,message={}",message);
}
}
}
}
}
public void sendMessageAll(String message) throws IOException {
for (WebSocket webSocket :webSocketSet ){
logger.info("【websocket消息】广播消息,message={}",message);
webSocket.session.getBasicRemote().sendText(message);
}
}
public void sendMessageTo(String message, String userId) throws IOException {
for (WebSocket item : webSocketSet) {
if (item.userCode.equals(userId)){
item.session.getAsyncRemote().sendText(message);
logger.info("【websocket消息】单播消息,message={}",message);
}
}
}
@OnError
public void onerror(Session session, Throwable throwable){
logger.info("【websocket 异常】,error message={}",throwable);
}
}
实际应用
String message ="您有一条新的工单处理";
try {
webSocket.sendMessageTo(message,userCode);
} catch (IOException e) {
e.printStackTrace();
logger.error("MsgPushService 推送失败:{}", e.getMessage());
}
效果:弹窗出新消息提示,并且有音乐声。消息提醒数+1
消息可以存一份,可随时查看,例如
遇到的一些问题及解决
WebSocket的心跳重连机制
https://blog.csdn.net/qq_33922980/article/details/102646295
WebSocket 长连接实现
https://www.jianshu.com/p/767163a33d78
Java 与 JavaScript 建立websocket长连接
https://blog.csdn.net/u012472945/article/details/79510467