之前工作中用的服务端主动请求一直用的sse,感兴趣的兄弟可以了解一下,但由于sse兼容性问题最近决定替换为websocket,这里分享一下个人的整合思路,有不对的欢迎兄弟们可以私信或留言。
1.导入依赖
<!--websocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.开启websocket配置
package com.yl.framework.websocket;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* websocket 配置
* 开启websocket
* @author shg
*/
@Configuration
public class WebSocketConfig
{
@Bean
public ServerEndpointExporter serverEndpointExporter()
{
return new ServerEndpointExporter();
}
}
3.websocketServer,用于和客户端交互
package com.yl.framework.websocket;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Semaphore;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* websocket 消息处理
*
* @author shg
*/
@Component
@ServerEndpoint("/websocket/message/{webSocketKey}")
public class WebSocketServer {
/**
* WebSocketServer 日志控制器
*/
private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketServer.class);
/**
* 默认最多允许同时在线人数100
*/
public static int socketMaxOnlineCount = 100;
/**
* 用户集
*/
private static Map<String, Session> USERS = new ConcurrentHashMap<String, Session>();
private static Semaphore socketSemaphore = new Semaphore(socketMaxOnlineCount);
/**
* 存储用户
*
* @param key 唯一键
* @param session 用户信息
*/
public static void putUser(String key, Session session)
{
USERS.put(key, session);
}
/**
* 移除用户
*
* @param session 用户信息
*
* @return 移除结果
*/
public static boolean removeBySession(Session session)
{
String key = null;
boolean flag = USERS.containsValue(session);
if (flag)
{
Set<Map.Entry<String, Session>> entries = USERS.entrySet();
for (Map.Entry<String, Session> entry : entries)
{
Session value = entry.getValue();
if (value.equals(session))
{
key = entry.getKey();
break;
}
}
}
else
{
return true;
}
return removeUserByKey(key);
}
/**
* 移出用户
*
* @param key 键
*/
public static boolean removeUserByKey(String key)
{
LOGGER.info("\n 正在移出用户 - {}", key);
Session remove = USERS.remove(key);
if (remove != null)
{
boolean containsValue = USERS.containsValue(remove);
LOGGER.info("\n 移出结果 - {}", containsValue ? "失败" : "成功");
return containsValue;
}
else
{
return true;
}
}
/**
* 获取在线用户列表
*
* @return 返回用户集合
*/
public static Map<String, Session> getUsers()
{
return USERS;
}
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("webSocketKey") String webSocketKey) throws Exception {
//判断是否建立过连接
if (Objects.nonNull(USERS.get(webSocketKey))) {
return;
}
// 尝试获取信号量
boolean semaphoreFlag = SemaphoreUtils.tryAcquire(socketSemaphore);
if (!semaphoreFlag) {
// 未获取到信号量
LOGGER.error("\n 当前在线人数超过限制数- {}", socketMaxOnlineCount);
WebSocketUtil.sendTextMessageToUserBySession(session, new WebSocketMessage("error", "当前在线人数超过限制数:" + socketMaxOnlineCount));
session.close();
} else {
// 添加用户
//WebSocketUsers.put(session.getId(), session);
//以webSocketKey作为唯一标识,登录时颁发并存到客户端的localStorage
putUser(webSocketKey, session);
LOGGER.info("\n 建立连接 - {}", session);
LOGGER.info("\n 当前人数 - {}", USERS.size());
WebSocketUtil.sendTextMessageToUserBySession(session, new WebSocketMessage("heartbeat", "pong"));
}
}
/**
* 连接关闭时处理
*/
@OnClose
public void onClose(Session session, @PathParam("webSocketKey") String webSocketKey) {
LOGGER.info("\n 关闭连接 - {}", session);
// 移除用户
removeUserByKey(webSocketKey);
// 获取到信号量则需释放
SemaphoreUtils.release(socketSemaphore);
}
/**
* 抛出异常时处理
*/
@OnError
public void onError(@PathParam("webSocketKey") String webSocketKey, Session session, Throwable exception) throws Exception {
if (session.isOpen()) {
// 关闭连接
session.close();
}
LOGGER.info("\n 连接异常 - {}", webSocketKey);
LOGGER.info("\n 异常信息 - {}", webSocketKey);
// 移出用户
removeUserByKey(webSocketKey);
// 获取到信号量则需释放
SemaphoreUtils.release(socketSemaphore);
}
/**
* 服务器接收到客户端消息时调用的方法
*/
@OnMessage
public void onMessage(@PathParam("webSocketKey") String webSocketKey, String message, Session session) {
WebSocketMessage socketMessage = JSONObject.parseObject(message, WebSocketMessage.class);
//判断该次请求的消息类型是心跳检测还是获取信息
if (socketMessage.getType().equals("heartbeat")) {
//立刻向前台发送消息,代表后台正常运行
WebSocketUtil.sendTextMessageToUserByWebSocketKey(webSocketKey, new WebSocketMessage("heartbeat", "ok"));
} else if (socketMessage.getType().equals("message")) {
//收到消息,执行业务逻辑......
}
}
}
这里核心参数是websocketKey,因为业务中允许同一账号多设备登录的情况我这里的websocketKey采用:用户名 + ‘_’ +uuid的形式生成。
4. 信号量工具类和消息推送工具类
package com.yl.framework.websocket;
import java.util.concurrent.Semaphore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 信号量相关处理
*
* @author shg
*/
public class SemaphoreUtils
{
/**
* SemaphoreUtils 日志控制器
*/
private static final Logger LOGGER = LoggerFactory.getLogger(SemaphoreUtils.class);
/**
* 获取信号量
*
* @param semaphore
* @return
*/
public static boolean tryAcquire(Semaphore semaphore)
{
boolean flag = false;
try
{
flag = semaphore.tryAcquire();
}
catch (Exception e)
{
LOGGER.error("获取信号量异常", e);
}
return flag;
}
/**
* 释放信号量
*
* @param semaphore
*/
public static void release(Semaphore semaphore)
{
try
{
semaphore.release();
}
catch (Exception e)
{
LOGGER.error("释放信号量异常", e);
}
}
}
package com.yl.framework.websocket;
import com.alibaba.fastjson.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.websocket.Session;
import java.io.IOException;
import java.util.Collection;
/**
* websocket 工具类
*
* @author shg
*/
public class WebSocketUtil {
/**
* WebSocketUsers 日志控制器
*/
private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketUtil.class);
/**
* 根据session发送文本消息到客户端
* @param session
* @param message 消息内容
*/
public static void sendTextMessageToUserBySession(Session session, WebSocketMessage message)
{
if (session != null)
{
try
{
session.getBasicRemote().sendText(JSONObject.toJSONString(message));
}
catch (IOException e)
{
LOGGER.error("\n[发送消息异常]", e);
}
}
else
{
LOGGER.info("\n[你已离线]");
}
}
/**
* 根据webSocketKey发送文本消息到客户端
* @param webSocketKey 用户名 + uuid
* @param message 消息内容
*/
public static void sendTextMessageToUserByWebSocketKey(String webSocketKey, WebSocketMessage message)
{
Session session = WebSocketServer.getUsers().get(webSocketKey);
if (session != null)
{
try
{
session.getBasicRemote().sendText(JSONObject.toJSONString(message));
}
catch (IOException e)
{
LOGGER.error("\n[发送消息异常]", e);
}
}
else
{
LOGGER.info("\n[你已离线]");
}
}
/**
* 群发消息文本消息
*
* @param message 消息内容
*/
public static void sendAllUserMessage(WebSocketMessage message)
{
Collection<Session> values = WebSocketServer.getUsers().values();
for (Session value : values)
{
sendTextMessageToUserBySession(value, message);
}
}
}
5.测试推送消息和文件,这里我的文件是放本地的,可以按需更改
package com.yl.framework.websocket;
import cn.hutool.core.io.IoUtil;
import com.yl.common.core.domain.AjaxResult;
import com.yl.common.utils.Base64Util;
import com.yl.framework.util.Byte2InputStream;
import org.apache.tomcat.util.codec.binary.Base64;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Date;
/**
* websocket模拟服务端向客户端发送文本和文件
*
* @author shg
*/
@RestController
public class WebSocketTestController {
/**
* webSocket后端主动发送文本
*
* @param webSocketKey 从前端的localStorage获取
*/
@GetMapping("/test/sendTextMessage/{webSocketKey}")
public AjaxResult sendTextMessage(@PathVariable("webSocketKey") String webSocketKey) {
//正常消息类型(message)
WebSocketUtil.sendTextMessageToUserByWebSocketKey(webSocketKey, new WebSocketMessage("message", "测试消息" + new Date().getTime()));
return AjaxResult.success();
}
/**
* webSocket后端主动发送文件
*
* @param webSocketKey 从前端的localStorage获取
*/
@GetMapping("/test/sendFileMessage/{webSocketKey}/{fileName}")
public AjaxResult sendFileMessage(@PathVariable("webSocketKey") String webSocketKey, @PathVariable("fileName") String fileName) throws Exception{
//正常消息类型(message)
InputStream inputStream = new FileInputStream("F:\\file\\" + fileName);
byte[] bytes = Byte2InputStream.inputStream2byte(inputStream);
String str = Base64Util.encodeBase64(bytes);
WebSocketUtil.sendTextMessageToUserByWebSocketKey(webSocketKey, new WebSocketMessage("message", str));
return AjaxResult.success();
}
}
到这里后端就算整合完毕了,前端整合如下:
1、登录时将websocket放到localsotrge,退出时清除:
// 登录
window.localStorage.setItem("webSocketKey",res.webSocketKey);
// 退出系统
window.localStorage.removeItem("webSocketKey")
2、这里是将核心代码封装了一个工具类,将消息分为message(业务消息)和heartbeat(心跳消息两种),js代码如下:
//暴露自定义websocket对象
export const socket = {
//后台请求路径
url: "",
//websocket对象
websocket: null,
//websocket状态
websocketState: false,
//重新连接次数
reconnectNum: 0,
//重连锁状态,保证重连按顺序执行
lockReconnect: false,
//定时器信息
timeout: null,
clientTimeout: null,
serverTimeout: null,
//初始化方法,根据url创建websocket对象封装基本连接方法,并重置心跳检测
initWebSocket(newUrl) {
socket.url = newUrl;
socket.websocket = new WebSocket(socket.url);
socket.websocket.onopen = socket.websocketOnOpen;
socket.websocket.onerror = socket.websocketOnError;
socket.websocket.onclose = socket.websocketOnClose;
this.resetHeartbeat()
},
reconnect() {
//判断连接状态
if (socket.lockReconnect) return;
socket.reconnectNum += 1;
//重新连接三次还未成功调用连接关闭方法
if (socket.reconnectNum === 3) {
socket.reconnectNum = 0;
socket.websocket.onclose()
return;
}
//等待本次重连完成后再进行下一次
socket.lockReconnect = true;
//5s后进行重新连接
socket.timeout = setTimeout(() => {
socket.initWebSocket(socket.url);
socket.lockReconnect = false;
}, 5000);
},
//重置心跳检测
resetHeartbeat() {
socket.heartbeat();
},
//心跳检测
heartbeat() {
socket.clientTimeout = setTimeout(() => {
if (socket.websocket) {
//向后台发送消息进行心跳检测
socket.websocket.send(JSON.stringify({ type: "heartbeat" }));
socket.websocketState = false;
//一分钟内服务器不响应则关闭连接
socket.serverTimeout = setTimeout(() => {
if (!socket.websocketState) {
socket.websocket.onclose()
} else {
this.resetHeartbeat()
}
}, 60 * 1000);
}
}, 3 * 1000);
},
//发送消息
sendMsg(message) {
socket.websocket.send(JSON.stringify({ type: "message", message: message}));
},
websocketOnOpen(event) {
//连接开启后向后台发送消息进行一次心跳检测
socket.websocket.send(JSON.stringify({ type: "heartbeat" }));
},
websocketOnError(error) {
console.log(error);
socket.reconnect();
},
websocketOnClose() {
socket.websocket.close();
},
};
3.引入websocket.js
//引入socket对象
import { socket } from "@/utils/websocket";
//引入base64生成文件工具类
import Base64StrToFileUtil from "@/utils/Base64StrToFileUtil";
4.使用
created() {
this.getMessageNumber()
//初始化websocket对象
socket.initWebSocket(process.env.VUE_APP_BASE_WS + "/" + window.localStorage.getItem("webSocketKey"));
//绑定接收消息方法
socket.websocket.onmessage = this.websocketOnMessage;
},
beforeDestroy() {
// 组件销毁时关闭链接释放资源
socket.websocketOnClose()
},
methods: {
/** 初始化连接测试*/
init() {
socket.websocketOnOpen();
},
/** 接收websocket消息*/
websocketOnMessage(event) {
//初始化界面时,会主动向后台发送一次消息,获取数据
this.websocketCount += 1;
//没有发送成功则重新初始化一次
if (this.websocketCount === 0) {
this.init();
}
let info = JSON.parse(event.data);
switch (info.type) {
case "heartbeat":
socket.websocketState = true;
break;
case "message":
this.loading = true;
this.$nextTick(() => {
// 收到文本消息
this.consumeTextMessage(info)
//收到文件消息
//this.consumeFileMessage(info);
})
break;
case "error":
this.loading = false;
break;
}
},
/** 处理推送的文本数据 */
consumeTextMessage(info) {
const h = this.$createElement;
this.$notify({
title: '新的通知',
message: h('i', { style: 'color: teal'}, info.message)
});
},
/** 处理推送的文件数据 这里发送的是xlsx文件,可根据需求自定义,也可以将文件名一起从后端传过来 */
consumeFileMessage(info) {
let infoObj = info
//服务器向客户端推送文件 生成文件
Base64StrToFileUtil.downloadFileByBase64(infoObj.message, "01.xlsx")
}
}
5、我这里发送文件是将文件转base64串的形式发送到前端,前端在生成对应文件,如果需要采用这种方式还需要添加base64转文件工具栏,代码如下:
const util = {
/** 通过base64字符串生成文件 */
downloadFileByBase64: function(base64Str, fileName) {
let myBlob = this.dataURLtoBlob(base64Str)
let myUrl = URL.createObjectURL(myBlob)
this.downloadFile(myUrl, fileName)
},
/**封装base64Str blob对象*/
dataURLtoBlob: function (base64Str) {
let bstr = atob(base64Str), n = bstr.length, u8arr = new Uint8Array(n);
alert(JSON.stringify(bstr))
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr]);
},
/**创建一个a标签,并做下载点击事件*/
downloadFile: function (hrefUrl, fileName) {
let a = document.createElement("a")
a.setAttribute("href", hrefUrl)
a.setAttribute("download", fileName)
a.setAttribute("target", "_blank")
let clickEvent = document.createEvent("MouseEvents");
clickEvent.initEvent("click", true, true);
a.dispatchEvent(clickEvent);
}
}
export default util
今天的整合就到这了,有觉得还可以的兄弟麻烦给点亮一下,谢谢!