VSS 国标信令发送设计:`SendLogic` 流水线

1 阅读5分钟

VSS 国标信令发送设计:SendLogic 流水线

本文将详细说明VSS项目中信令用什么发从设备注册到发出 Catalog 数据如何串起来、types.Request 等各扮演什么角色

项目地址 github.com/openskeye/g…


一、SendLogic 是做什么的?

SendLogic 实现 types.SipProcLogic,在 main.go 里与其他 SIP 协程任务一并 DO 启动。其核心是 一个永不退出的 select,从 ServiceContext 上的 SipSendXXX channel 读任务,每种信令一个 case,在 独立 goroutine 里执行具体发送逻辑。

可以把 SendLogic 理解成 「出站 SIP 总线调度器」——业务层、定时器、HTTP 接口不直接调底层 gosip,而是 往 channel 里丢「发送意图」;由 SendLogic 统一消费,再交给 GBSSender 包装、发 UDP/TCP。


二、为什么要这么设计?

2.1 单一写入出口

SIP 在 gosip 的 OnRequest 回调里跑; 若在多处随意 Send,容易出现:

  • 并发写同一套连接/事务状态难排查;
  • 重试、节流、日志散落。

收敛到 一条 goroutine + 多路 channel,上层只 投递任务,结构清晰。

2.2 用 channel 做队列(带缓冲)

ServiceContext 里例如 SipSendCatalog: make(chan *types.Request, 100)(见 internal/svc/service_context.go)。避免短时突发高并发同步阻塞。

2.3 每个任务再起 goroutine:隔离耗时与 panic 边界

SendLogiccase 里普遍 go func() { ... }(v)。原因:

  • VideoLiveInviteRTPPubACKRtpPub 可能较慢;不能卡住 select,否则其他信令饿死。
  • defer recoverCall("发送处理") 在外层 DO 包一层恢复;子任务错误多 打日志 + 返回,避免整条总线挂死。

2.4 InitFetchDataState.Wait():等配置/初始化完成再发信令

DO 开头 l.svcCtx.InitFetchDataState.Wait(),与 SIP Server Listen、其它 proc 一致。RPC 客户端、平台配置、规则等未就绪时不发消息,减少「空配置乱发导致的无效包」。


三、从「设备注册」到「发 Catalog」数据如何串联起来?

国标设备先 REGISTER,平台 200 OK 后,平台才有稳定 To/From/Call-ID/Contact 内容去发 MESSAGEINVITE 等。本仓库把 「注册当次解析出来的 sip.Request 固化为 types.Request,后续 同一设备 的出站信令 尽量复用这份语境(Via/传输、对端 URI 等),由 GBSSender 按国标协议包装发送。

3.1 注册成功:RegisterLogic 「下游流水线」

文件:internal/logic/gbs_sip/register.go

设备 上线Expires 非 0,record.Online=1)时,在 RPC DeviceUpsert 成功后,异步go func + Sleep 1s)做三件事:

动作Channel / 含义
SipCatalogLoop <- SipCatalogLoopReq{Req, Online:true, Now}CatalogLoopLogic 把该设备 登记进 SipCatalogLoopMap,参与 定时 Catalog
SipSendCatalog <- req立刻发第一次 Catalog(刷新目录)。
SipSendDeviceInfo <- req立刻拉设备信息(另 case 进 SendLogic)。
SipHeartbeatLoop <- ...心跳/过期检测 登记(HeartbeatOfflineLogic)。

为什么要 Sleep 1s 再发? 给设备 注册事务收尾、网络栈稳定 一点时间,降低「刚 200 OK 立刻 MESSAGE 被丢」的概率,非强制使用。

types.Request 里带什么?types.go
Original(原始 sip.Request)、ID(设备 20 位编码)、SourceTransportProtocolDeviceAddr。这正是 GBSSenderVia/From/To 时与 「设备当时的注册包」 对齐的输入。

设备 下线Expires==0):SipCatalogLoop <- Online:falseSipCatalogLoopMap 移除,并清理心跳 map,避免离线仍刷 Catalog。

3.2 定时 Catalog:CatalogLoopLogic → 再投递 SipSendCatalog

文件:internal/logic/gbs_proc/catalog_loop.go

  • SipCatalogLoop channel增删 SipCatalogLoopMap 中的条目。
  • proc goroutine:每秒 tick,遍历 SipCatalogLoopMap;源码条件为:item.Onlineitem.Now%val.Unix() == Config.Sip.CatalogInterval(见 catalog_loop.go)。

条件item.Nowval.Unix() 均为 Unix 秒注册之后通常 val.Unix() > item.Now,此时在 Go 中 item.Now % val.Unix() 等于 item.Now(被除数小于除数)。于是等式 item.Now == Config.Sip.CatalogInterval 才会成立——一般配置下 CatalogInterval 为 60、3600 等小整数,几乎不会与注册秒级时间戳相等,因此 「定时器每秒扫 map」这条路径往往极少真正投递 SipSendCatalog。线上 目录刷新 更常来自:注册成功立刻 SipSendCatalog心跳发现 map 无记录时的补发keepalive.go)、以及 HTTP 手工触发。若产品期望 严格每 N 秒周期 Catalog,应对照 catalog_loop.go 判据 是否需改为例如 (val.Unix()-item.Now)%N == 0 一类实现。

3.3 SendLogic.catalog:节流后再 GBSSender.Catalog()

文件:send_sip_proc.go

dt.ThrottleFixedGridTrailing(req.ID, 3*time.Second, func() {
    sip2.NewGBSSender(l.svcCtx, req, req.ID).Catalog()
})

为什么还有一层 3 秒节流?
定时器 CatalogLoop注册/keepalive 可能 短时间多次SipSendCatalog 塞同一设备;ThrottleFixedGridTrailing(见 core/pkg/dt/throttle_fixed_grid.go)在 固定时间栅格内合并槽尾执行最后一次,避免 对同一设备 Catalog 大量重复请求(保护设备)。

3.4 信令组装发送

文件:internal/pkg/sip/gbs_send.go

  1. makeRequestBody(types.SipMessageGBSCatalog{ CmdType: Catalog, DeviceID: l.deviceUniqueId, SN: l.SN(...) })XML body
  2. makeRequest(MESSAGE, headers..., body):标准头 Via / From / To / Call-ID / CSeq / Content-Type / Content-Length
  3. Send(...) 走 gosip 客户端发往设备。

注意:NewGBSSender(l.svcCtx, req, req.ID) 第三个参数在此是 设备 ID;Catalog 的 DeviceID 在 XML 里也是它。req 提供 与注册一致的传输、对端地址等数据

3.5 流程图

sequenceDiagram
    participant Dev as 设备
    participant SipIn as gbs_sip 接收
    participant Reg as RegisterLogic
    participant CatL as CatalogLoopLogic
    participant Bus as SipSend* channels
    participant Send as SendLogic
    participant GB as GBSSender

    Dev->>SipIn: REGISTER
    SipIn->>Reg: DO
    Reg->>Dev: 200 OK
    Reg->>CatL: SipCatalogLoop Online+Req
    CatL->>CatL: SipCatalogLoopMap.Set
    Reg->>Bus: SipSendCatalog req
    Bus->>Send: catalog(req)
    Send->>GB: Catalog() MESSAGE
    GB->>Dev: Catalog XML

    loop 每秒 tick(见正文:proc 判据与「每 N 秒」可能不一致)
        CatL-->>Bus: 条件满足时 SipSendCatalog
        Bus->>Send: catalog
        Send->>GB: Catalog (+ throttle)
    end

四、SendLogic 支持哪些类型?

Channel处理函数
SipSendCatalogcatalog
SipSendDeviceInfodeviceInfo
SipSendVideoLiveInviteVideoLiveInvite
SipSendTalkInvitetalkInvite
SipSendByebye
SipSendBroadcastbroadcast
SipSendTalktalk
SipSendQueryPresetPoints / SetqueryPresets / setPresets
SipSendQueryVideoRecordsqueryVideoRecords
SipSendSubscriptionsubscription

五、重点流程

VideoLiveInvite 不在 GBSSender 里结束:它串联 MSSDP(见《SDP详解》《stream_play》):

  1. ms.RTPPub:MS 侧准备收流。
  2. VideoLiveInvite:SIP INVITE + 平台 SDP。
  3. AckReq + 缓存 AckRequestMapFrom/To/tag/Call-IDBYE)。
  4. sdp.ParseString(200 OK Body)设备 SDP
  5. SendDirect(ack) + ACKRtpPub:MS 与 对端 IP:端口、filesize 对齐。
  6. PubStreamExistsState 标记流存在。

为什么要 inviteStep + StepRecord前端/SSE/诊断 一条 可观测时间线(拉流成功、INVITE 原文、ACK 原文、完成/错误)。


六、源码索引

说明路径
出站调度core/app/sev/vss/internal/logic/gbs_proc/send_sip_proc.go
定时 Catalog 注册表core/app/sev/vss/internal/logic/gbs_proc/catalog_loop.go
注册后触发core/app/sev/vss/internal/logic/gbs_sip/register.go
心跳与 Catalog 补发core/app/sev/vss/internal/logic/gbs_sip/keepalive.go(map 内无记录时可 SipSendCatalog
Catalog MESSAGE 实现core/app/sev/vss/internal/pkg/sip/gbs_send.go Catalog()
请求语境模型core/app/sev/vss/internal/types/types.go RequestSipCatalogLoopReq
Channel 初始化core/app/sev/vss/internal/svc/service_context.go
进程挂载core/app/sev/vss/main.go server.NewSipProc(svcCtx).DO(..., new(gbs_proc.SendLogic), new(gbs_proc.CatalogLoopLogic), ...)

七、小结

设计点目的
多 channel + 单调度循环出站集中、异步解耦、易加新信令类型
每任务 goroutine不阻塞总线;慢操作并行
注册写入 types.Request + map全站统一,后续命令免重复解析
CatalogLoop + SipSendCatalog上线/补线立即发 Catalog;CatalogLoop.proc 为每秒扫表
ThrottleFixedGridTrailing防 Catalog 风暴
GBSSenderVia/From/To/XML/SN 封装复用

本文可以结合《SDP详解-国标与工程实践》《流播放-VSS-stream_play详解》阅读,可覆盖从 Catalog 到 Invite 的完整媒体前奏。