前言
在日常开发中,我们经常需要实现实时消息推送的功能。比如新闻应用、聊天系统、监控告警等场景。这篇文章基于springboot和vue3来简单实现一个入门级的例子。
实现场景:在一个浏览器发送通知,其他所有打开的浏览器都能实时收到!

先大概介绍下sse
sse(server-sent events)是一种允许服务器向客户端推送数据的技术。与 websocket 相比,sse 更简单易用,特别适合只需要服务器向客户端单向通信的场景。
就像你订阅了某公众号,有新的文章就会主动推送给你一样。
下面来看下实际代码,完整代码都在这里。
后端实现(springboot)
控制器类 - 核心逻辑都在这里
package com.im.controller;
import lombok.extern.slf4j.slf4j;
import org.springframework.http.mediatype;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.sseemitter;
import java.io.ioexception;
import java.util.arraylist;
import java.util.list;
import java.util.concurrent.copyonwritearraylist;
@slf4j
@restcontroller
@requestmapping("/api/sse")
public class ssecontroller {
// 存储所有连接的客户端 - 关键:这个列表保存了所有浏览器的连接
private final list<sseemitter> emitters = new copyonwritearraylist<>();
/**
* 订阅sse事件 - 前端通过这个接口建立连接
* 当浏览器打开页面时,就会调用这个接口
*/
@getmapping(path = "/subscribe", produces = mediatype.text_event_stream_value)
public sseemitter subscribe() {
// 创建sse发射器,3600000毫秒=1小时超时
sseemitter emitter = new sseemitter(3600_000l);
log.info("新的sse客户端连接成功");
// 将新连接加入到客户端列表
emitters.add(emitter);
// 设置连接完成时的回调(用户关闭页面时会触发)
emitter.oncompletion(() -> {
log.info("sse客户端断开连接(正常完成)");
emitters.remove(emitter);
});
// 设置超时时的回调
emitter.ontimeout(() -> {
log.info("sse客户端断开连接(超时)");
emitters.remove(emitter);
});
// 设置错误时的回调
emitter.onerror(e -> {
log.error("sse客户端连接错误", e);
emitters.remove(emitter);
});
// 发送初始测试消息 - 告诉前端连接成功了
try {
news testnews = new news();
testnews.settitle("连接成功");
testnews.setcontent("您已成功连接到sse服务");
emitter.send(sseemitter.event()
.name("news") // 事件名称,前端根据这个来区分不同消息
.data(testnews)); // 实际发送的数据
} catch (ioexception e) {
log.error("发送初始消息失败", e);
}
return emitter;
}
/**
* 发送新闻通知 - 前端调用这个接口来发布新闻
* 这个方法会把新闻推送给所有连接的浏览器
*/
@postmapping("/send-news")
public void sendnews(@requestbody news news) {
list<sseemitter> deademitters = new arraylist<>();
log.info("开始向 {} 个客户端发送新闻: {}", emitters.size(), news.gettitle());
// 向所有客户端发送消息 - 关键:遍历所有连接
emitters.foreach(emitter -> {
try {
// 向每个客户端发送新闻数据
emitter.send(sseemitter.event()
.name("news") // 事件名称,前端监听这个事件
.data(news)); // 新闻数据
log.info("新闻发送成功到客户端");
} catch (ioexception e) {
// 发送失败,说明这个连接可能已经断开
log.error("发送新闻到客户端失败", e);
deademitters.add(emitter);
}
});
// 移除已经断开的连接,避免内存泄漏
emitters.removeall(deademitters);
log.info("清理了 {} 个无效连接", deademitters.size());
}
// 新闻数据模型 - 简单的java类,用来存储新闻数据
public static class news {
private string title; // 新闻标题
private string content; // 新闻内容
// getters and setters
public string gettitle() {
return title;
}
public void settitle(string title) {
this.title = title;
}
public string getcontent() {
return content;
}
public void setcontent(string content) {
this.content = content;
}
}
}
后端代码就这么多,代码非常的简单。
后端核心思路:
- 用一个列表
emitters保存所有浏览器的连接 - 当有新闻发布时,遍历这个列表,向每个连接发送消息
- 及时清理断开的连接,保持列表的清洁
前端实现(vue3)
1. 数据类型定义
// src/types/news.ts
// 定义新闻数据的类型
export interface news {
title: string // 新闻标题
content: string // 新闻内容
}
// 定义错误信息的类型
export interface sseerror {
message: string
}
2. sse服务类 - 封装连接逻辑
// src/utils/sseservice.ts
import type { news } from '@/types/news'
// 定义回调函数的类型
type messagecallback = (data: news) => void // 收到消息时的回调
type errorcallback = (error: event) => void // 发生错误时的回调
class sseservice {
private eventsource: eventsource | null = null // sse连接对象
private retrycount = 0 // 重试次数
private maxretries = 3 // 最大重试次数
private retrydelay = 3000 // 重试延迟(3秒)
private onmessagecallback: messagecallback | null = null // 消息回调
private onerrorcallback: errorcallback | null = null // 错误回调
/**
* 订阅sse服务 - 建立连接并设置回调函数
* @param onmessage 收到消息时的处理函数
* @param onerror 发生错误时的处理函数
*/
public subscribe(onmessage: messagecallback, onerror: errorcallback): void {
this.onmessagecallback = onmessage
this.onerrorcallback = onerror
this.connect() // 开始连接
}
/**
* 建立sse连接
*/
private connect(): void {
// 如果已有连接,先断开
if (this.eventsource) {
this.disconnect()
}
// 创建新的sse连接,连接到后端的/subscribe接口
this.eventsource = new eventsource('/api/sse/subscribe')
// 连接成功时的处理
this.eventsource.addeventlistener('open', () => {
console.log('sse连接建立成功')
this.retrycount = 0 // 连接成功后重置重试计数
})
// 监听新闻事件 - 当后端发送name为"news"的消息时会触发
this.eventsource.addeventlistener('news', (event: messageevent) => {
try {
// 解析后端发送的json数据
const data: news = json.parse(event.data)
console.log('收到新闻消息:', data)
// 调用消息回调函数,把新闻数据传递给组件
this.onmessagecallback?.(data)
} catch (error) {
console.error('解析sse消息失败:', error)
}
})
// 错误处理
this.eventsource.onerror = (error: event) => {
console.error('sse连接错误:', error)
this.onerrorcallback?.(error) // 调用错误回调
this.disconnect() // 断开连接
// 自动重连逻辑 - 网络不稳定时的自我恢复
if (this.retrycount < this.maxretries) {
this.retrycount++
console.log(`尝试重新连接 (${this.retrycount}/${this.maxretries})...`)
settimeout(() => this.connect(), this.retrydelay)
} else {
console.error('已达到最大重连次数,停止重连')
}
}
}
/**
* 取消订阅 - 组件销毁时调用
*/
public unsubscribe(): void {
this.disconnect()
this.onmessagecallback = null
this.onerrorcallback = null
}
/**
* 断开连接
*/
private disconnect(): void {
if (this.eventsource) {
this.eventsource.close() // 关闭连接
this.eventsource = null
}
}
}
// 导出单例实例,整个应用共用同一个sse服务
export default new sseservice()
3. 新闻通知组件 - 显示实时新闻
<!-- src/components/newsnotification.vue -->
<template>
<div class="news-container">
<h2>新闻通知</h2>
<!-- 连接状态 -->
<div v-if="loading" class="status loading">连接服务器中...</div>
<div v-if="error" class="status error">连接错误: {{ error }}</div>
<!-- 新闻列表 -->
<div class="news-list">
<div v-for="(news, index) in newslist" :key="index" class="news-item">
<h3>{{ news.title }}</h3>
<p>{{ news.content }}</p>
<div class="time">{{ getcurrenttime() }}</div>
</div>
</div>
<!-- 空状态 -->
<div v-if="newslist.length === 0 && !loading" class="no-news">暂无新闻通知</div>
</div>
</template>
<script lang="ts">
import { definecomponent, ref, onmounted, onunmounted } from 'vue'
import sseservice from '@/utils/sseservice'
import type { news } from '@/types/news'
export default definecomponent({
name: 'newsnotification',
setup() {
const newslist = ref<news[]>([]) // 新闻列表
const loading = ref<boolean>(true) // 加载状态
const error = ref<string | null>(null) // 错误信息
// 处理新消息
const handlenewmessage = (news: news): void => {
newslist.value.unshift(news) // 新消息放在最前面
}
// 处理错误
const handleerror = (err: event): void => {
error.value = (err as errorevent)?.message || '连接服务器失败'
loading.value = false
}
// 获取当前时间
const getcurrenttime = (): string => {
return new date().tolocaletimestring()
}
// 组件挂载时建立连接
onmounted(() => {
sseservice.subscribe(handlenewmessage, handleerror)
loading.value = false
})
// 组件销毁时断开连接
onunmounted(() => {
sseservice.unsubscribe()
})
return {
newslist,
loading,
error,
getcurrenttime,
}
},
})
</script>
<style scoped>
.news-container {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
text-align: center;
}
.loading {
background-color: #e3f2fd;
color: #1976d2;
}
.error {
background-color: #ffebee;
color: #d32f2f;
}
.news-list {
margin-top: 20px;
}
.news-item {
padding: 15px;
margin-bottom: 10px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fafafa;
}
.news-item h3 {
margin: 0 0 8px 0;
color: #333;
font-size: 16px;
}
.news-item p {
margin: 0 0 8px 0;
color: #666;
line-height: 1.4;
}
.time {
font-size: 12px;
color: #999;
text-align: right;
}
.no-news {
padding: 40px 20px;
text-align: center;
color: #999;
font-style: italic;
}
</style>
4. 新闻发送组件 - 管理员发送新闻
<!-- src/components/sendnewsform.vue -->
<template>
<div class="send-news-form">
<h2>发送新闻通知</h2>
<form @submit.prevent="sendnews">
<div class="form-group">
<label for="title">标题</label>
<input id="title" v-model="news.title" type="text" required placeholder="输入新闻标题" />
</div>
<div class="form-group">
<label for="content">内容</label>
<textarea
id="content"
v-model="news.content"
required
placeholder="输入新闻内容"
rows="4"
></textarea>
</div>
<button type="submit" :disabled="issending">
{{ issending ? '发送中...' : '发送新闻' }}
</button>
<!-- 操作反馈 -->
<div v-if="message" class="message" :class="messagetype">
{{ message }}
</div>
</form>
</div>
</template>
<script lang="ts">
import { definecomponent, ref, computed } from 'vue'
import type { news } from '@/types/news'
export default definecomponent({
name: 'sendnewsform',
setup() {
// 表单数据
const news = ref<news>({
title: '',
content: '',
})
// 界面状态
const issending = ref<boolean>(false)
const message = ref<string>('')
const issuccess = ref<boolean>(false)
// 消息类型样式
const messagetype = computed(() => {
return issuccess.value ? 'success' : 'error'
})
// 发送新闻
const sendnews = async (): promise<void> => {
issending.value = true
message.value = ''
try {
const response = await fetch('/api/sse/send-news', {
method: 'post',
headers: {
'content-type': 'application/json',
},
body: json.stringify(news.value),
})
if (response.ok) {
message.value = '新闻发送成功!'
issuccess.value = true
// 清空表单
news.value = { title: '', content: '' }
} else {
throw new error('发送失败')
}
} catch (err) {
message.value = '发送新闻失败'
issuccess.value = false
console.error(err)
} finally {
issending.value = false
}
}
return {
news,
issending,
message,
messagetype,
sendnews,
}
},
})
</script>
<style scoped>
.send-news-form {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
input,
textarea {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
font-size: 14px;
}
input:focus,
textarea:focus {
outline: none;
border-color: #1976d2;
}
textarea {
resize: vertical;
min-height: 80px;
}
button {
width: 100%;
background-color: #1976d2;
color: white;
padding: 10px 15px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
}
button:hover:not(:disabled) {
background-color: #1565c0;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.message {
margin-top: 15px;
padding: 10px;
border-radius: 4px;
text-align: center;
font-size: 14px;
}
.success {
background-color: #e8f5e8;
color: #2e7d32;
border: 1px solid #c8e6c9;
}
.error {
background-color: #ffebee;
color: #c62828;
border: 1px solid #ffcdd2;
}
</style>
5. 主页面引用组件
<!-- src/home.vue -->
<template>
<div class="welcome">
<sendnewsform />
<newsnotification />
</div>
</template>
<script lang="ts">
import { definecomponent } from 'vue'
import sendnewsform from './components/sendnewsform.vue'
import newsnotification from './components/newsnotification.vue'
</script>
全部代码和注释都在这里,可以直接启动项目测试了。
测试
1. 测试多浏览器接收
- 打开第一个浏览器(比如 chrome)访问应用
- 打开第二个浏览器(比如 firefox)访问应用
- 在任意浏览器中使用发送表单发布新闻
- 观察两个浏览器是否都实时收到了新闻通知
2. 预期效果
- 第一个浏览器打开:显示"连接成功"
- 第二个浏览器打开:显示"连接成功"
- 在任意浏览器发送新闻:两个浏览器都立即显示新新闻
- 关闭一个浏览器:不影响另一个浏览器的正常使用
总结
通过这个小案例的源码,我们学会了sse的简单使用:
- sse 的基本原理和使用方法
- springboot 如何维护多个客户端连接
- vue3 组合式 api 的使用
- 前后端分离架构的实时通信实现
这个方案非常适合新闻推送、系统通知、实时数据展示等场景。代码简单易懂,扩展性强,你也可以基于这个基础添加更多功能。
到此这篇关于springboot+vue3整合sse实现实时消息推送功能的文章就介绍到这了,更多相关springboot实时消息推送内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论