SEE是一种比WebSocket更加轻量级的后端通知前端的方法,由于项目中有一个需要在前端展示的图片会发生变化所以需要前端能够同步这些变化,如果是通过传统的轮询效率不高而且还会导致前端网页卡顿的情况,为了快速的实现这个效果,我找到了通过SEE通信的方法,以图像发生变化后通知后端再有后端通知前端需要前端重新获取图片信息,这样完成了这一需求。话不多说我们来看代码实现。
*注:经目前了解SEE只能传输文本,但是好像可以通过转码等操作实现二进制文件的传输,后续在做了解
首先我们声明一个SEE的控制类Controller
/**
* 测试SSE推送消息
*/
@RestController
@RequestMapping(path = "/see")
public class SEEController {
private static Map<String, SseEmitter> sseCache = new ConcurrentHashMap<>();
@GetMapping(path = "subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter push(String id, HttpServletResponse response) throws IOException {
System.out.println("连接客户端");
//设置超时时间30s(若在30s内无信息的推送则发出超时警告,故在后续需要添加定时器定时发送通知避免超时,
// 也可以确保客户端一直处在连接中)
SseEmitter sseEmitter = new SseEmitter(30000L);
//发送连接成功至前端页面
sseEmitter.send(SseEmitter.event().reconnectTime(1000).data("连接成功"));
response.setContentType("text/event-stream");
//一般都是这个编码,可以防止中文乱码
response.setCharacterEncoding("UTF-8");
sseCache.put(id, sseEmitter);
System.out.println("add " + id);
//超时处理
sseEmitter.onTimeout(() -> {
System.out.println(id + "超时");
sseCache.remove(id);
});
//连接完成,即客户端订阅完成
sseEmitter.onCompletion(() -> System.out.println("完成!!!"));
return sseEmitter;
}
@GetMapping(path = "push")
public String push(String id, String content) throws IOException {
SseEmitter sseEmitter = sseCache.get(id);
if (sseEmitter != null) {
sseEmitter.send(SseEmitter.event().name("msg").data("后端发送消息:" + content));
}
return "over";
}
@GetMapping(path = "over")
public String over(String id) {
SseEmitter sseEmitter = sseCache.get(id);
if (sseEmitter != null) {
sseEmitter.complete();
sseCache.remove(id);
}
return "over";
}
@GetMapping(path = "update")
public String updateSubscribers() {
// 在这里通知所有订阅了本频道的客户端
for (SseEmitter sseEmitter : sseCache.values()) {
try {
System.out.println("图片有所更改通知客户端");
// 向客户端发送通知
sseEmitter.send(SseEmitter.event().name("update").data("有新的更新"));
} catch (IOException e) {
// 处理异常
e.printStackTrace();
}
}
return "update sent";
}
}
接下来为了解决超时报错的问题,我们在同一目录下加上一个定时发送通知以保证在超时时间内保持通知的发送
@Component
public class SseScheduledTask {
private Map<String, SseEmitter> sseCache;
@Autowired
public SseScheduledTask(Map<String, SseEmitter> sseCache) {
this.sseCache = sseCache;
}
// 每隔一定时间执行一次任务
@Scheduled(fixedRate = 10000) // 每隔10秒发送一次事件
public void sendKeepAliveEvent() {
for (SseEmitter sseEmitter : sseCache.values()) {
try {
System.out.println("执行了保持连接的定时事件");
// 向客户端发送保持连接的事件
sseEmitter.send(SseEmitter.event().name("keep-alive").data("保持连接"));
} catch (IOException e) {
// 处理异常
e.printStackTrace();
}
}
}
}
再在这个过程中可能一下语句会报错
private Map<String, SseEmitter> sseCache;
@Autowired
public SseScheduledTask(Map<String, SseEmitter> sseCache) {
this.sseCache = sseCache;
}
报错内容是无法自动装配SseEmitter或Map<String, SseEmitter>的Bean,解决这个问题只需要在你定义的config文件夹(你专门用来放@Configuration类的地方)新建一个配置类SseEmitterConfig,将Bean手动添加进去即可
@Configuration
@ComponentScan("com.cloud.config.see")
public class SseEmitterConfig {
@Bean
public SseEmitter sseEmitter() {
return new SseEmitter();
}
// 如果需要Map<String, SseEmitter>类型的Bean,也可以手动定义
@Bean
public Map<String, SseEmitter> sseEmitterMap() {
return new ConcurrentHashMap<>();
}
}
此时即完成了后端的所有工作
那么下面演示测试方法
在浏览器中键入以下网址判断是否连接成功
http://localhost:8080/see/subscribe?id=1
若出现以上内容即连接成功
以下是我在项目中的部分代码提供参考
<template>
<div class="dataScreen-main-chart" style="width: 100%; height: 100%">
<span style="display: inline-block; width: 100%">
<span class="subWindowTitle" style="margin-left: 36%">对象识别</span>
<button class="reset-button" @click="resetCanvas" style="margin-left: 15%">画面置中</button>
<button class="reset-button" @click="update">更新</button>
</span>
<canvas width="800" height="400" id="canvas"></canvas>
</div>
</template>
<script setup lang="ts" name="Canvas">
import { onBeforeUnmount, onMounted, ref } from "vue";
import { fabric } from "fabric";
import { getImage, updateImage } from "@/api/modules/user";
import { useUserStore } from "@/stores/modules/user";
let canvas: {
width: any;
height: any;
setBackgroundImage: (arg0: any, arg1: any) => void;
renderAll: { (): void; (): void; bind: any };
setZoom: (arg0: number) => void;
add: (arg0: any) => void;
on: (arg0: string, arg1: (opt: any) => void) => void;
getZoom: () => any;
zoomToPoint: (arg0: { x: any; y: any }, arg1: any) => void;
setViewportTransform: (arg0: number[]) => void;
}; // 将 canvas 变量移出 init 函数,使其在整个组件中可用
const initialZoom = 1.065; // 初始缩放比例
// 定义点的数组
let points = [
{ x: 322.74603174603175, y: 472.19047619047615 },
{ x: 448.14285714285717, y: 530.9206349206349 },
{ x: 478.3015873015873, y: 478.53968253968253 },
{ x: 641.7936507936508, y: 524.5714285714286 },
{ x: 756.0793650793651, y: 353.1428571428571 },
{ x: 924.3333333333334, y: 115.04761904761904 },
{ x: 1000.5238095238095, y: 8.69841269841271 },
{ x: 1137.031746031746, y: 102.34920634920633 },
{ x: 1348.142857142857, y: 267.42857142857144 },
{ x: 1538.6190476190477, y: 421.3968253968254 },
{ x: 1771.952380952381, y: 626.1587301587301 },
{ x: 1648.142857142857, y: 830.9206349206349 },
{ x: 1544.968253968254, y: 978.5396825396826 },
{ x: 1254.4920634920634, y: 980.1269841269841 },
{ x: 875.1269841269841, y: 961.0793650793651 },
{ x: 481.4761904761905, y: 922.984126984127 },
{ x: 248.14285714285717, y: 889.6507936507937 },
{ x: 84.65079365079364, y: 859.4920634920634 },
{ x: 165.6031746031746, y: 696.0 },
{ x: 235.44444444444443, y: 549.968253968254 },
{ x: 264.015873015873, y: 545.2063492063492 }
];
for (let i = 0; i < points.length; i++) {
points[i].x *= 0.9;
points[i].y *= 0.9;
}
// const responseData = ref();
const responseURL = ref("public/cg_mask/augmented_image_1.jpg");
// let intervalId: string | number | NodeJS.Timeout | null | undefined = null;
// const fetchData = async () => {
// try {
// const response = await getPoints();
// responseData.value = response.data; // 将响应数据保存在 responseData 中
// } catch (error) {
// console.error("请求数据时出错:", error);
// }
// };
// //开始请求
// const startFetchingData = () => {
// // 每隔 0.01 秒调用一次 fetchData 方法
// intervalId = setInterval(fetchData, 10);
// };
// //停止请求
// const stopFetchingData = () => {
// // 停止定时调用 fetchData 方法
// if (intervalId !== null) {
// clearInterval(intervalId);
// intervalId = null;
// }
// };
//请求图片
const update = async () => {
updateImage();
};
const loadCanvasBackground = () => {
// 添加一个随机参数以防止缓存
const imageUrl = `public/moment.jpg?${Math.random()}`;
fabric.Image.fromURL(
imageUrl,
(img: { scaleToWidth: (arg0: any) => void; scaleToHeight: (arg0: any) => void }) => {
img.scaleToWidth(canvas.width);
img.scaleToHeight(canvas.height);
console.log("canvas.width: " + canvas.width);
console.log("canvas.height: " + canvas.height);
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
},
{ crossOrigin: "anonymous" }
);
};
// 使用画布
const init = () => {
// 创建画布
canvas = new fabric.Canvas("canvas");
// 设置初始显示坐标
canvas.setViewportTransform([1, 0, 0, 1, 20, -10]); // 设置初始显示坐标为 (-100, -100)
// // 初始化多边形
// const polygonPlane = new fabric.Polygon(
// [
// { x: 20, y: 150 },
// { x: 20, y: 280 },
// { x: 20, y: 380 },
// { x: 840, y: 380 },
// { x: 840, y: 20 }
// ],
// {
// fill: "#ffd3b6", // 填充色
// stroke: "#6639a6", // 线段颜色
// strokeWidth: 5
// }
// );
// 生成 SVG 路径字符串
let pathData = "M " + points[0].x + " " + points[0].y;
for (let i = 1; i < points.length - 1; i += 2) {
pathData += " Q " + points[i].x + " " + points[i].y + " " + points[i + 1].x + " " + points[i + 1].y;
}
// 创建曲线
const path = new fabric.Path(pathData, {
fill: "#ffd3b6",
stroke: "#6639a6",
strokeWidth: 5
});
// 创建三个圆形对象
const sensor1 = new fabric.Circle({
top: 50,
left: 50,
radius: 10,
fill: "green",
stroke: "#6639a6",
strokeWidth: 1
});
const sensor2 = new fabric.Circle({
top: 180,
left: 50,
radius: 10,
fill: "green",
stroke: "#6639a6",
strokeWidth: 1
});
const sensor3 = new fabric.Circle({
top: 50,
left: 100,
radius: 10,
fill: "green",
stroke: "#6639a6",
strokeWidth: 1
});
const sensor4 = new fabric.Circle({
top: 180,
left: 100,
radius: 10,
fill: "green",
stroke: "#6639a6",
strokeWidth: 1
});
const rect1 = new fabric.Rect({
top: 57,
left: 60,
width: 50,
height: 5,
fill: "blue"
});
const rect2 = new fabric.Rect({
top: 187,
left: 60,
width: 50,
height: 5,
fill: "blue"
});
const rect3 = new fabric.Rect({
top: 57,
left: 81,
width: 8,
height: 130,
fill: "blue"
});
const rect4 = new fabric.Rect({
top: 121,
left: -83,
width: 170,
height: 8,
fill: "blue"
});
// 禁止选取
path.selectable = false;
// 禁止缩放
sensor1.hasControls = false;
sensor2.hasControls = false;
// 设置初始缩放比例
canvas.setZoom(initialZoom);
const group = new fabric.Group([rect1, rect2, rect3, rect4, sensor1, sensor2, sensor3, sensor4], {
left: 200,
top: 100,
selectable: true // 设置为可选,以允许拖动
});
// canvas.add(polygonPlane);
// 将曲线添加到画布上
// canvas.add(path);
// 将图形居中
path.centerH();
path.centerV();
canvas.add(group);
// 创建一个组并添加到画布上
// 将图形居中
group.centerH();
group.centerV();
canvas.add(group);
// 设置缩放
// 监听鼠标事件
canvas.on("mouse:wheel", opt => {
let delta = opt.e.deltaY;
let zoom = canvas.getZoom();
zoom *= 0.999 ** delta;
// 限制zoom的缩放倍数
if (zoom > 20) zoom = 20;
if (zoom < 0.01) zoom = 0.01;
canvas.zoomToPoint(
{
x: opt.e.offsetX,
y: opt.e.offsetY
},
zoom
);
});
};
const resetCanvas = () => {
canvas.setViewportTransform([1, 0, 0, 1, 20, -10]); // 重置视口转换
canvas.setZoom(initialZoom); // 重置缩放级别
canvas.renderAll(); // 重新渲染画布
};
const receivedMessage = ref("");
let eventSource: EventSource;
onMounted(() => {
const userStore = useUserStore();
// 连接SSE
eventSource = new EventSource("http://localhost:8080/see/subscribe?id=" + userStore.getUserID());
// 处理SSE接收到消息事件
eventSource.addEventListener("message", event => {
console.log("Received SSE message:", event.data);
// 在这里处理收到的消息,例如更新组件的状态
receivedMessage.value = event.data;
// 执行相应的事件,例如触发某个函数
handleReceivedMessage();
});
// 处理SSE连接打开事件
eventSource.addEventListener("open", event => {
console.log("SSE Connection opened:", event);
});
// 处理SSE连接错误事件
eventSource.addEventListener("error", event => {
console.error("SSE Connection error:", event);
});
// 处理"update"事件
eventSource.addEventListener("update", event => {
console.log("Received SSE update event:", event.data);
// 在这里执行处理"update"事件的逻辑,例如触发某个函数
handleUpdateEvent();
});
init();
});
onBeforeUnmount(() => {
// 在组件销毁前关闭SSE连接
if (eventSource) {
eventSource.close();
}
});
const handleReceivedMessage = async () => {
// 在这里执行收到消息后的操作,可以是触发某个函数或更新组件的状态
console.log("Handling received message:", receivedMessage.value);
try {
const response = await getImage();
responseURL.value = response.data.url;
loadCanvasBackground();
} catch (error) {
console.error("请求图片时出错:", error);
}
};
const handleUpdateEvent = async () => {
// 在这里执行处理"update"事件的逻辑
console.log("Handling update event");
// 执行你的更新操作
loadCanvasBackground();
};
</script>
<style>
@import "../index.scss";
.reset-button {
background-color: #4caf50; /* Green */
border: none;
color: white;
padding: 10px 20px; /* 小一点的内边距 */
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 14px; /* 小一点的字体大小 */
margin: 4px 2px;
cursor: pointer;
transition-duration: 0.4s;
border-radius: 12px; /* 圆角 */
margin-right: 5px;
margin-left: 20px;
width: auto;
height: 40px;
}
.reset-button:hover {
background-color: #45a049;
}
</style>