SSE技术是基于单工通信模式,只是单纯的客户端向服务端发送请求,服务端不会主动发送给客户端。服务端采取的策略是抓住这个请求不放,等数据更新的时候才返回给客户端,当客户端接收到消息后,再向服务端发送请求,周而复始。
注意:因为EventSource对象是SSE的客户端,可能会有浏览器对其不支持
是 HTML5 遵循 W3C 标准提出的客户端和服务端之间进行实时通信的协议。
优点
缺点
是 HTML5 的一部分,提供了一种双向通信的机制。
优点
缺点
// 建立连接
createSseConnect(clientId){
if(window.EventSource){
const eventSource = new EventSource('http://127.0.0.1:8083/sse/createSseConnect?clientId='+clientId);
console.log(eventSource)
eventSource.onmessage = (event) =>{
console.log("onmessage:"+clientId+": "+event.data)
};
eventSource.onopen = (event) =>{
console.log("onopen:"+clientId+": "+event)
};
eventSource.onerror = (event) =>{
console.log("onerror :"+clientId+": "+event)
};
eventSource.close = (event) =>{
console.log("close :"+clientId+": "+event)
};
}else{
console.log("你的浏览器不支持SSE~")
}
console.log(" 测试 打印")
},
SseController
package com.joker.cloud.linserver.controller;
import com.joker.cloud.linserver.conf.sse.sseUtils;
import com.joker.common.message.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Map;
/**
* SseController
*
* @author joker
* @version 1.0
* 2023/8/9 11:18
**/
@RestController
@Slf4j
@CrossOrigin
@RequestMapping("/sse")
public class SseController {
@Autowired
private sseUtils sseUtils;
@GetMapping(value = "/createSseConnect", produces="text/event-stream;charset=UTF-8")
public SseEmitter createSseConnect(@RequestParam(name = "clientId", required = false) Long clientId) {
return sseUtils.connect(clientId);
}
@PostMapping("/sendMessage")
public void sendMessage(@RequestParam("clientId") Long clientId, @RequestParam("message") String message){
sseUtils.sendMessage(clientId, "123456789", message);
}
@GetMapping(value = "/listSseConnect")
public Result
sseUtils工具类
package com.joker.cloud.linserver.conf.sse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* sseUtils
*
* @author joker
* @version 1.0
* 2023/8/9 11:20
**/
@Slf4j
@Component
public class sseUtils {
private static final Map sseEmitterMap = new ConcurrentHashMap<>();
/**
* 创建连接
*/
public SseEmitter connect(Long userId) {
if (sseEmitterMap.containsKey(userId)) {
SseEmitter sseEmitter =sseEmitterMap.get(userId);
sseEmitterMap.remove(userId);
sseEmitter.complete();
}
try {
UUID uuid = UUID.randomUUID();
String str = uuid.toString();
String temp = str.substring(0, 8) + str.substring(9, 13) + str.substring(14, 18) + str.substring(19, 23) + str.substring(24);
// 设置超时时间,0表示不过期。默认30秒
SseEmitter sseEmitter = new SseEmitter(30*1000L);
sseEmitter.send(SseEmitter.event().id(temp).data(""));
// reconnectTime(10*1000L)
// 注册回调
sseEmitter.onCompletion(completionCallBack(userId));
// sseEmitter.completeWithError(errorCallBack(userId));
sseEmitter.onTimeout(timeoutCallBack(userId));
sseEmitterMap.put(userId, sseEmitter);
log.info("创建sse连接完成,当前用户:{}", userId);
return sseEmitter;
} catch (Exception e) {
log.info("创建sse连接异常,当前用户:{}", userId);
}
return null;
}
/**
* 给指定用户发送消息
*
*/
public boolean sendMessage(Long userId,String messageId, String message) {
if (sseEmitterMap.containsKey(userId)) {
SseEmitter sseEmitter = sseEmitterMap.get(userId);
try {
sseEmitter.send(SseEmitter.event().id(messageId).data(message));
// reconnectTime(10*1000L)
log.info("用户{},消息id:{},推送成功:{}", userId,messageId, message);
return true;
}catch (Exception e) {
sseEmitterMap.remove(userId);
log.info("用户{},消息id:{},推送异常:{}", userId,messageId, e.getMessage());
sseEmitter.complete();
return false;
}
}else {
log.info("用户{}未上线", userId);
}
return false;
}
/**
* 删除连接
* @param userId
*/
public void deleteUser(Long userId){
removeUser(userId);
}
private static Runnable completionCallBack(Long userId) {
return () -> {
log.info("结束sse用户连接:{}", userId);
removeUser(userId);
};
}
private static Throwable errorCallBack(Long userId) {
log.info("sse用户连接异常:{}", userId);
removeUser(userId);
return new Throwable();
}
private static Runnable timeoutCallBack(Long userId) {
return () -> {
log.info("连接sse用户超时:{}", userId);
removeUser(userId);
};
}
/**
* 断开
* @param userId
*/
public static void removeUser(Long userId){
if (sseEmitterMap.containsKey(userId)) {
SseEmitter sseEmitter = sseEmitterMap.get(userId);
sseEmitterMap.remove(userId);
sseEmitter.complete();
}else {
log.info("用户{} 连接已关闭",userId);
}
}
public Map listSseConnect(){
return sseEmitterMap;
}
}
模拟浏览器发送建立连接的请求:
切换到时间栏目,可以看到长连接始终保持着的:
切换到eventStream:可以看到后端通信的streams流数据
使用postMan 模拟后端服务器推送给客户端消息
浏览器建立的连接中会看到服务器推送到客户端的消息内容及ID等基础信息
控制台也可以监听到事件的变化并输出