Documentation
¶
Overview ¶
* @Author: kamalyes 501893067@qq.com * @Date: 2025-11-15 00:00:00 * @LastEditors: kamalyes 501893067@qq.com * @LastEditTime: 2025-12-28 00:00:00 * @FilePath: \go-wsc\messaging\ack.go * @Description: ACK消息确认机制 * * Copyright (c) 2025 by kamalyes, All Rights Reserved.
* @Author: kamalyes 501893067@qq.com * @Date: 2026-09-22 10:08:00 * @LastEditors: kamalyes 501893067@qq.com * @LastEditTime: 2026-09-22 10:08:00 * @FilePath: \go-wsc\messaging\context_keys.go * @Description: 消息域上下文键 —— 连接归属与离线广播聚合器的单一事实源 * * 迁移自 constants/context.go(P2 批2 域化): * - SenderID/UserID 键由 transport 升级器在连接 ctx 注入、消息域消费, * 键与消费语义同属消息链路,归位消息域(transport 单向依赖 messaging,无循环) * - 离线广播聚合器键供批量扇出路径聚合未命中用户,扇出结束后统一跨节点推送 * * Copyright (c) 2026 by kamalyes, All Rights Reserved.
* @Author: kamalyes 501893067@qq.com * @Date: 2026-09-07 21:00:00 * @LastEditors: kamalyes 501893067@qq.com * @LastEditTime: 2026-09-22 10:42:00 * @FilePath: \go-wsc\messaging\delivery_fallback.go * @Description: 投递兜底路由 —— 拒绝不等于丢弃(P0 分级送达基座) * * TrySend 返回 false(SendChan 满/客户端关闭)时按消息分级路由: * - 必达级(Guaranteed):立即转离线补发(ACK 超时链路仍作双保险——时间轮到期后 * ClaimStaleSending 转离线,此处提前到"入队失败即转",不等 5min 兜底) * - 普通级(Standard):立即转离线(用户上线时推送——修复"广播丢弃彻底丢失") * - 高频级(Ephemeral):丢弃计数(latest-wins 语义:下一条同 key 消息自然覆盖, * 旧值过期是正确行为而非丢失) * * 广播路径的成员级兜底(BroadcastToFiltered/BroadcastToUserIDs 的 TrySend false 分支) * 复用同一路由:消息 Receiver 为空时补写目标用户再转离线,修复广播离线丢失缺陷 * * Copyright (c) 2026 by kamalyes, All Rights Reserved.
* @Author: kamalyes 501893067@qq.com * @Date: 2026-09-11 22:30:00 * @LastEditors: kamalyes 501893067@qq.com * @LastEditTime: 2026-09-22 10:57:00 * @FilePath: \go-wsc\messaging\frame_writer.go * @Description: writev 合帧批量写 —— 突发 N 条消息 N 次 syscall 降至 1 次 writev * * RFC6455 帧自定界(服务端帧无掩码),net.Buffers 聚合多个 [帧头+载荷] * 一次 writev 落盘。首条消息直写(低延迟),积压部分合帧(吞吐): * - 无积压:1 条 1 次 syscall(与原路径持平) * - 突发 N 条:1 + 1 次 syscall(首条直写 + N-1 合帧一次写出) * * 写锁安全性:写泵是唯一数据写者(单写者模型,与原 WriteMessage 路径一致); * WriteControl(协议 ping/pong)与数据写的理论交错窗口与原路径相同(见 * closeClientConnection 注释——控制帧独立锁,交错时客户端走异常重连,不影响正确性) * 压缩扩展未启用(DefaultUpgrader 未开 EnableCompression),帧格式为标准无压缩帧 * * Copyright (c) 2026 by kamalyes, All Rights Reserved.
* @Author: kamalyes 501893067@qq.com * @Date: 2026-09-22 10:35:00 * @LastEditors: kamalyes 501893067@qq.com * @LastEditTime: 2026-10-26 09:38:02 * @FilePath: \go-wsc\messaging\json_peek.go * @Description: JSON 顶层字段就地探测(入站控制类消息快路径,免完整反序列化) * * 动机:handleTextMessage 原先用一个 2 字段的探测结构体做 json.Unmarshal 来识别 * 心跳/ACK,命中即返回。但 json.Unmarshal 即使只映射 2 个字段也要扫描并跳过整个文档, * 于是**非控制类消息(数据消息,占数据面绝大多数)要付两次完整解析**: * 一次探测 + 一次真正的 HubMessage 反序列化。 * * 本文件提供只读的顶层字符串字段探测:单次线性扫描,零分配,不构造中间对象。 * * 正确性优先于速度——探测只在**无歧义**时返回命中: * - 只认深度 0(最外层对象)的键,嵌套对象里的同名键不算 * - 字符串内部的字节不参与键匹配(含转义处理),避免 content 里出现 * "message_type" 字样时误判 * - 任何结构异常(截断、非对象、非字符串值)一律返回未命中 * * 误判方向的安全性:返回"未命中"是安全的——调用方会落到完整反序列化, * 且完整解析后仍会复查 MessageType 作兜底(见 handleTextMessage 的类型 switch)。 * 返回"错误的命中"才是危险方向,故上述三条约束缺一不可。
* @Author: kamalyes 501893067@qq.com * @Date: 2026-09-23 10:05:00 * @LastEditors: kamalyes 501893067@qq.com * @LastEditTime: 2026-09-23 10:05:00 * @FilePath: \go-wsc\messaging\replay_gate.go * @Description: 首连离线回放门闩(重连场景的投递顺序闭环) * * 问题背景:首连触发的 PushOfflineMessages 异步执行(workerPool 回调池), * 期间新到的实时消息直接进 sendChan → 用户先收到新消息、后收到更早的 * 离线消息(乱序);drain 队列还可能把重连瞬间被误转离线的消息重复推出 * * 解法:首连时 begin 门闩 → 该用户的实时投递被暂存 → 离线回放完成后 * end 按暂存顺序补投 → 开闸。每用户投递顺序 = 消息真实时序 * * 边界保护: * - 每用户暂存上限 maxReplayHoldout:超出后直接投递不再暂存(宁可极小 * 概率乱序,也不无限堆积内存) * - end 由 defer 保证必达:回放 panic 也会开闸,闸门不会永久卡死 * - 单机模式无离线处理器时不启用门闩(PushOfflineMessages nil-safe 跳过) * * Copyright (c) 2026 by kamalyes, All Rights Reserved.
Index ¶
- func InitializeOfflineQueue(hub spi.StoreTarget, deps OfflineDeps) error
- func NewReplayGate() *replayGate
- type AckManager
- func (am *AckManager) AddPendingMessage(msg *models.HubMessage) *PendingMessage
- func (am *AckManager) AddPendingMessageWithExpire(msg *models.HubMessage, timeout time.Duration, maxRetry int) *PendingMessage
- func (am *AckManager) CleanupExpired() int
- func (am *AckManager) ConfirmMessage(messageID string, ack *AckMessage) bool
- func (am *AckManager) GetMaxRetry() int
- func (am *AckManager) GetPendingCount() int
- func (am *AckManager) GetPendingMessage(messageID string) (*PendingMessage, bool)
- func (am *AckManager) GetTimeout() time.Duration
- func (am *AckManager) RemovePendingMessage(messageID string)
- func (am *AckManager) SetExpireDuration(duration time.Duration)
- func (am *AckManager) SetOfflineRepo(handler spi.OfflineStore)
- func (am *AckManager) Shutdown()
- type AckMessage
- type AckStatus
- type ContextKey
- type ErrorCallback
- type Host
- type HubWorkerPool
- func (w *HubWorkerPool) Stats() WorkerPoolStats
- func (w *HubWorkerPool) Stop()
- func (w *HubWorkerPool) SubmitCallback(ctx context.Context, task func())
- func (w *HubWorkerPool) SubmitDistributed(ctx context.Context, task func())
- func (w *HubWorkerPool) SubmitMessage(ctx context.Context, task func())
- func (w *HubWorkerPool) SubmitRecord(ctx context.Context, task func())
- func (w *HubWorkerPool) TrySubmitCallback(task func()) bool
- func (w *HubWorkerPool) TrySubmitDistributed(task func()) bool
- func (w *HubWorkerPool) TrySubmitMessage(task func()) bool
- func (w *HubWorkerPool) TrySubmitRecord(task func()) bool
- type HybridOfflineMessageConfig
- type HybridOfflineMessageHandler
- func (h *HybridOfflineMessageHandler) ClearOfflineMessages(ctx context.Context, userID string, groupIDs []string) error
- func (h *HybridOfflineMessageHandler) DeleteOfflineMessages(ctx context.Context, userID string, messageIDs []string) error
- func (h *HybridOfflineMessageHandler) DrainOfflineQueue(ctx context.Context, userID string, limit int) ([]*models.HubMessage, error)
- func (h *HybridOfflineMessageHandler) GetOfflineMessageCount(ctx context.Context, userID string) (int64, error)
- func (h *HybridOfflineMessageHandler) GetOfflineMessages(ctx context.Context, userID string, limit int, cursor string) ([]*models.HubMessage, string, error)
- func (h *HybridOfflineMessageHandler) StoreOfflineMessage(ctx context.Context, userID string, msg *models.HubMessage) error
- func (h *HybridOfflineMessageHandler) UpdatePushStatus(ctx context.Context, messageIDs []string, pushErr error) error
- type Manager
- func (m *Manager) BroadcastAfterDelay(ctx context.Context, msg *models.HubMessage, delay time.Duration)
- func (m *Manager) BroadcastByUserType(ctx context.Context, userType models.UserType, msg *models.HubMessage) int
- func (m *Manager) BroadcastExclude(ctx context.Context, msg *models.HubMessage, excludeUserIDs []string) int
- func (m *Manager) BroadcastFrameToUserIDs(ctx context.Context, userIDs []string, msg *models.HubMessage, frame []byte) int
- func (m *Manager) BroadcastPriority(ctx context.Context, msg *models.HubMessage, priority models.Priority)
- func (m *Manager) BroadcastToClientType(ctx context.Context, clientType models.ClientType, msg *models.HubMessage) int
- func (m *Manager) BroadcastToDepartment(ctx context.Context, department models.Department, msg *models.HubMessage) int
- func (m *Manager) BroadcastToFiltered(ctx context.Context, condition func(*models.Client) bool, ...) int
- func (m *Manager) BroadcastToNamespace(ctx context.Context, namespace string, msg *models.HubMessage) int
- func (m *Manager) BroadcastToRole(ctx context.Context, role models.UserRole, msg *models.HubMessage) int
- func (m *Manager) BroadcastToSSEClients(msg *models.HubMessage)
- func (m *Manager) BroadcastToUserIDs(ctx context.Context, userIDs []string, msg *models.HubMessage) int
- func (m *Manager) CleanupExpiredAcks() int
- func (m *Manager) DeleteMessageRecord(ctx context.Context, id uint) error
- func (m *Manager) DeleteMessageRecordByMessageID(ctx context.Context, messageID string) error
- func (m *Manager) Deliver(ctx context.Context, msg *models.HubMessage, excludeSender bool) *models.DeliverResult
- func (m *Manager) EndUserReplay(userID string)
- func (m *Manager) GetClientsByClientType(clientType models.ClientType) []*models.Client
- func (m *Manager) GetClientsByDepartment(department models.Department) []*models.Client
- func (m *Manager) GetClientsByRole(role models.UserRole) []*models.Client
- func (m *Manager) GetClientsByUserType(userType models.UserType) []*models.Client
- func (m *Manager) GetClientsByVIPLevel(minVIPLevel models.VIPLevel) []*models.Client
- func (m *Manager) HandleBroadcastMessage(ctx context.Context, msg *models.HubMessage)
- func (m *Manager) HoldUserDelivery(userID string, deliver func())
- func (m *Manager) InvokeErrorCallback(ctx context.Context, err error, severity models.ErrorSeverity) error
- func (m *Manager) InvokeMessageReceivedCallback(ctx context.Context, client *models.Client, msg *models.HubMessage) error
- func (m *Manager) NotifyObservers(ctx context.Context, msg *models.HubMessage)
- func (m *Manager) NotifyObserversDirect(msg *models.HubMessage, namespace string, groupIDs []string)
- func (m *Manager) OfferAggregatable(ctx context.Context, msg *models.HubMessage, excludeSender bool) bool
- func (m *Manager) PushOfflineMessages(ctx context.Context, client *models.Client) (pushedIDs, failedIDs []string)
- func (m *Manager) QueryMessageRecord(ctx context.Context, messageID, receiver string) (*models.MessageSendRecord, error)
- func (m *Manager) QueryMessageRecordsByClientIP(ctx context.Context, clientIP string, limit int) ([]*models.MessageSendRecord, error)
- func (m *Manager) QueryMessageRecordsByNodeIP(ctx context.Context, nodeIP string, limit int) ([]*models.MessageSendRecord, error)
- func (m *Manager) QueryMessageRecordsByReceiver(ctx context.Context, receiver string, limit int) ([]*models.MessageSendRecord, error)
- func (m *Manager) QueryMessageRecordsBySender(ctx context.Context, sender string, limit int) ([]*models.MessageSendRecord, error)
- func (m *Manager) QueryMessageRecordsByStatus(ctx context.Context, status models.MessageSendStatus, limit int) ([]*models.MessageSendRecord, error)
- func (m *Manager) QueryRetryableMessageRecords(ctx context.Context, limit int) ([]*models.MessageSendRecord, error)
- func (m *Manager) ScanNodeAckTimeouts()
- func (m *Manager) ScheduleAckTimeouts(keys []models.MessageRecordKey)
- func (m *Manager) SendConditional(ctx context.Context, condition func(*models.Client) bool, ...) int
- func (m *Manager) SendPriority(ctx context.Context, userID string, msg *models.HubMessage, ...)
- func (m *Manager) SendToAllClientsInMap(ctx context.Context, clientMap map[string]*models.Client, ...)
- func (m *Manager) SendToClient(ctx context.Context, client *models.Client, msg *models.HubMessage)
- func (m *Manager) SendToClientSerialized(ctx context.Context, client *models.Client, msg *models.HubMessage, ...) bool
- func (m *Manager) SendToClientsWithRetry(ctx context.Context, clients []*models.Client, msg *models.HubMessage, ...) map[string]*models.SendResult
- func (m *Manager) SendToGroupMembers(ctx context.Context, memberIDs []string, msg *models.HubMessage, ...) *models.BroadcastResult
- func (m *Manager) SendToMultipleUsers(ctx context.Context, userIDs []string, msg *models.HubMessage) map[string]error
- func (m *Manager) SendToUserViaSSE(userID string, msg *models.HubMessage) bool
- func (m *Manager) SendToUserWithRetry(ctx context.Context, toUserID string, msg *models.HubMessage) *models.SendResult
- func (m *Manager) SendWithCallback(ctx context.Context, userID string, msg *models.HubMessage, ...)
- func (m *Manager) StartClientPumps(client *models.Client)
- func (m *Manager) Stop()
- func (m *Manager) StoreOfflineOnDeliveryFailure(msg *models.HubMessage, deliveryErr error)
- func (m *Manager) SwapBroadcastFallbackCount() int64
- func (m *Manager) SwapBroadcastSentCount() int64
- func (m *Manager) SwapMessageSentCount() int64
- func (m *Manager) TryBeginUserReplay(userID string) bool
- func (m *Manager) TryDeliverToClient(ctx context.Context, client *models.Client, msg *models.HubMessage, ...) bool
- func (m *Manager) TrySendWithFallback(client *models.Client, data []byte, msg *models.HubMessage, ...) bool
- func (m *Manager) Wait()
- func (m *Manager) WithAckManager(am *AckManager) *Manager
- func (m *Manager) WithAckTimeoutTimer(t *syncx.HashedWheelTimer) *Manager
- func (m *Manager) WithErrorCallback(cb ErrorCallback) *Manager
- func (m *Manager) WithIDGenerator(g models.IDGenerator) *Manager
- func (m *Manager) WithMessageReceivedCallback(cb MessageReceivedCallback) *Manager
- func (m *Manager) WithMessageSendCallback(cb MessageSendCallback) *Manager
- func (m *Manager) WithOfflineHandler(h spi.OfflineQueue) *Manager
- func (m *Manager) WithReplayGate(g *replayGate) *Manager
- func (m *Manager) WithWorkerPool(wp *HubWorkerPool) *Manager
- type MessageReceivedCallback
- type MessageSendCallback
- type OfflineDeps
- type PendingMessage
- type WorkerPoolStats
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func InitializeOfflineQueue ¶
func InitializeOfflineQueue(hub spi.StoreTarget, deps OfflineDeps) error
InitializeOfflineQueue 组装并注入混合离线消息处理器
两半依赖任一缺失时返回错误而非静默跳过:离线消息是「用户离线期间的 消息不丢」这条承诺的唯一兜底,缺一半会让消息在无人察觉的情况下丢失。 若业务侧确实不需要离线消息能力,应显式不调用本函数。
func NewReplayGate ¶
func NewReplayGate() *replayGate
NewReplayGate 创建门闩(编排层构造 messaging.Manager 时经 WithReplayGate 注入)
Types ¶
type AckManager ¶
type AckManager struct {
// contains filtered or unexported fields
}
AckManager ACK管理器 基于 syncx.ShardedMap 实现待确认消息的分片存储,替代单一 mutex + map 高并发下 AddPending/Confirm/Remove 操作完全并行,锁竞争降低 64 倍
func NewAckManager ¶
func NewAckManager(ackTimeout time.Duration, maxRetry int) *AckManager
NewAckManager 创建ACK管理器
func NewAckManagerWithOptions ¶
func NewAckManagerWithOptions(ackTimeout time.Duration, maxRetry int, expireDuration time.Duration, offlineRepo spi.OfflineStore) *AckManager
NewAckManagerWithOptions 创建ACK管理器(带选项)
func (*AckManager) AddPendingMessage ¶
func (am *AckManager) AddPendingMessage(msg *models.HubMessage) *PendingMessage
AddPendingMessage 添加待确认消息
func (*AckManager) AddPendingMessageWithExpire ¶
func (am *AckManager) AddPendingMessageWithExpire(msg *models.HubMessage, timeout time.Duration, maxRetry int) *PendingMessage
AddPendingMessageWithExpire 添加待确认消息(带过期时间)
func (*AckManager) CleanupExpired ¶
func (am *AckManager) CleanupExpired() int
CleanupExpired 清理过期的待确认消息 使用 ShardedMap.Range 遍历(分片读锁粒度),收集过期 key 后批量删除
func (*AckManager) ConfirmMessage ¶
func (am *AckManager) ConfirmMessage(messageID string, ack *AckMessage) bool
ConfirmMessage 确认消息
func (*AckManager) GetPendingCount ¶
func (am *AckManager) GetPendingCount() int
GetPendingCount 获取待确认消息数量(原子读取,零锁开销)
func (*AckManager) GetPendingMessage ¶
func (am *AckManager) GetPendingMessage(messageID string) (*PendingMessage, bool)
GetPendingMessage 获取待确认消息
func (*AckManager) GetTimeout ¶
func (am *AckManager) GetTimeout() time.Duration
GetTimeout 获取ACK超时时间
func (*AckManager) RemovePendingMessage ¶
func (am *AckManager) RemovePendingMessage(messageID string)
RemovePendingMessage 移除待确认消息
func (*AckManager) SetExpireDuration ¶
func (am *AckManager) SetExpireDuration(duration time.Duration)
SetExpireDuration 设置消息过期时间
func (*AckManager) SetOfflineRepo ¶
func (am *AckManager) SetOfflineRepo(handler spi.OfflineStore)
SetOfflineRepo 设置离线消息处理器 用于统一使用 Hub 的离线消息处理器
func (*AckManager) Shutdown ¶
func (am *AckManager) Shutdown()
Shutdown 关闭ACK管理器 遍历所有待确认消息取消 context,然后清空分片存储
type AckMessage ¶
type AckMessage struct {
MessageID string `json:"message_id"` // HubMessage消息ID
Status AckStatus `json:"status"` // ACK状态
Timestamp time.Time `json:"timestamp"` // 时间戳
Error string `json:"error"` // 错误信息
}
AckMessage ACK消息结构
type ContextKey ¶
type ContextKey string
ContextKey 上下文键类型(消息域:连接归属用户 + 离线广播聚合器)
const ( // ContextKeyUserID 用户ID ContextKeyUserID ContextKey = "user_id" // ContextKeySenderID 发送者ID(连接级 ctx 注入,标识连接归属用户) ContextKeySenderID ContextKey = "sender_id" // ContextKeyOfflineBroadcastCollector 批量扇出路径的离线广播聚合器 // // 群组成员可达数万,扇出 goroutine 内逐人同步广播会造成 N 倍集群流量放大与 // Redis 连接池耗尽(OOM 教训);但完全跳过又会丢失"索引滞后但实际在线"用户的实时投递。 // 折中:扇出期间仅聚合收集未命中用户,扇出结束后统一逐人跨节点推送(真的推送), // 真正离线的用户由离线存储 + 重连上线拉取兜底 ContextKeyOfflineBroadcastCollector ContextKey = "offline_broadcast_collector" )
type ErrorCallback ¶
ErrorCallback 错误处理回调(统一处理消息链路各环节错误)
type Host ¶
type Host interface {
// Context 返回 Hub 生命周期上下文
Context() context.Context
// GetLogger 日志器
GetLogger() spi.Logger
// GetNodeID 当前节点 ID
GetNodeID() string
// GetConfig 全局配置
GetConfig() *wscconfig.WSC
// IsShuttingDown Hub 是否正在关闭(读泵据此区分服务端主动断开与异常断开)
IsShuttingDown() bool
// GetShardedRegistry 分片连接注册表(未注入时返回 nil)
GetShardedRegistry() *connection.ShardedRegistry
// Unregister 注销客户端连接(连接生命周期归编排层)
Unregister(client *models.Client)
// HandleHeartbeat 处理心跳消息(连接域时间轮续期 + 统计刷新)
HandleHeartbeat(client *models.Client)
// GetMessageSink 消息记录仓储(未注入时返回 nil)
GetMessageSink() spi.MessageSink
// GetGroupRepo 群组仓储(未注入时返回 nil)
GetGroupRepo() spi.GroupStore
// GetStatsRepo 节点统计仓储(未注入时返回 nil,兼作消息计数开关)
GetStatsRepo() spi.HubStats
// HasPubsub 是否配置分布式发布订阅(与 gRPC 共同构成跨节点通道开关)
HasPubsub() bool
// IsGRPCEnabled 集群 gRPC 通道是否启用
IsGRPCEnabled() bool
// RouteToCluster 路由消息到集群其他节点
RouteToCluster(ctx context.Context, msg *models.HubMessage, opts cluster.ClusterDispatchOptions) error
// BatchGetUserNodes 批量查询多个用户所在节点(Pipeline 单次往返,信封过滤与路由上下文一致)
// 单个用户查询传单元素切片复用本端口(不再保留单查方法);
// 在线判定语义:len(nodes)>0 即在线;查询失败/未注入返回 nil,调用方回退逐用户查询
BatchGetUserNodes(ctx context.Context, userIDs []string) map[string][]string
// CheckAndRouteToNode 检查用户在线节点并按需跨节点投递
// presetNodes 非空时跳过节点查询(调用方已预取,消第二次 Redis 往返)
// 返回:是否命中跨节点路由、目标节点列表、错误
CheckAndRouteToNode(ctx context.Context, userID string, msg *models.HubMessage, presetNodes []string) (bool, []string, error)
// GetAllClusterNodeIDs 获取集群全部节点 ID
GetAllClusterNodeIDs() []string
// MarkRerouteAttempted 标记消息已尝试重路由(防循环投递)
MarkRerouteAttempted(messageID string, targetNodes []string, p2p bool)
// DeleteRerouteGuard 清理重路由守卫条目(消息到达终态时调用)
DeleteRerouteGuard(messageID string)
// SubmitClusterDispatch 提交集群批量分发任务(队列满时返回 false)
SubmitClusterDispatch(msg *models.HubMessage, opts cluster.ClusterDispatchOptions) bool
// GetMessageRecordOutbox 消息记录攒批 outbox(未注入时返回 nil,消息域降级单条 Create)
GetMessageRecordOutbox() *batcher.MessageRecordOutbox
// TrackReceiverMessageStats 投递接收方消息统计
TrackReceiverMessageStats(connectionID string, receiverType models.UserType, dataSize int)
// TrackConnectionError 记录连接错误(异常断开排查)
TrackConnectionError(connectionID string, userType models.UserType, err error)
// GetObserverNotifier 观察者通知批量处理器(未注入时返回 nil,观察者通知降级 no-op)
GetObserverNotifier() *batcher.ObserverNotificationBatcher
// NotifyObserverClients 向观察者客户端批量投递通知(观察者投递统一实现:
// Clone + observer metadata + 预序列化一次 + TrySend 直投,旁路投递不走状态回报)
// 返回成功投递的观察者数
NotifyObserverClients(ctx context.Context, observers []*models.Client, msg *models.HubMessage) int32
// AdmitMessage 消息级准入裁决(闸门未启用时恒放行)
AdmitMessage(msg *models.HubMessage, isBroadcast bool) overload.AdmitVerdict
// AdmissionOnDelivered 投递完成埋点(闸门未启用时 no-op)
AdmissionOnDelivered()
// OnWriteBatch 写泵批量埋点(n = 本批消息数,闸门/指标未启用时 no-op)
OnWriteBatch(n int)
// DeferBroadcast 广播延迟重投(延迟队列,拒绝不等于丢弃)
DeferBroadcast(ctx context.Context, msg *models.HubMessage, retry func(context.Context, *models.HubMessage))
// GetBroadcastShaper 广播出向整形器(未启用时返回 nil)
GetBroadcastShaper() *overload.Shaper
// GetEphemeralCoalescer 高频消息合并器(未启用时返回 nil)
GetEphemeralCoalescer() *overload.Coalescer
// GetMessageAggregator 消息聚合器(未启用时返回 nil)
GetMessageAggregator() *overload.MessageAggregator
// GetOverloadMetrics 过载漏斗指标(编排层恒注入)
GetOverloadMetrics() *overload.OverloadMetrics
// GetMessageStatusUpdater 消息状态批量更新器(未注入时返回 nil)
GetMessageStatusUpdater() *batcher.MessageStatusUpdater
// GetAdmissionLevel 当前过载水位(闸门未启用时返回 LevelNormal)
GetAdmissionLevel() overload.OverloadLevel
}
Host 消息域宿主端口
消息收发链路(发送/广播/分发/ACK 超时/离线转存)运行时依赖的 全部外部能力,由 hub 编排层实现并注入。
type HubWorkerPool ¶
type HubWorkerPool struct {
// MessagePool 消息发送池(sendToUser、broadcast 等)
MessagePool *syncx.WorkerPool
// CallbackPool 业务回调池(连接/断开回调、事件回调)
CallbackPool *syncx.WorkerPool
// RecordPool 数据库记录池(消息记录、统计同步)
RecordPool *syncx.WorkerPool
// DistributedPool 跨节点消息处理池
DistributedPool *syncx.WorkerPool
// contains filtered or unexported fields
}
HubWorkerPool Hub 工作池集合 按任务类型分池,避免低优先级任务饿死高优先级任务
func NewHubWorkerPool ¶
func NewHubWorkerPool(cfg *wscconfig.WorkerPoolConfig, log spi.Logger) *HubWorkerPool
NewHubWorkerPool 创建 Hub 工作池集合 syncx.NewWorkerPool 在构造时自动启动 worker goroutine 弹性策略:构造仅启动保底数量,积压按需扩容至上限,空闲超过 cfg.IdleTimeout 自动回收
func (*HubWorkerPool) SubmitCallback ¶
func (w *HubWorkerPool) SubmitCallback(ctx context.Context, task func())
SubmitCallback 提交业务回调任务(阻塞式)
func (*HubWorkerPool) SubmitDistributed ¶
func (w *HubWorkerPool) SubmitDistributed(ctx context.Context, task func())
SubmitDistributed 提交跨节点消息处理任务(阻塞式)
func (*HubWorkerPool) SubmitMessage ¶
func (w *HubWorkerPool) SubmitMessage(ctx context.Context, task func())
SubmitMessage 提交消息发送任务(阻塞式,队列满时等待)
func (*HubWorkerPool) SubmitRecord ¶
func (w *HubWorkerPool) SubmitRecord(ctx context.Context, task func())
SubmitRecord 提交数据库记录任务(阻塞式)
func (*HubWorkerPool) TrySubmitCallback ¶
func (w *HubWorkerPool) TrySubmitCallback(task func()) bool
TrySubmitCallback 非阻塞提交业务回调任务
func (*HubWorkerPool) TrySubmitDistributed ¶
func (w *HubWorkerPool) TrySubmitDistributed(task func()) bool
TrySubmitDistributed 非阻塞提交跨节点消息处理任务
func (*HubWorkerPool) TrySubmitMessage ¶
func (w *HubWorkerPool) TrySubmitMessage(task func()) bool
TrySubmitMessage 非阻塞提交消息发送任务 队列满时返回 false,调用方可降级处理(如丢弃、记录失败)
func (*HubWorkerPool) TrySubmitRecord ¶
func (w *HubWorkerPool) TrySubmitRecord(task func()) bool
TrySubmitRecord 非阻塞提交数据库记录任务 队列满时丢弃(记录任务可丢失,不影响核心业务)
type HybridOfflineMessageConfig ¶
type HybridOfflineMessageConfig struct {
RedisClient redis.UniversalClient // Redis 客户端(必需)
DB *gorm.DB // MySQL 数据库(必需)
KeyPrefix string // Redis key 前缀,默认 "wsc:offline:"
QueueTTL time.Duration // Redis 队列过期时间,默认 7 天
Logger spi.Logger // 日志器(可选)
}
HybridOfflineMessageConfig 混合存储配置
type HybridOfflineMessageHandler ¶
type HybridOfflineMessageHandler struct {
// contains filtered or unexported fields
}
HybridOfflineMessageHandler 混合离线消息处理器 使用 Redis 队列存储短期离线消息(性能优先,快速推送) 使用 RDBMS offline_messages 表持久化(数据安全,防止 Redis 数据丢失) 注意:Redis 和 MySQL 必须同时初始化,双保险存储
func NewHybridOfflineMessageHandler ¶
func NewHybridOfflineMessageHandler(queue spi.MessageQueue, store spi.OfflineStore, config *wscconfig.OfflineMessage, log spi.Logger) *HybridOfflineMessageHandler
NewHybridOfflineMessageHandler 创建混合离线消息处理器
两个后端实现均由适配器注入 —— core 不构造任何具体存储实现, 依赖方向恒为 adapter → core。
参数:
- queue: Redis 队列实现(必需,由 adapter/redis 注入)
- store: RDBMS 持久化实现(必需,由 adapter/gorm 注入)
- config: 离线消息配置对象
- log: 日志记录器(nil 时使用默认)
func (*HybridOfflineMessageHandler) ClearOfflineMessages ¶
func (h *HybridOfflineMessageHandler) ClearOfflineMessages(ctx context.Context, userID string, groupIDs []string) error
ClearOfflineMessages 清空用户的离线消息 groupIDs: 用户在该 (appID, namespace) 下的全部 group(含 "" 表示 P2P 队列),逐组清 Redis + 一次清 MySQL
func (*HybridOfflineMessageHandler) DeleteOfflineMessages ¶
func (h *HybridOfflineMessageHandler) DeleteOfflineMessages(ctx context.Context, userID string, messageIDs []string) error
DeleteOfflineMessages 删除已推送的离线消息(appID/namespace 从 ctx 提取)
func (*HybridOfflineMessageHandler) DrainOfflineQueue ¶
func (h *HybridOfflineMessageHandler) DrainOfflineQueue(ctx context.Context, userID string, limit int) ([]*models.HubMessage, error)
DrainOfflineQueue 排空 Redis 队列(单组 FIFO,仅 Redis) 按 ctx 的单个 (appID, ns, group) 出队;limit<=0 表示一次取尽该队列
func (*HybridOfflineMessageHandler) GetOfflineMessageCount ¶
func (h *HybridOfflineMessageHandler) GetOfflineMessageCount(ctx context.Context, userID string) (int64, error)
GetOfflineMessageCount 获取离线消息数量(MySQL 跨组计数) MySQL 为双写超集(含 Redis 内 + Redis 已过期的全部待推送消息),故直接以 MySQL 计数为准
func (*HybridOfflineMessageHandler) GetOfflineMessages ¶
func (h *HybridOfflineMessageHandler) GetOfflineMessages(ctx context.Context, userID string, limit int, cursor string) ([]*models.HubMessage, string, error)
GetOfflineMessages 查询 MySQL 离线消息(跨组:命名空间内该用户全部 group) Redis 队列由 DrainOfflineQueue 单独排空;本方法只负责 MySQL 持久层的分页查询
参数:
- userID: 用户ID
- limit: >0 最多返回指定数量;<=0 最多 1 万条
- cursor: 上次返回的最后一条 message_id,空串从头开始
返回 nextCursor 为空表示无更多数据
func (*HybridOfflineMessageHandler) StoreOfflineMessage ¶
func (h *HybridOfflineMessageHandler) StoreOfflineMessage(ctx context.Context, userID string, msg *models.HubMessage) error
StoreOfflineMessage 存储离线消息
多端登录场景说明: 当用户有多个设备(如ABC三个设备)时: - 如果ABC都离线:存储离线消息,任一设备上线时推送 - 如果AB在线C离线:消息已发送到AB,**不存储**离线消息,C上线后通过历史记录接口同步
核心原则: - **只有用户所有设备都离线时**,才存储离线消息并主动推送 - 有任何设备在线,消息已通过WebSocket实时送达,其他设备通过拉取历史记录获取 - 离线消息存储是基于用户维度的,用于在用户完全离线期间保证消息不丢失
去重机制: - 通过message_id保证消息唯一性(数据库unique索引) - 如果同一条消息重复存储,数据库层面会报错,但不影响功能
性能优化:Redis 和 MySQL 双写并行化
- 原实现:顺序执行 storeToRedis + storeToDatabase,延迟 = T(Redis) + T(MySQL)
- 现实现:并行执行,延迟 = max(T(Redis), T(MySQL))
- msg 为只读(两个 goroutine 仅读取字段,无并发修改)
- queueRepo(Redis 客户端)和 dbRepo(GORM 连接池)均为并发安全
func (*HybridOfflineMessageHandler) UpdatePushStatus ¶
func (h *HybridOfflineMessageHandler) UpdatePushStatus(ctx context.Context, messageIDs []string, pushErr error) error
UpdatePushStatus 更新离线消息推送状态 pushErr为nil表示推送成功,非nil表示推送失败
type Manager ¶
type Manager struct {
// contains filtered or unexported fields
}
Manager 消息域管理器
组件注入采用「未注入即降级」语义:
- ackManager 为 nil 时 ACK 快路径跳过
- ackTimeoutTimer 为 nil 时跨节点 ACK 超时调度 no-op(兜底扫描仍在)
- offlineHandler 为 nil 时投递兜底仅计数不转存
- idGenerator 为 nil 时需调用方保证 msg.ID 非空
func (*Manager) BroadcastAfterDelay ¶
func (m *Manager) BroadcastAfterDelay(ctx context.Context, msg *models.HubMessage, delay time.Duration)
BroadcastAfterDelay 延迟广播消息(全局广播,走 Deliver)
func (*Manager) BroadcastByUserType ¶
func (m *Manager) BroadcastByUserType(ctx context.Context, userType models.UserType, msg *models.HubMessage) int
BroadcastByUserType 发送消息给特定用户类型的所有客户端(经 attrIdx 定点,自适应回退全量)
func (*Manager) BroadcastExclude ¶
func (m *Manager) BroadcastExclude(ctx context.Context, msg *models.HubMessage, excludeUserIDs []string) int
BroadcastExclude 广播消息给所有客户端,但排除指定用户
func (*Manager) BroadcastFrameToUserIDs ¶
func (m *Manager) BroadcastFrameToUserIDs(ctx context.Context, userIDs []string, msg *models.HubMessage, frame []byte) int
BroadcastFrameToUserIDs 预编码帧直发扇出(聚合 drain 专用零二次编码入口)
frame 为已编码的 WS 帧字节(如聚合器产出的裸 JSON 数组帧),逐成员 TrySend 直发 消除常规路径的二次 Marshal;msg 仍承担三件事:
- 信封匹配键(AppID/Namespace/GroupIDs,per-device 过滤)
- SSE 对象通道(TrySendSSE,SSE 无预编码通道)
- TrySend 失败时的成员级分级兜底(routeDeliveryFallback)
frame 为空时回退一次 Marshal(与常规路径同语义,防御调用方未编码场景) 准入/整形/分片并行/顺序保证与 BroadcastToUserIDs 完全一致
func (*Manager) BroadcastPriority ¶
func (m *Manager) BroadcastPriority(ctx context.Context, msg *models.HubMessage, priority models.Priority)
BroadcastPriority 根据优先级广播消息(全局广播,走 Deliver)
func (*Manager) BroadcastToClientType ¶
func (m *Manager) BroadcastToClientType(ctx context.Context, clientType models.ClientType, msg *models.HubMessage) int
BroadcastToClientType 发送消息给特定客户端类型(经 attrIdx 定点,自适应回退全量)
func (*Manager) BroadcastToDepartment ¶
func (m *Manager) BroadcastToDepartment(ctx context.Context, department models.Department, msg *models.HubMessage) int
BroadcastToDepartment 发送消息给特定部门的所有用户(经 attrIdx 定点,自适应回退全量)
func (*Manager) BroadcastToFiltered ¶
func (m *Manager) BroadcastToFiltered(ctx context.Context, condition func(*models.Client) bool, msg *models.HubMessage) int
BroadcastToFiltered 预序列化消息并直接发送给符合条件的客户端 消除逐客户端 Clone/序列化/入队/DB 记录开销:
- 消息只 json.Marshal 1 次(原方案每客户端 1 次)
- 不走 SendToUserWithRetry(原方案每客户端 Clone×2 + 在线检查 + 入队 + DB 记录)
- 零拷贝遍历(原方案 GetClientsCopy + FilterSlice 双重拷贝)
削峰填谷接线(分级准入 + 出向整形):
- Admit 拒绝(VerdictDelay/Offline)→ 延迟队列(填谷重投),队列满走分级兜底
- shaper 拒绝(洪峰整形)→ 延迟队列;必达级跳过整形(与 Admit 矩阵的"必达恒放行"一致)
func (*Manager) BroadcastToNamespace ¶
func (m *Manager) BroadcastToNamespace(ctx context.Context, namespace string, msg *models.HubMessage) int
BroadcastToNamespace 命名空间广播统一入口(本地投递 deliverToNamespace 与 跨节点消费侧 hub.handleDistributedBroadcast 共用,保证两端路径一致)
经 nsIdx 反向索引定点反查该命名空间的本地在线用户后 BroadcastToUserIDs 扇出, 替代 BroadcastToFiltered 对全部连接的 O(N) 全量扫描:
- 多租户 pod 上命名空间广播命中率通常极低(如 10w 连接中目标 ns 仅 10 个用户),索引将 O(全部连接) 收敛为 O(该 ns 本地在线用户)
- 空命中直接返回(索引与注册表在同一 AddClient/RemoveClient 内事务性维护,无该 ns 用户时全量扫描同样 0 投递,跳过序列化与准入开销)
自适应回退:命中用户数 ≥ 总连接数的 nsBroadcastFullScanPercent% 时退回全量扫描 (索引路径逐 userID 查锁的单价更高,大命中率下 64 shard 并行批量遍历反而更快)
msg 信封在本入口统一同步(ContextWithRoute):appID 归一化 + namespace 显式写入 确保 BroadcastToUserIDs 的 per-device 信封过滤(ClientMatchesEnvelope)与目标 ns 一致
func (*Manager) BroadcastToRole ¶
func (m *Manager) BroadcastToRole(ctx context.Context, role models.UserRole, msg *models.HubMessage) int
BroadcastToRole 发送消息给特定角色的所有用户(经 attrIdx 定点,自适应回退全量)
func (*Manager) BroadcastToSSEClients ¶
func (m *Manager) BroadcastToSSEClients(msg *models.HubMessage)
BroadcastToSSEClients 广播给全部 SSE 客户端(appId/namespace 信封隔离) 通过 ForEachSSEClientParallel 并行分片读锁遍历(百万级优化)
func (*Manager) BroadcastToUserIDs ¶
func (m *Manager) BroadcastToUserIDs(ctx context.Context, userIDs []string, msg *models.HubMessage) int
BroadcastToUserIDs 预序列化消息并直接发送给指定用户ID列表的在线客户端 O(m) 复杂度(m=用户数),按成员ID反查 shardedRegistry,仅锁定相关 shard 相比 BroadcastToFiltered 的 O(n)(n=总连接数),群组广播场景大幅减少遍历与锁范围 适用于已知目标用户ID列表的场景(群组广播、多群组广播)
削峰填谷接线:同 BroadcastToFiltered(分级准入 + 出向整形 + 延迟队列路由)
func (*Manager) CleanupExpiredAcks ¶
CleanupExpiredAcks 清理过期的 ACK 待确认消息(未注入 ACK 管理器时 no-op 返回 0)
func (*Manager) DeleteMessageRecordByMessageID ¶
DeleteMessageRecordByMessageID 根据消息ID删除消息记录 参数:
- ctx: 上下文(用于超时控制和取消)
- messageID: 消息ID
func (*Manager) Deliver ¶
func (m *Manager) Deliver(ctx context.Context, msg *models.HubMessage, excludeSender bool) *models.DeliverResult
Deliver 统一消息投递入口 — 路由全由 ctx + msg 决定,一套逻辑打通所有场景
路由元数据来源:ctx(appID/namespace/groupIDs)+ msg(Receiver/RequireAck) 调用方通过 routing.NewRoute().WithAppID(appID).WithNamespace(ns).WithGroupIDs(gids).Inject(ctx) 注入路由后调一次 Deliver。
决策树(按优先级):
- msg.Receiver != "" → P2P(SendToUserWithRetry,在线投递 + 离线存储 + 重试)
- len(groupIDs) > 0: - msg.RequireAck == true → 群组可靠投递(per-member SendToUserWithRetry + 离线存储) - msg.RequireAck == false → 群组广播(fire-and-forget,BroadcastToUserIDs + 跨节点)
- namespace != "" → 命名空间广播(BroadcastToFiltered + 跨节点 ns 广播)
- namespace == "" → 全局广播(handleBroadcast + clusterBatcher)
群组投递语义:ns 为来源租户记号(必传校验,81407),成员定位按 (appID, groupIDs) 跨 ns 聚合(业务侧按租户建组、同 gid 多实例,投递信封 ns 置空=通配,跨 ns 成员实时送达)
excludeSender 仅群组场景生效(P2P/广播场景 msg.Sender 不参与过滤)
返回非 nil *models.DeliverResult,错误收集到 result.Errors;按 result.Mode + 计数字段判断结果
func (*Manager) EndUserReplay ¶
EndUserReplay 结束回放并按序补投暂存的实时投递(registry 任务闭包 defer 调用; 放弃回放权的触发方不得调用,否则会误开 inflight 回放的门闩)
func (*Manager) GetClientsByClientType ¶
func (m *Manager) GetClientsByClientType(clientType models.ClientType) []*models.Client
GetClientsByClientType 按客户端类型获取客户端(委托 FilterClients 零拷贝)
func (*Manager) GetClientsByDepartment ¶
func (m *Manager) GetClientsByDepartment(department models.Department) []*models.Client
GetClientsByDepartment 获取特定部门的所有客户端(委托 FilterClients 零拷贝)
func (*Manager) GetClientsByRole ¶
GetClientsByRole 获取特定角色的所有客户端(委托 FilterClients 零拷贝)
func (*Manager) GetClientsByUserType ¶
GetClientsByUserType 获取特定用户类型的所有客户端(委托 FilterClients 零拷贝)
func (*Manager) GetClientsByVIPLevel ¶
GetClientsByVIPLevel 获取特定VIP等级及以上的客户端(委托 FilterClients 零拷贝)
func (*Manager) HandleBroadcastMessage ¶
func (m *Manager) HandleBroadcastMessage(ctx context.Context, msg *models.HubMessage)
HandleBroadcastMessage 处理广播消息
削峰填谷接线(与 broadcastToFiltered 同一套基座):
- 准入闸门(分级水位):拒绝时延迟队列重投(填谷)/ 转离线,拒绝不等于丢弃
- 出向整形(GCRA):洪峰平滑扇出,被拒时进延迟队列;必达级跳过
- 重投目标 doBroadcastMessage 不重复准入(防死循环,与 broadcastToFilteredNow 同语义)
func (*Manager) HoldUserDelivery ¶
HoldUserDelivery 用户级投递过闸(回放中暂存、开闸后立即执行)
func (*Manager) InvokeErrorCallback ¶
func (m *Manager) InvokeErrorCallback(ctx context.Context, err error, severity models.ErrorSeverity) error
InvokeErrorCallback 触发错误处理回调 此方法用于统一处理各种错误
func (*Manager) InvokeMessageReceivedCallback ¶
func (m *Manager) InvokeMessageReceivedCallback(ctx context.Context, client *models.Client, msg *models.HubMessage) error
InvokeMessageReceivedCallback 触发消息接收回调
func (*Manager) NotifyObservers ¶
func (m *Manager) NotifyObservers(ctx context.Context, msg *models.HubMessage)
NotifyObservers 通知观察者(观察者未启用时为 no-op) 从 ctx 提取 namespace+groupIDs 定位观察范围,提交批量处理器攒批投递
func (*Manager) NotifyObserversDirect ¶
func (m *Manager) NotifyObserversDirect(msg *models.HubMessage, namespace string, groupIDs []string)
NotifyObserversDirect 直接通知观察者(不经批处理队列,避免递归入队) 由 observerBatcher flush 调用:本地观察者投递 + 跨节点广播,无 per-message goroutine
func (*Manager) OfferAggregatable ¶
func (m *Manager) OfferAggregatable(ctx context.Context, msg *models.HubMessage, excludeSender bool) bool
OfferAggregatable 可聚合档消息入窗尝试(群组广播链路统一接线点)
返回 true = 已入本地聚合窗口:调用方跳过即时本地扇出(drain 窗口批量扇出,各节点对本地成员各自聚合——源节点 + 跨节点入站流共用本入口) 返回 false = 不聚合或降级直通(调用方走原路径,性能降级不丢消息):
- 聚合器未启用 / 消息非可聚合档 / 多群组 / 排除发送者(窗口扇出无法复现逐成员排除语义)
- 窗口满 / 群组数满(Offer 降级直通)
- 序列化失败(编码前置一次,失败直通单发)
编码前置:入窗即完成本消息的序列化(drain 拼装为纯字节拼接,零重编码)
func (*Manager) PushOfflineMessages ¶
func (m *Manager) PushOfflineMessages(ctx context.Context, client *models.Client) (pushedIDs, failedIDs []string)
PushOfflineMessages 用户上线离线消息回放(两阶段全量补发)
全量补发策略(双写存储下的两阶段推送;全部状态在注入的共享存储, Deployment 滚动更新 / Pod 重新调度下跨 Pod 可见,Pod 本地零依赖):
- 按组 drain Redis 队列(FIFO,短期高性能消息优先投递):枚举用户在该 namespace 下加入的全部 group + P2P(空 group)队列,drain 出的消息推送 成功后按 message_id 删 MySQL(双写去重,避免阶段 2 重复推送)
- 跨组分页查 MySQL 剩余消息(Redis 已过期 / drain 异常残留),推送后删除
降级:groupRepo 不可用时只 drain P2P 队列 + 跨组查 MySQL(MySQL 跨组覆盖 所有 group 的消息,不丢消息,仅 Redis 短期队列残留待自然过期)
未注入离线处理器时返回空切片(nil-safe 降级);推送失败的离线源消息不再 重新转存(send.go 离线源防循环),仅更新推送状态保留待下次上线重试。 返回成功/失败推送的 messageID 列表(编排层据此触发应用回调)
func (*Manager) QueryMessageRecord ¶
func (m *Manager) QueryMessageRecord(ctx context.Context, messageID, receiver string) (*models.MessageSendRecord, error)
QueryMessageRecord 根据消息ID+接收者查询消息记录 参数:
- ctx: 上下文(用于超时控制和取消)
- messageID: 消息ID
- receiver: 接收者ID(广播类记录传空字符串)
返回:
- *models.MessageSendRecord: 消息记录
- error: 错误信息
func (*Manager) QueryMessageRecordsByClientIP ¶
func (m *Manager) QueryMessageRecordsByClientIP(ctx context.Context, clientIP string, limit int) ([]*models.MessageSendRecord, error)
QueryMessageRecordsByClientIP 根据客户端IP查询消息记录 参数:
- ctx: 上下文(用于超时控制和取消)
- clientIP: 客户端IP地址
- limit: 返回结果数量限制(0 表示不限制)
func (*Manager) QueryMessageRecordsByNodeIP ¶
func (m *Manager) QueryMessageRecordsByNodeIP(ctx context.Context, nodeIP string, limit int) ([]*models.MessageSendRecord, error)
QueryMessageRecordsByNodeIP 根据节点IP查询消息记录 参数:
- ctx: 上下文(用于超时控制和取消)
- nodeIP: 服务器节点IP
- limit: 返回结果数量限制(0 表示不限制)
func (*Manager) QueryMessageRecordsByReceiver ¶
func (m *Manager) QueryMessageRecordsByReceiver(ctx context.Context, receiver string, limit int) ([]*models.MessageSendRecord, error)
QueryMessageRecordsByReceiver 根据接收者查询消息记录 参数:
- ctx: 上下文(用于超时控制和取消)
- receiver: 接收者ID
- limit: 返回结果数量限制(0 表示不限制)
func (*Manager) QueryMessageRecordsBySender ¶
func (m *Manager) QueryMessageRecordsBySender(ctx context.Context, sender string, limit int) ([]*models.MessageSendRecord, error)
QueryMessageRecordsBySender 根据发送者查询消息记录 参数:
- ctx: 上下文(用于超时控制和取消)
- sender: 发送者ID
- limit: 返回结果数量限制(0 表示不限制)
func (*Manager) QueryMessageRecordsByStatus ¶
func (m *Manager) QueryMessageRecordsByStatus(ctx context.Context, status models.MessageSendStatus, limit int) ([]*models.MessageSendRecord, error)
QueryMessageRecordsByStatus 根据状态查询消息记录 参数:
- ctx: 上下文(用于超时控制和取消)
- status: 消息状态
- limit: 返回结果数量限制(0 表示不限制)
func (*Manager) QueryRetryableMessageRecords ¶
func (m *Manager) QueryRetryableMessageRecords(ctx context.Context, limit int) ([]*models.MessageSendRecord, error)
QueryRetryableMessageRecords 查询可重试的消息记录 参数:
- ctx: 上下文(用于超时控制和取消)
- limit: 返回结果数量限制(0 表示不限制)
func (*Manager) ScanNodeAckTimeouts ¶
func (m *Manager) ScanNodeAckTimeouts()
ScanNodeAckTimeouts 跨节点投递 ACK 超时兜底扫描(见 node_ack_timeout.go)
func (*Manager) ScheduleAckTimeouts ¶
func (m *Manager) ScheduleAckTimeouts(keys []models.MessageRecordKey)
ScheduleAckTimeouts 批量注册跨节点 ACK 超时任务(outbox flush 成功后回调, 仅传入状态仍为 sending 的记录键;注册延后 = outbox 攒批间隔,秒级超时语义无损)
func (*Manager) SendConditional ¶
func (m *Manager) SendConditional(ctx context.Context, condition func(*models.Client) bool, msg *models.HubMessage) int
SendConditional 根据条件发送消息给符合条件的客户端 复用 BroadcastToFiltered:预序列化一次 + 直接 TrySend,避免逐客户端 Clone/序列化/入队/DB 记录
func (*Manager) SendPriority ¶
func (m *Manager) SendPriority(ctx context.Context, userID string, msg *models.HubMessage, priority models.Priority)
SendPriority 根据优先级发送消息
func (*Manager) SendToAllClientsInMap ¶
func (m *Manager) SendToAllClientsInMap(ctx context.Context, clientMap map[string]*models.Client, msg *models.HubMessage)
SendToAllClientsInMap 发送消息到映射中的所有客户端 预序列化一次消息,避免对每个客户端重复 json.Marshal trace 恢复:ctx 无 trace 时从消息信封恢复(msg.ContextFrom),保证投递日志 与消息原始链路同一 trace_id(此前的 host.Context() 是 trace 断点)
func (*Manager) SendToClient ¶
SendToClient 发送消息到客户端(内部序列化)
func (*Manager) SendToClientSerialized ¶
func (m *Manager) SendToClientSerialized(ctx context.Context, client *models.Client, msg *models.HubMessage, preSerialized []byte) bool
SendToClientSerialized 发送消息到客户端(支持预序列化数据) preSerialized 为预序列化的 []byte,为 nil 时内部序列化 SSE 客户端忽略 preSerialized,直接发送 msg 对象 返回是否成功投递到客户端通道(跨节点 PubSub 路径据此统计投递成败)
分级投递接线:
- 高频级(Ephemeral)→ Coalescer latest-wins 合并(50ms 周期投递最新值)
- Admit 拒绝(Delay/Offline,P2P 场景仅极端水位触发)→ 转离线补发 + 上层重试兜底
- 实时路径零改动(TrySend 成功路径)
失败兜底按送达分级路由(与广播路径 TrySendWithFallback 同一语义):
- 高频级:丢弃计数(latest-wins 旧值过期是正确行为,转离线会让用户上线后收到一堆过期的弹幕/typing 状态消息)
- 普通级:转离线补发(拒绝≠丢弃)
func (*Manager) SendToClientsWithRetry ¶
func (m *Manager) SendToClientsWithRetry(ctx context.Context, clients []*models.Client, msg *models.HubMessage, maxRetries int) map[string]*models.SendResult
SendToClientsWithRetry 发送消息给多个客户端(带重试) 使用预分配 slice + 索引定位写入,消除 mutex(每个 goroutine 写不同索引,无竞争)
func (*Manager) SendToGroupMembers ¶
func (m *Manager) SendToGroupMembers(ctx context.Context, memberIDs []string, msg *models.HubMessage, excludeSender bool) *models.BroadcastResult
SendToGroupMembers 向会话成员批量发送消息(兼容旧版本接口) 参数:
- ctx: 上下文
- memberIDs: 成员ID列表
- msg: 要发送的消息
- excludeSender: 是否排除发送者本身
返回:
- models.BroadcastResult: 广播结果,包含成功、失败、离线统计
示例:
向会话成员广播,排除发送者自己 result := hub.SendToGroupMembers(ctx, memberIDs, msg, true) 简单批量发送(不排除发送者) result := hub.SendToGroupMembers(ctx, userIDs, msg, false)
func (*Manager) SendToMultipleUsers ¶
func (m *Manager) SendToMultipleUsers(ctx context.Context, userIDs []string, msg *models.HubMessage) map[string]error
SendToMultipleUsers 并发发送消息给多个用户 使用 runFanout 有界并发投递 + 预分配 slice + 索引写入,消除 mutex 竞争
func (*Manager) SendToUserViaSSE ¶
func (m *Manager) SendToUserViaSSE(userID string, msg *models.HubMessage) bool
SendToUserViaSSE 经 SSE 通道向用户投递(SSE 未启用或用户无订阅时返回 false) namespace 隔离:msg.Namespace 非空时仅投递给同 ns 的 SSE 设备,避免跨 ns 串扰
func (*Manager) SendToUserWithRetry ¶
func (m *Manager) SendToUserWithRetry(ctx context.Context, toUserID string, msg *models.HubMessage) *models.SendResult
SendToUserWithRetry 带重试机制的发送消息给指定用户 公开 P2P 入口:先 EnsureRouteDefaults 归一化 appID/namespace(P2P 严格契约,空补默认值) 内部路径(sendToUserWithRetry/sendToUser)不再归一化——群组可靠投递的 per-member 扇出走同一管道,信封 ns="" 为跨 ns 通配语义(见 Deliver 群组分派器),归一化会破坏通配
func (*Manager) SendWithCallback ¶
func (m *Manager) SendWithCallback(ctx context.Context, userID string, msg *models.HubMessage, onSuccess func(*models.SendResult), onError func(error))
SendWithCallback 发送消息并在完成时执行回调
func (*Manager) StartClientPumps ¶
StartClientPumps 启动客户端读写泵并安装协议级 PING 处理器(编排层注册路径调用) 在注册临界区后调用:此后 socket 有了唯一读者(读泵)与唯一写者(写泵), 泵启动前到达的消息由注册入口初始化的 SendChan 缓冲吸收,启动后排空,无丢失。 SSE 连接短路返回:SSE 无 gorilla 连接语义,写由 SSE 专用通道驱动(见 transport/sse.go)
func (*Manager) StoreOfflineOnDeliveryFailure ¶
func (m *Manager) StoreOfflineOnDeliveryFailure(msg *models.HubMessage, deliveryErr error)
StoreOfflineOnDeliveryFailure 在在线投递失败时异步转存离线消息
触发条件(全部满足才转存):
- offlineMessageHandler 可用
- msg.Receiver 非空(P2P 消息,广播场景不转存)
- msg.Source != offline(离线推送本身失败不重新存,避免无限循环)
状态流转:
- 转存成功 → message_record 状态从 Failed 覆盖为 UserOffline(消息已安全暂存,等用户上线推送)
- 转存失败 → 保持调用方已标记的 Failed 状态(消息确实丢了)
注意:多设备场景下若部分设备投递成功部分失败,失败设备仍会触发转存 这不会导致重复推送:用户上线 drain 时 pushAndDeleteOffline 成功后按 message_id 删 MySQL,客户端也可按 message_id 去重
func (*Manager) SwapBroadcastFallbackCount ¶
SwapBroadcastFallbackCount 取出并清零广播兜底转存计数
func (*Manager) SwapBroadcastSentCount ¶
SwapBroadcastSentCount 取出并清零广播发送计数
func (*Manager) SwapMessageSentCount ¶
SwapMessageSentCount 取出并清零点对点消息发送计数
func (*Manager) TryBeginUserReplay ¶
TryBeginUserReplay 尝试标记用户进入首连离线回放,返回是否获得回放权 registry 首连分支调用:速断速连窗口同一用户可叠加多次首连触发(上线→回落→再上线) 并发回放会重复 drain 同一队列(离线消息重复投递)、且首个回放的 end 会提前开闸(乱序窗口重现); inflight 回放刚 drain 过队列,重复触发放弃回放权是安全的(期间新转离线的消息由下次上线兜底)
func (*Manager) TryDeliverToClient ¶
func (m *Manager) TryDeliverToClient(ctx context.Context, client *models.Client, msg *models.HubMessage, data []byte) bool
TryDeliverToClient 返回实际准入或入队结果,离线兜底由汇总投递结果的调用方负责
func (*Manager) TrySendWithFallback ¶
func (m *Manager) TrySendWithFallback(client *models.Client, data []byte, msg *models.HubMessage, guarantee models.DeliveryGuarantee) bool
TrySendWithFallback 带分级兜底的非阻塞投递(广播扇出内部循环用)
性能契约:成功路径 = TrySend 一次(零新增指令);失败路径才走兜底路由 guarantee 由调用方在扇出循环外预解析一次传入,复用成功路径送达埋点与失败兜底路由, 消除逐客户端重复 ResolveGuarantee 返回:是否实时送达(false = 已转离线/按高频语义处理,消息不会静默丢失)
func (*Manager) WithAckManager ¶
func (m *Manager) WithAckManager(am *AckManager) *Manager
WithAckManager 注入 ACK 管理器
func (*Manager) WithAckTimeoutTimer ¶
func (m *Manager) WithAckTimeoutTimer(t *syncx.HashedWheelTimer) *Manager
WithAckTimeoutTimer 注入跨节点 ACK 超时时间轮
func (*Manager) WithErrorCallback ¶
func (m *Manager) WithErrorCallback(cb ErrorCallback) *Manager
WithErrorCallback 注入错误处理回调
func (*Manager) WithIDGenerator ¶
func (m *Manager) WithIDGenerator(g models.IDGenerator) *Manager
WithIDGenerator 注入消息 ID 生成器
func (*Manager) WithMessageReceivedCallback ¶
func (m *Manager) WithMessageReceivedCallback(cb MessageReceivedCallback) *Manager
WithMessageReceivedCallback 注入消息接收回调
func (*Manager) WithMessageSendCallback ¶
func (m *Manager) WithMessageSendCallback(cb MessageSendCallback) *Manager
WithMessageSendCallback 注入消息发送完成回调
func (*Manager) WithOfflineHandler ¶
func (m *Manager) WithOfflineHandler(h spi.OfflineQueue) *Manager
WithOfflineHandler 注入离线消息处理器(spi.OfflineQueue 契约;混合实现见 HybridOfflineMessageHandler)
func (*Manager) WithReplayGate ¶
WithReplayGate 注入首连回放门闩(未注入时投递直通,行为与旧版一致)
func (*Manager) WithWorkerPool ¶
func (m *Manager) WithWorkerPool(wp *HubWorkerPool) *Manager
WithWorkerPool 注入工作池集合
type MessageReceivedCallback ¶
type MessageReceivedCallback func(ctx context.Context, client *models.Client, msg *models.HubMessage) error
MessageReceivedCallback 消息接收回调(客户端上行消息交给业务层处理)
type MessageSendCallback ¶
type MessageSendCallback func(msg *models.HubMessage, result *models.SendResult)
MessageSendCallback 消息发送完成回调
type OfflineDeps ¶
type OfflineDeps struct {
// Queue Redis 队列实现(如 redisadapter.NewOfflineQueue(...))
Queue spi.MessageQueue
// Store RDBMS 持久化实现(如 gormadapter.NewOfflineStoreFor(...))
Store spi.OfflineStore
// Config 离线消息配置
Config *wscconfig.OfflineMessage
// Logger 日志器(nil 使用默认)
Logger spi.Logger
}
OfflineDeps 混合离线消息处理器的两半依赖
该处理器同时需要 Redis 队列与 RDBMS 持久化,但两者分属不同适配器, 单个适配器的 hook 都拿不到完整依赖 —— 故由本函数在两侧 hook 跑完后统一组装。
type PendingMessage ¶
type PendingMessage struct {
Message *models.HubMessage // 原始消息
AckChan chan *AckMessage // ACK确认通道
Timestamp time.Time // 发送时间
Timeout time.Duration // 超时时间
Retry int // 重试次数
MaxRetry int // 最大重试次数
// contains filtered or unexported fields
}
PendingMessage 待确认消息
func (*PendingMessage) WaitForAck ¶
func (pm *PendingMessage) WaitForAck() (*AckMessage, error)
WaitForAck 等待ACK确认
func (*PendingMessage) WaitForAckWithRetry ¶
func (pm *PendingMessage) WaitForAckWithRetry(retryFunc func() error) (*AckMessage, error)
WaitForAckWithRetry 等待ACK确认并支持重试