mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-28 03:06:38 +08:00
229 lines
5.1 KiB
Go
229 lines
5.1 KiB
Go
// Package service — SSE (Server-Sent Events) 事件流服务。
|
|
package service
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"sync"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// SSEHub 管理 SSE 客户端连接和事件广播。
|
|
type SSEHub struct {
|
|
clients map[chan SSEEvent]bool
|
|
broadcast chan SSEEvent
|
|
register chan chan SSEEvent
|
|
unregister chan chan SSEEvent
|
|
log *zap.Logger
|
|
tickets map[string]*sseTicket
|
|
ticketMu sync.RWMutex
|
|
stopCh chan struct{}
|
|
}
|
|
|
|
type SSEEvent struct {
|
|
Type string `json:"type"`
|
|
Payload interface{} `json:"payload"`
|
|
}
|
|
|
|
// SSEEvent 事件类型常量。
|
|
const (
|
|
EventTypeScan = "scan"
|
|
EventTypeDownload = "download"
|
|
EventTypeSubscribe = "subscribe"
|
|
EventTypeTask = "task"
|
|
EventTypeSystem = "system"
|
|
EventTypeAuth = "auth"
|
|
)
|
|
|
|
// sseTicket 是一次性 OTP 票据。
|
|
type sseTicket struct {
|
|
UserID string
|
|
ExpiresAt time.Time
|
|
}
|
|
|
|
// NewSSEHub 创建 SSE Hub 实例。
|
|
func NewSSEHub(log *zap.Logger) *SSEHub {
|
|
return &SSEHub{
|
|
clients: make(map[chan SSEEvent]bool),
|
|
broadcast: make(chan SSEEvent, 256),
|
|
register: make(chan chan SSEEvent),
|
|
unregister: make(chan chan SSEEvent),
|
|
tickets: make(map[string]*sseTicket),
|
|
log: log,
|
|
stopCh: make(chan struct{}),
|
|
}
|
|
}
|
|
|
|
// Run 启动 SSE Hub 的事件循环。
|
|
func (h *SSEHub) Run() {
|
|
for {
|
|
select {
|
|
case client := <-h.register:
|
|
h.clients[client] = true
|
|
h.log.Debug("SSE client connected", zap.Int("total", len(h.clients)))
|
|
|
|
case client := <-h.unregister:
|
|
if _, ok := h.clients[client]; ok {
|
|
delete(h.clients, client)
|
|
close(client)
|
|
h.log.Debug("SSE client disconnected", zap.Int("total", len(h.clients)))
|
|
}
|
|
|
|
case event := <-h.broadcast:
|
|
h.distribute(event)
|
|
|
|
case <-h.stopCh:
|
|
h.closeAll()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop 停止 SSE Hub。
|
|
func (h *SSEHub) Stop() {
|
|
close(h.stopCh)
|
|
}
|
|
|
|
// ClientChannel SSE 客户端通道包装器。
|
|
type ClientChannel struct {
|
|
Ch chan SSEEvent
|
|
}
|
|
|
|
// Subscribe 注册一个新的 SSE 客户端,返回 SSE 客户端包装器。
|
|
func (h *SSEHub) Subscribe() *ClientChannel {
|
|
ch := make(chan SSEEvent, 100)
|
|
h.register <- ch
|
|
return &ClientChannel{Ch: ch}
|
|
}
|
|
|
|
// Unsubscribe 取消注册 SSE 客户端。
|
|
func (h *SSEHub) Unsubscribe(client *ClientChannel) {
|
|
if client != nil && client.Ch != nil {
|
|
h.unregister <- client.Ch
|
|
}
|
|
}
|
|
|
|
// Broadcast 向所有连接的客户端广播事件。
|
|
func (h *SSEHub) Broadcast(eventType string, payload interface{}) {
|
|
event := SSEEvent{
|
|
Type: eventType,
|
|
Payload: payload,
|
|
}
|
|
select {
|
|
case h.broadcast <- event:
|
|
default:
|
|
h.log.Warn("SSE broadcast queue full, dropping event", zap.String("type", eventType))
|
|
}
|
|
}
|
|
|
|
// SendToUser 向指定用户发送事件(通过 UserID 匹配)。
|
|
// 注意:此方法需要在客户端连接时关联 UserID。
|
|
func (h *SSEHub) SendToUser(userID string, eventType string, payload interface{}) {
|
|
// 目前通过广播实现,未来可扩展为按用户分组
|
|
h.Broadcast(eventType, payload)
|
|
}
|
|
|
|
// distribute 将事件分发给所有客户端。
|
|
func (h *SSEHub) distribute(event SSEEvent) {
|
|
data, err := json.Marshal(event)
|
|
if err != nil {
|
|
h.log.Error("failed to marshal SSE event", zap.Error(err))
|
|
return
|
|
}
|
|
|
|
for client := range h.clients {
|
|
select {
|
|
case client <- event:
|
|
default:
|
|
// 客户端通道已满,跳过
|
|
h.log.Warn("SSE client buffer full", zap.String("event", string(data)))
|
|
}
|
|
}
|
|
}
|
|
|
|
// closeAll 关闭所有客户端连接。
|
|
func (h *SSEHub) closeAll() {
|
|
for client := range h.clients {
|
|
close(client)
|
|
}
|
|
h.clients = make(map[chan SSEEvent]bool)
|
|
}
|
|
|
|
// GenerateTicket 生成一次性 SSE 连接票据(用于无 JWT 场景下的安全连接)。
|
|
func (h *SSEHub) GenerateTicket(userID string) (string, error) {
|
|
buf := make([]byte, 16)
|
|
if _, err := rand.Read(buf); err != nil {
|
|
return "", err
|
|
}
|
|
ticket := hex.EncodeToString(buf)
|
|
|
|
h.ticketMu.Lock()
|
|
defer h.ticketMu.Unlock()
|
|
|
|
h.tickets[ticket] = &sseTicket{
|
|
UserID: userID,
|
|
ExpiresAt: time.Now().Add(10 * time.Second),
|
|
}
|
|
|
|
return ticket, nil
|
|
}
|
|
|
|
// ValidateTicket 验证 SSE 连接票据,返回关联的用户 ID。
|
|
func (h *SSEHub) ValidateTicket(ticket string) (string, error) {
|
|
h.ticketMu.Lock()
|
|
defer h.ticketMu.Unlock()
|
|
|
|
t, ok := h.tickets[ticket]
|
|
if !ok {
|
|
return "", ErrInvalidTicket
|
|
}
|
|
|
|
if time.Now().After(t.ExpiresAt) {
|
|
delete(h.tickets, ticket)
|
|
return "", ErrTicketExpired
|
|
}
|
|
|
|
userID := t.UserID
|
|
delete(h.tickets, ticket)
|
|
|
|
return userID, nil
|
|
}
|
|
|
|
// CleanupTickets 清理过期的票据。
|
|
func (h *SSEHub) CleanupTickets() {
|
|
h.ticketMu.Lock()
|
|
defer h.ticketMu.Unlock()
|
|
|
|
now := time.Now()
|
|
for ticket, t := range h.tickets {
|
|
if now.After(t.ExpiresAt) {
|
|
delete(h.tickets, ticket)
|
|
}
|
|
}
|
|
}
|
|
|
|
// SSE Hub 错误定义。
|
|
var (
|
|
ErrInvalidTicket = &SSEError{Message: "invalid ticket"}
|
|
ErrTicketExpired = &SSEError{Message: "ticket expired"}
|
|
)
|
|
|
|
// SSEError SSE 相关错误。
|
|
type SSEError struct {
|
|
Message string
|
|
}
|
|
|
|
func (e *SSEError) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
// ClientCount 返回当前连接的客户端数量。
|
|
func (h *SSEHub) ClientCount() int {
|
|
h.ticketMu.RLock()
|
|
defer h.ticketMu.RUnlock()
|
|
return len(h.clients)
|
|
}
|