LuckyOx

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

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 ResponseEntity subscribe(@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 订阅连接,并维护在线人数
    */
    @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"));
      }
      }
      }
      }
posted on 2026-02-25 15:30  lucky_ox  阅读(4)  评论(0)    收藏  举报