云计算百科
云计算领域专业知识百科平台

基于gosip库实现国标信令服务器实战教程:从server到收发信令

本篇以 core/app/sev/vss 为参照,讲解VSS是如何接入 github.com/ghettovoice/gosip,用来完成 SIP 信令的“从收请求、到响应,再到主动发起发起请求,以 INVITE+ACK为例。

参考代码 点击直达

为便于落地,下面会用 INVITE 贯穿两条典型链路:

  • 入站 INVITE(设备 -> VSS):VSS 的 InviteLogic 负责解析 SDP、分配本地 RTP 端口、回 200OK+SDP。
  • 出站 INVITE(VSS -> 设备):VSS 的 VideoLiveInvite 负责构建 SDP、发起 INVITE、解析响应 SDP、再发 ACK。

  • 1. VSS 中 gosip 的角色

    在 core/app/sev/vss 中,gosip.Server 既承担“服务端(接收设备请求)”,也承担“客户端(主动发起请求)”的事务层能力:

    • 作为服务端:server.NewSipSev(svcCtx).SipGbsServer(…) 创建 gosip.NewServer(…),并用 OnRequest 把 REGISTER / INVITE / ACK / BYE / MESSAGE 等方法路由到业务 handler。
    • 作为客户端:出站发起 SIP 时,GBSSender.Send() 会根据 TransportProtocol 选择 GBSUDPSev / GBSTCPSev,然后调用 RequestWithContext(ctx, req) 等待事务返回的最终响应。

    2. 创建服务

    VSS 程序入口在 core/app/sev/vss/main.go。启动顺序可以概括为:

    • 加载配置、创建 svcCtx
    • 初始化(initialize.DO)
    • 启动两个 SIP Server:GBS over TCP + UDP
    • 启动 SIP 发送处理循环(gbs_proc.SendLogic 等)
    • 等待退出并调用 Shutdown()

    main.go 中与 SIP Server 相关的关键代码片段如下:

    // SIP 服务器
    {
    var wg sync.WaitGroup
    wg.Add(2)

    // GBS TCP
    go func() {
    defer wg.Done()
    server.NewSipSev(svcCtx).SipGbsServer(server.SipTCP, gbs_sip.RegisterHandlers(svcCtx))
    }()

    // GBS UDP
    go func() {
    defer wg.Done()
    server.NewSipSev(svcCtx).SipGbsServer(server.SipUDP, gbs_sip.RegisterHandlers(svcCtx))
    }()

    wg.Wait()
    }

    server.NewSipProc(svcCtx).DO(
    // …
    new(gbs_proc.SendLogic),
    // …
    )

    <-stop
    {
    (*svcCtx.GBSTCPSev).Shutdown()
    (*svcCtx.GBSUDPSev).Shutdown()
    }


    2.1 SipGbsServer:真正创建 gosip.NewServer + 监听 + 挂载 handler

    SipGbsServer 在 core/app/sev/vss/internal/server/sip.go 中实现。它完成了四件事:

  • 等待初始化就绪:svcCtx.InitFetchDataState.Wait(),这一步也是vss项目中所有服务的数据依赖
  • 创建 gosip server:gosip.NewServer(…)
  • 注册每个 SIP 方法的回调:sipSvr.OnRequest(key, item)
  • 开始监听:sipSvr.Listen(network, addr),并写回 svcCtx.GBSUDPSev/GBSTCPSev
  • 节选代码:

    var (
    sipSvr = gosip.NewServer(gosip.ServerConfig{Host: s.svcCtx.Config.InternalIp}, nil, nil, NewLogger())
    addr = fmt.Sprintf("%s:%d", s.svcCtx.Config.Host, s.svcCtx.Config.Sip.Port)
    )

    for key, item := range handlers {
    if err := sipSvr.OnRequest(key, item); err != nil {
    functions.LogError(fmt.Sprintf("Sip GBS Request [%S] err: %s", key, err.Error()))
    }
    }

    if err := sipSvr.Listen(string(networkType), addr); err != nil {
    panic(err)
    }

    if networkType == SipTCP {
    s.svcCtx.GBSTCPSev = &sipSvr
    } else {
    s.svcCtx.GBSUDPSev = &sipSvr
    }


    2.2 RegisterHandlers:把 SIP 方法路由到业务逻辑

    core/app/sev/vss/internal/handler/gbs_sip/routers.go 定义了路由表。以 INVITE 为例:

    sip.INVITE: func(req sip.Request, tx sip.ServerTransaction) {
    sip2.DO("GBS", svcCtx, req, tx, nil, new(gbssip.InviteLogic))
    },

    这意味着:gosip 收到 INVITE 后,会进入 sip2.DO,最终由 InviteLogic.DO() 处理业务。


    3. 入站 INVITE(设备 -> VSS):InviteLogic 响应 200OK + SDP

    入站 INVITE 的处理链路可以按“包装层 + 业务层”拆开:

  • gosip 触发 handler:OnRequest(sip.INVITE, handler)
  • sip2.DO(…) 做通用处理(解析请求、黑名单校验、错误语义、统一响应策略)
  • 调用 InviteLogic.DO() 实现 INVITE 的具体业务(解析 SDP、校验、分配端口、回包)

  • 3.1 统一入口:sip2.DO 怎么把 sip.Request 变成你们的 types.Request

    在 core/app/sev/vss/internal/pkg/sip/sip_handler.go 中,DO() 做了这些关键事情:

    • 调用 ParseToRequest(h.req) 解析 From、Source、Body、Via 等信息,生成 *types.Request
    • 维护日志:写入 svcCtx.SipLog
    • 超时控制:context.WithTimeout(…, svcCtx.Config.Timeout)
    • 调用业务逻辑:h.logic.New(ctx, h.svcCtx, data, h.tx).DO()
    • 根据 types.Response 决定是否响应、如何响应

    ParseToRequest 的关键点在 core/app/sev/vss/internal/pkg/sip/utils.go:

    • ID 来自 req.From().Address.User().String()
    • TransportProtocol 从 ViaHop.Transport 推断(UDP/TCP 等)
    • Body 就是 req.Body()(INVITE 的 SDP 正在这里)

    3.2 业务层:InviteLogic.DO() 做了什么

    入站 INVITE 的核心代码在 core/app/sev/vss/internal/logic/gbs_sip/invite.go,它的流程可以概括为:

  • 解析 SDP:
    • sdpInfo, err := sdp.ParseString(l.req.Original.Body())
    • 检查 len(sdpInfo.Media) > 0
  • 校验请求是否合法:
    • 通过 l.req.ID 去 svcCtx.TalkSipData 的 key 里查找匹配(避免“未知对讲会话”被错误启动)
  • 分配本地端口:
    • usablePort := common.UsablePort(l.svcCtx)
    • 这里会跳过当前正在占用的端口,并检查系统可用性
  • 直接用 ServerTransaction 回 200OK + SDP:
    • sip2.NewGBSSender(…).InviteSDPResponse(l.tx, sdpInfo, usablePort)
    • 这一步会调用 tx.Respond(resp) 把响应发回去
  • 写入 RTP 连接信息(供后续音频 RTP 发送/接收使用):
    • common.SetTalkRtpConnInfo(svcCtx, sdpInfo, talkSipKey, usablePort)
  • 返回 **Ignore: true**❗❗❗:
    • 表示“响应已经在 InviteSDPResponse 里完成了”,不要让 sip2.DO 继续再做一层默认 OK 回包
  • 如果你想快速理解为什么入站 INVITE 必须先回 SDP 再写缓存,可以重点看 common.SetTalkRtpConnInfo(core/app/sev/vss/internal/pkg/common/talk.go)做了这些事:

    • 从 SDP 的 Media[0] / Media[].Formats[] 中推导音频 codec:PCMA / PCMU / AAC 对应 payloadType 与 sampleRate
    • 从 SDP 的 SSRC(有些设备带前缀时会裁剪)解析出数字型 SSRC,并写入缓存
    • 把本次可用端口 usablePort 写入 RTPUsablePort,并计算 RTPRtpPort/RTPRtcpPort 的对应关系
    • 把 sdpInfo.Connection.Address 写入 RTPRemoteIP,为后续 RTP 通信准备“对端 IP + 本端端口”

    “先自定义响应,再返回 Ignore”的模式在 InviteLogic 里是明确存在的:

    // 回复invite 200OK
    if err := sip2.NewGBSSender(l.svcCtx, l.req, l.req.ID).InviteSDPResponse(l.tx, sdpInfo, usablePort); err != nil {
    return &types.Response{Error: types.NewErr(err.Error())}
    }

    return &types.Response{Ignore: true}


    3.3 入站 INVITE 时序图(设备 -> VSS GBS)

    注意区分GBS与GBC的区别

    GBSSender (InviteSDPResponse)

    InviteLogic

    VSS GBS (gosip.Server)

    设备

    GBSSender (InviteSDPResponse)

    InviteLogic

    VSS GBS (gosip.Server)

    设备

    #mermaid-svg-8HKYHt8fbhj1cmht{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-8HKYHt8fbhj1cmht .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-8HKYHt8fbhj1cmht .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-8HKYHt8fbhj1cmht .error-icon{fill:#552222;}#mermaid-svg-8HKYHt8fbhj1cmht .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-8HKYHt8fbhj1cmht .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-8HKYHt8fbhj1cmht .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-8HKYHt8fbhj1cmht .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-8HKYHt8fbhj1cmht .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-8HKYHt8fbhj1cmht .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-8HKYHt8fbhj1cmht .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-8HKYHt8fbhj1cmht .marker{fill:#333333;stroke:#333333;}#mermaid-svg-8HKYHt8fbhj1cmht .marker.cross{stroke:#333333;}#mermaid-svg-8HKYHt8fbhj1cmht svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-8HKYHt8fbhj1cmht p{margin:0;}#mermaid-svg-8HKYHt8fbhj1cmht .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8HKYHt8fbhj1cmht text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-8HKYHt8fbhj1cmht .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-8HKYHt8fbhj1cmht .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-8HKYHt8fbhj1cmht .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-8HKYHt8fbhj1cmht .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-8HKYHt8fbhj1cmht #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-8HKYHt8fbhj1cmht .sequenceNumber{fill:white;}#mermaid-svg-8HKYHt8fbhj1cmht #sequencenumber{fill:#333;}#mermaid-svg-8HKYHt8fbhj1cmht #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-8HKYHt8fbhj1cmht .messageText{fill:#333;stroke:none;}#mermaid-svg-8HKYHt8fbhj1cmht .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8HKYHt8fbhj1cmht .labelText,#mermaid-svg-8HKYHt8fbhj1cmht .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-8HKYHt8fbhj1cmht .loopText,#mermaid-svg-8HKYHt8fbhj1cmht .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-8HKYHt8fbhj1cmht .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-8HKYHt8fbhj1cmht .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-8HKYHt8fbhj1cmht .noteText,#mermaid-svg-8HKYHt8fbhj1cmht .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-8HKYHt8fbhj1cmht .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8HKYHt8fbhj1cmht .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8HKYHt8fbhj1cmht .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-8HKYHt8fbhj1cmht .actorPopupMenu{position:absolute;}#mermaid-svg-8HKYHt8fbhj1cmht .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-8HKYHt8fbhj1cmht .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-8HKYHt8fbhj1cmht .actor-man circle,#mermaid-svg-8HKYHt8fbhj1cmht line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-8HKYHt8fbhj1cmht :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    INVITE (SDP in body)

    handler(sip.INVITE) + ServerTransaction tx

    Parse SDP + 校验 talkSipKey(语音对讲可选)

    InviteSDPResponse(tx, sdpInfo, usablePort)

    tx.Respond(200OK + SDP)

    SetTalkRtpConnInfo(本地端口/ssrc/codec/rtp连接信息)(语音对讲可选)

    200OK + SDP


    4. 出站 INVITE(VSS -> 设备):VideoLiveInvite + ACK

    出站 INVITE 没有走 InviteLogic(因为 InviteLogic 是服务端接收 INVITE 的 handler)。出站 INVITE 的触发链路是:

  • 外部 HTTP 触发“拉流/回放”入口(例如 video_live_invite.go)
  • 把“需要发起 INVITE 的参数”投递到 svcCtx.SipSendVideoLiveInvite channel
  • gbs_proc.SendLogic 循环消费该 channel
  • SendLogic.VideoLiveInvite 调用 sip2.NewGBSSender(…).VideoLiveInvite(req)
  • GBSSender.VideoLiveInvite 构建 INVITE 的 SDP 并通过 gosip 的 RequestWithContext 发起请求
  • 收到响应后解析 SDP,并构建 ACK 再发一次

  • 4.1 触发点:HTTP 逻辑如何投递 SipSendVideoLiveInvite

    HTTP 入口在 core/app/sev/vss/internal/logic/http/gbs/video_live_invite.go。在准备好 stream/media server 信息后,会做:

    l.svcCtx.SipSendVideoLiveInvite <- &types.SipVideoLiveInviteMessage{
    StreamPort: streamRes.StreamPort,
    MediaTransMode: streamRes.TransportProtocol.MediaTransMode,
    MediaServerUrl: streamRes.MediaServerUrl,
    MediaServerIP: msIP,
    MediaServerPort: streamRes.MSNode.HttpPort,
    StreamName: streamName,
    PlayType: args.PlayType,
    ChannelUniqueId: args.ChannelItem.UniqueId,
    DeviceUniqueId: args.DeviceUniqueId,
    Req: sipReqRes.Req,
    TransportProtocol: args.DeviceItem.TransportProtocol(),
    Download: args.Download,
    }


    4.2 SendLogic.VideoLiveInvite:主动发 INVITE,并在响应后发 ACK

    核心在 core/app/sev/vss/internal/logic/gbs_proc/send_sip_proc.go 的 VideoLiveInvite:

    握手顺序工作:

    • invite -> ack(from to tag callid) -> info(…) -> notify(…) -> bye

    其中最与 gosip 直接相关的是 INVITE/ACK 两步:

  • INVITE:
    • inviteData, inviteRes, err := sip2.NewGBSSender(…).VideoLiveInvite(req)
    • 要检查成功 inviteRes.StatusCode() (项目里允许 <= 200)
  • ACK:
    • ackData, err := sip2.NewGBSSender(…).AckReq(inviteRes)
    • sip2.NewGBSSender(…).SendDirect(ackData) 把 ACK 发出去
  • 紧接着它会把响应 SDP 再解析一遍,然后启动 RTP 相关流程(ms.New(…).ACKRtpPub(…))。其中最关键的输入是:

    • sdpInfo.Media[0].Port:RTP 端口
    • sdpInfo.Connection.Address:远端 IP
    • filesize:从 SDP 的 attributes 里提取(用于后续 RTP/下载控制等)

    4.3 GBSSender.VideoLiveInvite:如何构造 INVITE + SDP

    core/app/sev/vss/internal/pkg/sip/gbs_send.go 中,VideoLiveInvite() 会:

  • 组装 SIP 头:
    • Via / From / To / Call-ID / User-Agent
    • CSeq: INVITE
    • Content-Type: Application/SDP
  • 生成 SDP:
    • video media(PS/MPEG4/H264/H265)
    • Connection/Origin 使用 MediaServerIP、ChannelUniqueId、本地 Sip.ID 等
    • 根据 PlayType 决定是否走 playback 分支,并设置 Subject header
  • makeRequest(sip.INVITE, headers, body) 构造 sip.Request
  • l.Send(request) 触发 gosip 的出站事务
  • 其中 l.Send() 是这样选 transport server 的(简化描述):

    • l.req.TransportProtocol == "UDP" -> 调 GBSUDPSev.RequestWithContext
    • 否则 -> 调 GBSTCPSev.RequestWithContext

    4.4 出站 INVITE 时序图(VSS -> 设备)

    Device

    GBSSender + gosip client transaction

    SendLogic.VideoLiveInvite

    SipSendVideoLiveInvite Channel

    HTTP/WS 接口

    Device

    GBSSender + gosip client transaction

    SendLogic.VideoLiveInvite

    SipSendVideoLiveInvite Channel

    HTTP/WS 接口

    #mermaid-svg-7KxZiS93y5XzqJPY{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7KxZiS93y5XzqJPY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7KxZiS93y5XzqJPY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7KxZiS93y5XzqJPY .error-icon{fill:#552222;}#mermaid-svg-7KxZiS93y5XzqJPY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7KxZiS93y5XzqJPY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7KxZiS93y5XzqJPY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7KxZiS93y5XzqJPY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7KxZiS93y5XzqJPY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7KxZiS93y5XzqJPY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7KxZiS93y5XzqJPY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7KxZiS93y5XzqJPY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7KxZiS93y5XzqJPY .marker.cross{stroke:#333333;}#mermaid-svg-7KxZiS93y5XzqJPY svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7KxZiS93y5XzqJPY p{margin:0;}#mermaid-svg-7KxZiS93y5XzqJPY .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7KxZiS93y5XzqJPY text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-7KxZiS93y5XzqJPY .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-7KxZiS93y5XzqJPY .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-7KxZiS93y5XzqJPY .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-7KxZiS93y5XzqJPY .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-7KxZiS93y5XzqJPY #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-7KxZiS93y5XzqJPY .sequenceNumber{fill:white;}#mermaid-svg-7KxZiS93y5XzqJPY #sequencenumber{fill:#333;}#mermaid-svg-7KxZiS93y5XzqJPY #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-7KxZiS93y5XzqJPY .messageText{fill:#333;stroke:none;}#mermaid-svg-7KxZiS93y5XzqJPY .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7KxZiS93y5XzqJPY .labelText,#mermaid-svg-7KxZiS93y5XzqJPY .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-7KxZiS93y5XzqJPY .loopText,#mermaid-svg-7KxZiS93y5XzqJPY .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-7KxZiS93y5XzqJPY .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-7KxZiS93y5XzqJPY .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-7KxZiS93y5XzqJPY .noteText,#mermaid-svg-7KxZiS93y5XzqJPY .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-7KxZiS93y5XzqJPY .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7KxZiS93y5XzqJPY .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7KxZiS93y5XzqJPY .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7KxZiS93y5XzqJPY .actorPopupMenu{position:absolute;}#mermaid-svg-7KxZiS93y5XzqJPY .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-7KxZiS93y5XzqJPY .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7KxZiS93y5XzqJPY .actor-man circle,#mermaid-svg-7KxZiS93y5XzqJPY line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-7KxZiS93y5XzqJPY :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

    投递 SipVideoLiveInviteMessage

    消费消息

    构造 INVITE (headers + SDP) 并 RequestWithContext

    INVITE (SDP)

    200OK (SDP in response body)

    inviteRes 返回

    AckReq(inviteRes) + SendDirect(ACK)

    ACK

    启动 RTP 推/接入(ms.ACKRtpPub 等)


    5. 哪些字段必须保持一致 ❗❗❗❗❗❗❗❗❗❗

    在 send_sip_proc.go 里有非常关键的点:

    invite -> ack(from to tag callid) -> notify(…) -> bye

    从ack开始 from to tag callid到后续流程这几个值需要保持一致

    参考代码 点击直达 core/app/sev/vss/internal/logic/gbs_proc/send_sip_proc.go:260

    结合 GBSSender.AckReq(resp sip.Response) 的实现,可以看出 ACK 依赖响应里的:

    • Call-ID:从 inviteRes.CallID() 提取
    • To/From:从 inviteRes.To() / inviteRes.From() 取回

    所以当需要扩展更多 SIP 方法(比如补齐 catalog/notify/broadcast 或者增加 talk 完整版流程)时,务必从:

    • 发起方构造的请求头字段
    • 响应方回来的 To/From/Call-ID/CSeq

    注意这两端做字段对齐


    6. 阅读代码的推荐路线,最快把握 gosip 集成方式

    如果你要快速理解 VSS 如何使用 gosip,建议按下面顺序读:

  • core/app/sev/vss/main.go:服务启动与退出流程
  • core/app/sev/vss/internal/server/sip.go:gosip.NewServer + Listen + OnRequest
  • core/app/sev/vss/internal/handler/gbs_sip/routers.go:方法路由表(INVITE -> InviteLogic)
  • core/app/sev/vss/internal/pkg/sip/sip_handler.go:sip2.DO 通用解析/响应框架
  • core/app/sev/vss/internal/logic/gbs_sip/invite.go:入站 INVITE 的业务实现
  • core/app/sev/vss/internal/logic/http/gbs/video_live_invite.go:出站 INVITE 的触发入口
  • core/app/sev/vss/internal/logic/gbs_proc/send_sip_proc.go:出站 INVITE + ACK 的握手编排
  • core/app/sev/vss/internal/pkg/sip/gbs_send.go:INVITE/ACK SDP 与 header 的构造

  • 7. 注意事项

    • sip.INVITE 的入站场景与 GBSSender.VideoLiveInvite 的出站场景区分清楚(同为 INVITE,但 SDP/缓存 key/媒体类型不同)
    • Ack 后续的 RTP 协议入参与 SDP 中的 connection/ssrc/port/codec 做一致性校验(避免问题难排查)
    • GBC是信令下级服务器 (平台或者设备端)实际与GBS使用方式大体一致,需要注意的是GBC是设备方,而GBS是作为上级信令服务器
    • 如果在同样的工程结构里实现完整的信令服务器(比如catalog / register / keepalive / bye 等),需要严格按照项目结构注册路由等
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 基于gosip库实现国标信令服务器实战教程:从server到收发信令
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!