Springboot使用SSE推送消息到客戶端的實(shí)現(xiàn)
1、場景:
服務(wù)端主動推動消息到客戶端(Electron 桌面應(yīng)用)
普通的HTTP/HTTPS請求要先客戶端發(fā)送請求,然后服務(wù)端才能返回結(jié)果
服務(wù)端主動推送數(shù)據(jù)到客戶端,有兩種方案:
SSE:只能從服務(wù)端主動推送數(shù)據(jù)到客戶端(單向),客戶端發(fā)送數(shù)據(jù)還是要使用HTTP/HTTPS請求
Socket 通信:客戶端和服務(wù)端都可以發(fā)送數(shù)據(jù)給對方(雙向)
先記錄SSE的配置(我的服務(wù)端之前是用nodejs express 寫的,已經(jīng)使用 socket.io與客戶端適配好了,現(xiàn)在用Springboot重構(gòu))
2、實(shí)現(xiàn):
1、springboot 配置:
1、先實(shí)現(xiàn)Server層,方便其他控制器調(diào)用
package com.xxx.controller;
import com.qiang.service.SseService;
import com.qiang.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@RestController
@RequestMapping("/sse")
public class SseController {
// 注入 SSE 服務(wù)類
@Autowired
private SseService sseService;
@Autowired
private UserService userService;
/**
* 建立 SSE 連接(調(diào)用 Service 的注冊方法)
*/
@GetMapping("/connect")
public SseEmitter connect(@RequestParam String clientId) {
// 查詢會話成員表,userId通過客戶端id獲取該用戶所有的群聊
String userId = clientId.split("_")[2].trim();
List<String> groupIdList = userService.getUserGroupChat(userId);
Set<String> groupIdSet = new HashSet<>();
if (groupIdList != null && !groupIdList.isEmpty()) {
groupIdSet = new HashSet<>(groupIdList); // List → Set,自動去重
}
return sseService.registerClient(clientId, groupIdSet);
}
/**
* 手動推送(調(diào)用 Service 的推送方法)
*/
@PostMapping("/push")
public String push(@RequestBody Map<String, String> params) {
String clientId = params.get("clientId");
String message = params.get("message");
return sseService.sendMessage(clientId, "business", message);
}
/**
* 獲取在線客戶端數(shù)量
*/
@GetMapping("/count")
public String getClientCount() {
return "當(dāng)前在線客戶端數(shù)量:" + sseService.getConnectedClientCount();
}
}2、再實(shí)現(xiàn)控制層:
package com.xxx.controller;
import com.qiang.service.SseService;
import com.qiang.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@RestController
@RequestMapping("/sse")
public class SseController {
// 注入 SSE 服務(wù)類
@Autowired
private SseService sseService;
@Autowired
private UserService userService;
/**
* 建立 SSE 連接(調(diào)用 Service 的注冊方法)
*/
@GetMapping("/connect")
public SseEmitter connect(@RequestParam String clientId) {
// 查詢會話成員表,userId通過客戶端id獲取該用戶所有的群聊
String userId = clientId.split("_")[2].trim();
List<String> groupIdList = userService.getUserGroupChat(userId);
Set<String> groupIdSet = new HashSet<>();
if (groupIdList != null && !groupIdList.isEmpty()) {
groupIdSet = new HashSet<>(groupIdList); // List → Set,自動去重
}
return sseService.registerClient(clientId, groupIdSet);
}
/**
* 手動推送(調(diào)用 Service 的推送方法)
*/
@PostMapping("/push")
public String push(@RequestBody Map<String, String> params) {
String clientId = params.get("clientId");
String message = params.get("message");
return sseService.sendMessage(clientId, "business", message);
}
/**
* 獲取在線客戶端數(shù)量
*/
@GetMapping("/count")
public String getClientCount() {
return "當(dāng)前在線客戶端數(shù)量:" + sseService.getConnectedClientCount();
}
}2、客戶端(Electron 配置):
eventsource 是瀏覽器對象
我是在客戶端主進(jìn)程中接收消息的,要下載依賴。如果是在渲染進(jìn)程或者是普通前端使用則不需要下載依賴
"eventsource": "^2.0.2"
1、工具函數(shù):
// src/util/sseClient.js(主進(jìn)程專用)
const EventSource = require('eventsource');
class SseClient {
constructor(clientId) {
this.clientId = clientId;
this.isConnected = false; // 標(biāo)記是否真正連接成功
this.initSse();
}
initSse() {
this.close(); // 關(guān)閉舊連接
const sseUrl = `http://localhost:8088/sse/connect?clientId=${this.clientId}`;
console.log(`[SSE] 嘗試連接:${sseUrl}`);
this.es = new EventSource(sseUrl);
// 1. 連接成功(標(biāo)記真正的連接狀態(tài))
this.es.onopen = () => {
this.isConnected = true;
console.log('[SSE] 連接成功');
};
// 2. 優(yōu)化錯誤處理(過濾無害錯誤)
this.es.onerror = (e) => {
// 過濾:連接成功前的無消息錯誤(無害)
if (!this.isConnected && e.message === undefined) {
console.log('[SSE] 初始化階段臨時錯誤(無害):', e.type);
return; // 不打印錯誤,避免干擾
}
// 真正的錯誤(連接斷開/失?。?
this.isConnected = false;
console.error('[SSE] 真正的連接錯誤:', {
type: e.type,
message: e.message || '未知錯誤',
readyState: this.es.readyState // 0:連接中, 1:已連接, 2:已關(guān)閉
});
// 僅在連接關(guān)閉時重連
if (this.es.readyState === EventSource.CLOSED) {
console.log(`[SSE] 3秒后嘗試重連...`);
setTimeout(() => this.initSse(), 3000);
}
};
// 3. 正常接收消息
this.es.onmessage = (e) => {
try {
const cleanData = e.data.replace(/^data: /, '').trim();
const messageObj = JSON.parse(cleanData);
// 打印解析結(jié)果(驗(yàn)證)
console.log('[SSE] 解析后的完整對象:', messageObj);
} catch (err) {
// 解析失敗時的容錯
console.warn('[SSE] 解析失敗,原始數(shù)據(jù):', e.data);
console.error('[SSE] 解析錯誤詳情:', err);
}
};
// 4. 監(jiān)聽自定義事件(如 notification/business)
this.es.addEventListener('notification', (e) => {
const data = JSON.parse(e.data);
console.log('[SSE] 通知消息:', data);
});
}
close() {
if (this.es) {
this.es.close();
this.es = null;
this.isConnected = false;
}
}
// 手動推送(修復(fù)后的 POST 版本)
async triggerPush(message) {
if (!this.isConnected) {
console.warn('[SSE] 未連接,無法推送');
return null;
}
try {
const response = await fetch(`${this.serverUrl || 'http://localhost:8088'}/sse/push`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clientId: this.clientId, message })
});
const result = await response.text();
console.log('[SSE] 手動推送結(jié)果:', result);
return result;
} catch (err) {
console.error('[SSE] 手動推送失?。?, err);
return null;
}
}
}
module.exports = SseClient;2、調(diào)用:
// clientId 要唯一,否則服務(wù)端推送消息時會有影響
new SseClient("自定義的clientId");
到此這篇關(guān)于Springboot使用SSE推送消息到客戶端的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Springboot SSE推送內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot+dubbo+validation 進(jìn)行rpc參數(shù)校驗(yàn)的實(shí)現(xiàn)方法
這篇文章主要介紹了springboot+dubbo+validation 進(jìn)行rpc參數(shù)校驗(yàn)的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09
四步輕松搞定java web每天定時執(zhí)行任務(wù)
本篇文章主要介紹了四步輕松搞定java web每天定時執(zhí)行任務(wù),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-01-01
SpringBoot中的application.properties無法加載問題定位技巧
這篇文章主要介紹了SpringBoot中的application.properties無法加載問題定位技巧,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05
基于SpringBoot實(shí)現(xiàn)多文件批量下載并打包為ZIP壓縮包的完整解決方案
在日常的 Web 開發(fā)中,文件下載是非常常見的功能需求,而多文件批量下載并打包為 ZIP 壓縮包 更是高頻場景(比如批量下載合同、圖片、報表等),本文將基于 SpringBoot 框架,手把手教你實(shí)現(xiàn)這一功能,從核心思路到完整代碼,讓你快速掌握,需要的朋友可以參考下2026-02-02
java實(shí)現(xiàn)批量導(dǎo)入Excel表格數(shù)據(jù)到數(shù)據(jù)庫
這篇文章主要為大家詳細(xì)介紹了java實(shí)現(xiàn)批量導(dǎo)入Excel表格數(shù)據(jù)到數(shù)據(jù)庫,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-08-08

