什么是SSE ?
SSE(Server-Sent Events)是一种用于实现服务器主动向客户端推送数据的技术,也被称为“事件流”(Event Stream)。它基于 HTTP 协议,利用了其长连接特性,在客户端与服务器之间建立一条持久化连接,并通过这条连接实现服务器向客户端的实时数据推送。
SSE 的优点和适用场景
- 简单易用: SSE 协议相对于 WebSocket 更简单,实现起来更加轻量级,不需要复杂的握手过程。
- 实时性: SSE 适合需要实时推送数据的场景,如即时通讯、股票市场报价、实时数据监控等。
- 基于标准: SSE 是基于 HTTP 的标准协议,与现有的 Web 技术兼容性良好。
SSE 的实现步骤
-
服务端实现: 使用支持 SSE 的服务器端技术(如Node.js的express框架),在 HTTP 头部添加特定的 Content-Type(text/event-stream)和其他 SSE 相关字段,定期向客户端发送数据。
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }); res.write('data: Hello\n\n'); // 发送数据给客户端 -
客户端实现: 使用 JavaScript 创建一个 EventSource 对象,监听从服务器发送的事件。
var eventSource = new EventSource('/sse-endpoint'); eventSource.onmessage = function(event) { console.log('Received event: ', event.data); // 处理接收到的数据 };
4. SSE 的局限性
- 单向通信: SSE 是服务器向客户端的单向通信,客户端无法向服务器端发送数据,因此不适合需要双向通信的应用。
- 兼容性: 虽然现代浏览器普遍支持 SSE,但是在一些旧版本浏览器和移动设备上的支持可能有限。
5. 基于springboot 实现 SSE
添加依赖
在 pom.xml(如果是 Maven 项目)中添加 Spring Web 相关依赖,确保项目能够支持 Web 开发:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
创建 SSE 示例
创建一个操作类,用于处理 SSE 请求并向客户端发送事件流。
@Service
public class SseEmitterServiceImpl implements SseEmitterService {
private final Logger log = LoggerFactory.getLogger(SseEmitterServiceImpl.class);
/**
* 使用map对象,便于根据userId来获取对应的SseEmitter
*/
private final Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();
public SseEmitter connect(String userId) {
if(sseEmitterMap.containsKey(userId)){
return sseEmitterMap.get(userId);
}
// 设置超时时间,0表示不过期。默认30S,超时时间未完成会抛出异常:AsyncRequestTimeoutException
SseEmitter sseEmitter = new SseEmitter(5*60*1000L);
// 注册回调
sseEmitter.onCompletion(completionCallBack(userId));
sseEmitter.onError(infoCallBack(userId));
sseEmitter.onTimeout(timeoutCallBack(userId));
sseEmitterMap.put(userId, sseEmitter);
log.info("创建新的 SSE 连接,当前用户 {}, 连接总数 {}", userId, sseEmitterMap.size());
return sseEmitter;
}
/**
* 给制定用户发送消息
*
* @param userId 指定用户名
* @param message 消息
*/
@Override
public void sendMessage(String userId, String message) {
if(sseEmitterMap.containsKey(userId)){
try {
SseEmitter sseEmitter = sseEmitterMap.get(userId);
sseEmitter.send(message);
}catch (Exception e){
removeUser(userId);
}
}
}
/**
* 查询当前用户是否在线
* @return
*/
@Override
public boolean isOnline(String userId) {
return sseEmitterMap.entrySet().stream()
.anyMatch(entry -> entry.getKey().startsWith(userId + ":"));
}
/**
* 群发消息
*/
public void batchSendMessage(String message, List<String> ids) {
ids.forEach(userId -> sendMessage(userId, message));
}
/**
* 群发所有人
*/
public void batchSendMessage(String message) {
sseEmitterMap.forEach((k, v) -> {
try {
v.send(message, MediaType.APPLICATION_JSON);
} catch (IOException e) {
log.info("用户 {} 推送异常", k, e);
removeUser(k);
}
});
}
/**
* 移除用户连接
*
* @param userId 用户 ID
*/
public void removeUser(String userId) {
try {
if (sseEmitterMap.containsKey(userId)) {
SseEmitter sseEmitter = sseEmitterMap.get(userId);
if(sseEmitter!=null){
sseEmitter.complete();
sseEmitterMap.remove(userId);
}else {
log.info("用户连接不存在");
}
}
}catch (Exception e){
log.info("用户 {} 推送异常",userId);
}
}
/**
* 获取当前连接信息
*
* @return 所有的连接用户
*/
public List<String> getIds() {
return new ArrayList<>(sseEmitterMap.keySet());
}
/**
* 获取当前的连接数量
*
* @return 当前的连接数量
*/
public int getUserCount() {
return sseEmitterMap.size();
}
private Runnable completionCallBack(String userId) {
return () -> {
log.info("用户 {} 结束连接", userId);
};
}
private Runnable timeoutCallBack(String userId) {
return () -> {
log.info("用户 {} 连接超时", userId);
removeUser(userId);
};
}
private Consumer<Throwable> infoCallBack(String userId) {
return throwable -> {
log.info("用户 {} 连接异常", userId);
removeUser(userId);
};
}
}
创建一个控制器类,以下是由于单账号多点登录,需单独区别
@GetMapping("/connect")
@ApiOperation("sse用于创建连接")
public SseEmitter connect(HttpServletRequest request, HttpServletResponse response) {
String sessionId = ClientIdUtils.getClientKey(request, response);
Long userId = AdminContext.getCurrentUser().getId();
String key = RedisKeyUtils.getKey(userId.toString(),sessionId);
SseEmitter connect = sseEmitterService.connect(key);
MessageResultVo deferredResultVo = smsService.selectMessageRemindByUserId(userId);
sseEmitterService.sendMessage(key, JSONObject.toJSONString(deferredResultVo));
return connect;
}
可能出现的异常
这个异常是由于当前连接对象已经终端,由于sse是由后端服务器主动向客服端推送消息,故而不知道连接是否存活,发送消息时导致的,由tomcat内部报错,可忽略,或者业务可以增加心跳连接的方式解决
AsyncRequestTimeoutException异常是由于初始化示例连接时新增了有效时间,导致服务端会到期主动中断并且再次连接,过程中可能会出现AsyncRequestTimeoutException,可自行全局捕获。