本篇以 core/app/sev/vss 为参照,讲解VSS是如何接入 github.com/ghettovoice/gosip,用来完成 SIP 信令的“从收请求、到响应,再到主动发起发起请求,以 INVITE+ACK为例。
参考代码 点击直达
为便于落地,下面会用 INVITE 贯穿两条典型链路:
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 中实现。它完成了四件事:
节选代码:
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 的处理链路可以按“包装层 + 业务层”拆开:
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,它的流程可以概括为:
- sdpInfo, err := sdp.ParseString(l.req.Original.Body())
- 检查 len(sdpInfo.Media) > 0
- 通过 l.req.ID 去 svcCtx.TalkSipData 的 key 里查找匹配(避免“未知对讲会话”被错误启动)
- usablePort := common.UsablePort(l.svcCtx)
- 这里会跳过当前正在占用的端口,并检查系统可用性
- sip2.NewGBSSender(…).InviteSDPResponse(l.tx, sdpInfo, usablePort)
- 这一步会调用 tx.Respond(resp) 把响应发回去
- common.SetTalkRtpConnInfo(svcCtx, sdpInfo, talkSipKey, usablePort)
- 表示“响应已经在 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 的触发链路是:
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 两步:
- inviteData, inviteRes, err := sip2.NewGBSSender(…).VideoLiveInvite(req)
- 要检查成功 inviteRes.StatusCode() (项目里允许 <= 200)
- 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() 会:
- Via / From / To / Call-ID / User-Agent
- CSeq: INVITE
- Content-Type: Application/SDP
- video media(PS/MPEG4/H264/H265)
- Connection/Origin 使用 MediaServerIP、ChannelUniqueId、本地 Sip.ID 等
- 根据 PlayType 决定是否走 playback 分支,并设置 Subject header
其中 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,建议按下面顺序读:
7. 注意事项
- sip.INVITE 的入站场景与 GBSSender.VideoLiveInvite 的出站场景区分清楚(同为 INVITE,但 SDP/缓存 key/媒体类型不同)
- Ack 后续的 RTP 协议入参与 SDP 中的 connection/ssrc/port/codec 做一致性校验(避免问题难排查)
- GBC是信令下级服务器 (平台或者设备端)实际与GBS使用方式大体一致,需要注意的是GBC是设备方,而GBS是作为上级信令服务器
- 如果在同样的工程结构里实现完整的信令服务器(比如catalog / register / keepalive / bye 等),需要严格按照项目结构注册路由等
网硕互联帮助中心



评论前必须登录!
注册