01 开发心得-ServiceContext设计与使用

2 阅读4分钟

VSS 开发心得:ServiceContext 的设计与使用

ServiceContext(代码里写作 svcCtx)是 VSS 进程内的中心依赖容器:配置、RPC 客户端、Redis、各类 SIP 出站 channel、并发安全的 map/set 状态、以及 gosip 服务端句柄都挂在这里。理解它的边界使用约定,比零散看单个 Logic 文件更容易把「HTTP / SIP 收 / SIP 发 / WS / SSE」串成一条线。

类型定义core/app/sev/vss/internal/types/types.goServiceContext struct
构造core/app/sev/vss/internal/svc/service_context.goNewServiceContext
注入入口main.gosvcCtx := svc.NewServiceContext(c),再传给 HTTP/SIP/WS/SSE 与各类 SipProc


1. 设计目标:一个进程,一块共享状态

VSS 不是「每个请求 new 一套服务」,而是 单进程多协程

  • 收 SIP:gosip 在 OnRequest 里回调,RegisterHandlers(svcCtx) 闭包捕获 svcCtx
  • 发 SIP:业务只往 SipSendXXX channel 里投递;SendLogic 统一消费。
  • HTTP/WS:Handler 同样拿 svcCtx 调 RPC 或往 channel 塞任务。

把共享资源收敛到 ServiceContext,好处是:

  • 依赖显式:打开 struct 就知道能碰哪些客户端、哪些队列。
  • 生命周期单一:随进程创建、随进程退出。
  • 避免隐形全局变量:除个别包级工具外,业务逻辑以 svcCtx 入参为主。

代价是:谁都能改 map、谁都能写 channel,需要团队纪律。


2. NewServiceContext

NewServiceContext初始化了两类东西:

类别代表字段说明
不可变「配置 / 客户端」ConfigRpcClientsRedisClient启动后一般只读;RPC 带拦截器(重试、keepalive、Api2DBRpc 鉴权等)。
可变「运行时状态」各类 chanxmap.XMapset.CSetBroadcast多 goroutine 读写:channel 协程约定XMap/CSet 自身线程安全,普通 DictionaryMap 是原生 map(见 §3)。

另外:

  • Setting:先 New 零值,FetchDataLogic 周期拉配置后覆盖/合并。
  • DictionaryMap / OnvifDiscoverDevices / MediaServerRecords / DeviceOnlineState:由 FetchDataLogic 刷新;消费者读内存即可。
  • GBSTCPSev / GBSUDPSev:在 SipGbsServer Listen 成功后才赋值为 gosip Server 指针出站 Send(TCP 事务)依赖其非空gbs_send.go)。

3. 字段速查:先归类

3.1 SIP 出站总线(生产者 → SendLogic

缓冲 channel,投递即意图,不要在热路径上阻塞过久:

SipSendCatalogSipSendDeviceInfoSipSendVideoLiveInviteSipSendTalkInviteSipSendBye、控制/预置位/录像/订阅/广播/对讲、SipLog 等。

习惯:HTTP 或收包 Logic 只 <- 投递;真正 gosip 发送在 SendLogic + GBSSender

3.2 定时与状态机(channel + map)

  • SipCatalogLoop / SipCatalogLoopMap:注册上下线维护 Catalog 任务;SipHeartbeatLoop / SipHeartbeatLoopMap:过期与心跳超时。
  • SetDeviceOnline / DeviceOnlineStateUpdateMap:设备/通道在线写 DB 的批量聚合入口(见设备在线状态文档)。
  • AckRequestMap:INVITE 后 ACK/BYE 依赖的对话缓存(key 含设备、流名等)。

习惯map 的 key 语义(设备 ID vs 通道 ID vs streamName)一旦混用,排障成本极高;新增 key 时写清注释并全局 grep 消费方。

3.3 并发安全注意点

DictionaryMapmap[string]*categories.Item[...](原生 map)

FetchDataLogic.dictionaries 里是 l.svcCtx.DictionaryMap = maps 整体替换,读侧若在无锁环境下长期持有旧指针再读写,理论上仍有可见性习惯问题;工程上约定:业务只读 map,不在 HTTP 请求里写 DictionaryMap。若未来改为细粒度更新,需要 mutex 或原子替换策略 再评审。

xmap.XMapset.CSet:仓库封装为并发安全结构,可放心在多协程 Get/Set(仍注意 业务逻辑上的覆盖与竞态,例如同一 key 被两处覆盖)。

3.4 流与 invite 防击穿的集合

  • InviteRequestState + InviteRequestLock:同一 streamName 并发 invite 控制。
  • PubStreamExistsState:媒体侧流存在标记。
  • FetchDeviceVideoState:录像信息拉取互斥场景。

4. 生命周期与 InitFetchDataState(容易忽略的细节)

ServiceContext 内有 InitFetchDataState sync.WaitGroup

  • main 里在启动 SipProc 前执行 InitFetchDataState.Add(2),与 FetchDataLogic 中两条分支各 Done() 一次 配对(字典 / 设置链)。
  • SendLogicCatalogLoopLogicHeartbeatOfflineLogic出站与定时任务DO 开头 Wait():保证 首帧字典、Setting、媒体列表、在线快照、ONVIF 等尽量就绪 后再发信令或定时任务。

注意sync.WaitGroup 在计数为 0 时,Wait() 会立即返回
SipGbsServer 入口也有 InitFetchDataState.Wait():若在 Add(2) 之前 gosip 监听协程已跑到这里,则 不会阻塞——即 收 SIP 与「FetchData 完成」之间可能存在时间窗。实际业务是否在 Logic 内强依赖 Setting,决定在极端冷启动下是否会有短暂不一致。出站侧则普遍 显式 Wait

开发心得:新增「必须在首包数据就绪后才能跑」的逻辑时,优先与现有 proc 一样 InitFetchDataState.Wait();若仅依赖本地 Config 即可,可不必 Wait,但要在代码审查时说清。


5. 使用约定

  1. 不要长期持有 svcCtx 的子字段切片指针跨 goroutine 写:例如从 OnvifDiscoverDevices 切一片子 slice 再在后台改元素,除非保证生命周期与只读。
  2. 往 channel 投递考虑阻塞:缓冲区满会卡住 HTTP 或 SIP 收包线程;必要时应 select + 超时/丢弃策略(与 SipLog 等思路一致)。
  3. 新增全局状态时:优先 xmap / set + 明确 key;避免再增加无锁原生 map。
  4. TCP SIP 发送:确保 Listen 已完成、GBSTCPSev 已赋值,再依赖 RequestWithContext/Send;单元测试里常需 mock 或跳过真发送。

6. 小结

  • ServiceContext = 配置 + RPC + Redis + SIP 队列 + 运行时 map/set + gosip Server 指针 的聚合体,是 VSS 的「小型服务定位器」。
  • 出站 SIP:统一 channel → SendLogic状态注册 / 心跳 / ACK / 流 各管一类 map,key 语义要统一。
  • InitFetchDataState:协调 FetchDataproc,注意 WaitGroup 在 Add 前的 Wait 不阻塞 与冷启动时间窗。
  • 线程安全DictionaryMap 原生 map 整体替换 仍为敏感点;其余优先用 xmap/set