SSE
SSE Server-Sent Events(服务器推送事件)
关键词:轻量,简单,稳定,在单向数据传输方面足够用
是一种单向数据传输技术,基于HTTP协议实现,连接一旦建立,客户端不需要发消息,只需要接受就OK了,较于WebSocket,SSE更轻,开发起来更方便,而且浏览器断线后会自动重连,我们不需要写额外的心跳或者重连逻辑
然后主要用于服务器单向传输,比如AI
实时在线人数-单服务器demo
当用户播放视频时,前端通过EventSource建立SSE长链接,调用方法;
作为后端,我们需要用一个SseEmitter对象返回,SseEmitter表示一个持续写数据的连接, 服务器随时可以向这个连接 emitter.send(...) 推送事件
生成clientId,表示本次链接的唯一标识
找到/创建房间桶 RoomBucket,里面记录着这个房间的在线人数和对应的SseEmitter
接着更新下这个RoomBucket的信息,接着给这个新连接发一次 init,然后给给房间所有连接广播最新在线人数即可
当用户关闭视频或者其他异样也一样,更新对应的RoomBucket信息,然后广播给房间里的所有订阅者即可
/**
-
SSE 在线人数接口
*/
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class SseOnlineController {private final RoomSseRegistry registry;
/**
- 订阅房间在线人数(SSE)
/
@GetMapping(value = "/sse/rooms/{roomId}/online", produces = TEXT_EVENT_STREAM_VALUE)
public ResponseEntitysubscribe(@PathVariable String roomId) { *
SseEmitter emitter = registry.subscribe(roomId);
// 建议加上这些头,减少缓存/缓冲带来的延迟
return ResponseEntity.ok()
.header(HttpHeaders.CACHE_CONTROL, "no-cache")
.header("X-Accel-Buffering", "no") // nginx: 禁用缓冲
.body(emitter);
}
}
/
- 订阅房间在线人数(SSE)
-
以“视频房间”为维度管理 SSE 订阅连接,并维护在线人数
*/
@Component
public class RoomSseRegistry {/**
- Key:roomId,Value:roomBucket
*/
private final ConcurrentHashMap<String, RoomBucket> rooms = new ConcurrentHashMap<>();
/**
- 创建订阅:加入房间并返回 emitter
*/
public SseEmitter subscribe(String roomId) {
// 过期时间1h
SseEmitter emitter = new SseEmitter(60 * 60 * 1000L);
// 生成连接 ID
String clientId = UUID.randomUUID().toString();
// 按 roomId 获取房间桶;如果这个房间还不存在,就原子地创建一个新的 RoomBucket 放进去并返回
RoomBucket bucket = rooms.computeIfAbsent(roomId, k -> new RoomBucket());
// 先注册再计数,避免并发下 emitter 回调时找不到
bucket.clients.put(clientId, emitter);
int online = bucket.onlineCount.incrementAndGet();
// 连接关闭/超时/异常:移除并减在线数
emitter.onCompletion(() -> removeClient(roomId, clientId));
emitter.onTimeout(() -> removeClient(roomId, clientId));
emitter.onError((ex) -> removeClient(roomId, clientId));
// 给当前连接发一条初始化事件(包含当前在线数)
safeSend(emitter, SseEmitter.event()
.name("init")
.id(String.valueOf(Instant.now().toEpochMilli()))
.data(new OnlinePayload(roomId, online, clientId), MediaType.APPLICATION_JSON));
// 广播最新在线数给房间内所有订阅者
broadcastOnline(roomId, online);
return emitter;
}
/**
- 移除某个连接,并广播在线数变化
*/
private void removeClient(String roomId, String clientId) {
RoomBucket bucket = rooms.get(roomId);
if (bucket == null) return;
SseEmitter removed = bucket.clients.remove(clientId);
if (removed == null) return; // 已移除过,避免重复减计数
int online = bucket.onlineCount.decrementAndGet();
broadcastOnline(roomId, online);
// 房间没人了就回收,避免内存占用
if (bucket.clients.isEmpty()) {
rooms.remove(roomId, bucket);
}
}
/**
- 广播在线数
*/
private void broadcastOnline(String roomId, int online) {
RoomBucket bucket = rooms.get(roomId);
if (bucket == null) return;
OnlinePayload payload = new OnlinePayload(roomId, online, null);
// 遍历发送,失败则移除(避免僵尸连接)
for (Map.Entry<String, SseEmitter> entry : bucket.clients.entrySet()) {
String clientId = entry.getKey();
SseEmitter emitter = entry.getValue();
boolean ok = safeSend(emitter, SseEmitter.event()
.name("online")
.id(String.valueOf(Instant.now().toEpochMilli()))
.data(payload, MediaType.APPLICATION_JSON));
if (!ok) {
removeClient(roomId, clientId);
}
}
}
private boolean safeSend(SseEmitter emitter, SseEmitter.SseEventBuilder event) {
try {
emitter.send(event);
return true;
} catch (IOException | IllegalStateException ex) {
return false;
}
}/**
- 房间桶:连接集合 + 在线计数
*/
private static class RoomBucket {
// Key:clientId,Value:emitter
private final ConcurrentHashMap<String, SseEmitter> clients = new ConcurrentHashMap<>();
private final AtomicInteger onlineCount = new AtomicInteger(0);
}
/**
- 推送载荷(简单 JSON)
*/
public record OnlinePayload(String roomId, int online, String clientId) {}
/**
- 定时心跳:防止中间层代理超时断开
*/
@Scheduled(fixedRate = 15000)
public void heartbeat() {
for (Map.Entry<String, RoomBucket> roomEntry : rooms.entrySet()) {
RoomBucket bucket = roomEntry.getValue();
for (Map.Entry<String, SseEmitter> client : bucket.clients.entrySet()) {
SseEmitter emitter = client.getValue();
// ping 事件:前端可以忽略
safeSend(emitter, SseEmitter.event()
.name("ping")
.data("ok"));
}
}
}
}
- Key:roomId,Value:roomBucket
浙公网安备 33010602011771号