☰
GB28181摄像头接入实战:心跳保活与TCP/UDP推流、结束推流全解析
2026/10/15 7:14:13 网站建设 项目流程

系列文章写到第四篇,不少朋友私信说,前面把 SIP 信令服务器的基础框架搭起来了,注册也能通了,但手里那台 GB28181 摄像头一上线还是让人心慌:不知道它是否还活着,点播时 INVITE 发出去没有回音,好不容易画面出来了,想停又停不干净。这篇就把 GB28181 摄像头的心跳保活、TCP/UDP 推流、以及结束推流这三大块一次讲透,基于 SpringBoot 的 Java 实现,能跑通、能复现、能排查。

GB28181 整个交互链路里,信令服务器承担的是“调度中枢”的角色。摄像头不会主动告诉你“我现在要给你推流”,你必须用 SIP 信令去邀请它、告诉它往哪里推、用什么方式推,然后它才会把 RTP 流发过来。而这一切的前提,是设备在线、状态可信,也就是靠心跳机制来维护。所以这篇的内容,实际上是 GB28181 平台从“能注册”走向“能点播”最关键的一步。

我会直接按实际开发顺序来讲:先解决设备心跳保活,再做 INVITE 推流信令,然后处理 TCP/UDP 两种推流模式,最后把 BYE 结束推流和异常回收讲清楚。每一步都给出可用的 Java 代码思路、SIP 消息示例和我在实测中踩过的坑。

1. 先把设备状态这张表管好:心跳保活的设计

1.1 为什么 GB28181 要用自己定义的 KeepAlive

很多人第一次接触 GB28181 的心跳,会误以为它就是 SIP 协议里的 OPTIONS 方法。实际上 GB28181 定义了一套自己的消息:设备注册成功后,周期性地向信令服务器发送 KEEPALIVE 请求,消息正文是 XML,里面带设备 ID、状态、SN 等信息。服务器收到后回复 200 OK,同时把设备状态标记为在线。

这套机制和标准 SIP 的 OPTIONS 玩法不一样,它的目的非常明确:让平台侧随时知道摄像头的真实存活状态。摄像头可能因为断网、断电、程序崩溃而离线,如果只靠注册的 Contact 去猜测,根本不准。KEEPALIVE 的周期在注册时由设备决定,很多 IPC 默认是 60 秒一次,也有的是 30 秒,具体要看设备自身配置或平台下发的配置。

我自己的项目里,直接在设备对象上保存 心跳时间戳 和 心跳周期。每收到一次 KEEPALIVE,就更新这个时间戳,然后让一个后台定时任务去扫描所有设备,如果当前时间减去心跳时间戳超过 3 个周期,就判定离线。

1.2 KeepAlive 消息的结构长什么样

拿一台常见 IPC 举例子,设备发过来的 KEEPALIVE 大概是下面这个样子:

KEEPALIVE sip:34020000001110000001@3402000000 SIP/2.0 Via: SIP/2.0/UDP 192.168.1.64:5060;rport;branch=z9hG4bK123456 From: <sip:34020000001110000001@3402000000>;tag=1234567890 To: <sip:34020000002000000001@3402000000>;tag=abcdef Call-ID: 1234567890@192.168.1.64 CSeq: 128 KEEPALIVE Content-Type: Application/MANSCDP+xml Max-Forwards: 70 Content-Length: 123 <?xml version="1.0" encoding="GB2312"?> <Notify> <CmdType>Keepalive</CmdType> <SN>128</SN> <DeviceID>34020000001110000001</DeviceID> <Status>OK</Status> </Notify>

注意几个关键点:

  • 请求行方法名是KEEPALIVE,不是标准 SIP 方法,但格式遵循 SIP 文本协议。
  • 消息头里Content-Type是Application/MANSCDP+xml,这是 GB28181 的扩展类型。
  • 正文 XML 中CmdType为Keepalive,DeviceID是设备国标编号,Status表示设备自身状态。

服务器收到之后,只需要解析出DeviceID,找到对应设备,更新在线状态,然后拼一个 200 OK 回给设备,不需要关心 SN 等字段是否对上,但最好把 SN 原样带回业务层,方便后续排查和和设备日志对上。

1.3 SpringBoot 里怎么统一收 SIP 消息

由于我们做的是“简单实现”,我建议不要一开始就引入完整的 SIP 栈,而是自己写一个基于 Netty 的 UDP/TCP 接收层。SIP 是纯文本协议,包头和包体之间用空行分隔,拿到完整消息后按行解析第一行的方法名,就能区分 REGISTER、INVITE、KEEPALIVE、BYE 等。

一个比较省事的做法是:定义统一的消息处理入口,根据第一行方法名路由到不同的 Handler。伪代码如下:

public void onSipMessage(String remoteAddr, int remotePort, String message) { String firstLine = message.substring(0, message.indexOf("\r\n")); String method = firstLine.split(" ")[0]; switch (method) { case "REGISTER" -> handleRegister(remoteAddr, remotePort, message); case "KEEPALIVE" -> handleKeepAlive(remoteAddr, remotePort, message); case "INVITE" -> handleInvite(remoteAddr, remotePort, message); case "ACK" -> handleAck(remoteAddr, remotePort, message); case "BYE" -> handleBye(remoteAddr, remotePort, message); case "MESSAGE" -> handleMessage(remoteAddr, remotePort, message); default -> sendBadRequest(remoteAddr, remotePort, "Unsupported method"); } }

这里建议用 Netty,因为后面推流接收 RTP 也需要端口监听,Netty 一套下来比较顺手。如果只是为了心跳,用 Java 原生DatagramSocket也能顶,但到了 INVITE 的 TCP 信令和 TCP 流接收时就会很难受。Netty 统一处理 TCP、UDP 两个 Channel 是最稳妥的。

1.4 心跳超时判定与离线通知

心跳超时不能只更新一个时间戳就完事,平台侧往往需要在设备离线时联动。我的实现是:

@Component public class DeviceStatusMonitor { @Scheduled(fixedRate = 10_000) public void scan() { long now = System.currentTimeMillis(); deviceManager.allDevices().forEach((deviceId, device) -> { int timeoutMs = device.getKeepAliveInterval() * 3 * 1000; if (now - device.getLastKeepAliveAt() > timeoutMs) { if (device.isOnline()) { device.setOnline(false); // 通知业务层:设备离线、停止该设备的推流会话 notifyDeviceOffline(deviceId); } } }); } }

这里有一个容易忽略的点:设备离线时,如果它之前还有正在进行的推流会话,不能只是标记状态,必须主动向流媒体模块发起停止收流,并清理会话资源。否则端口一直被占用,RTP 线程一直跑着,时间久了平台就“假死”了。我实际调试时遇到过因为忘记清会话,导致 200 路摄像头离线重连后,内存里堆积了几百个无用对象,老年代直接打满的问题。

2. 推流的前置准备:INVITE 信令与 SDP 协商

2.1 点播一个摄像头前需要确认什么

设备在线只是基础,真正发起 INVITE 之前还需要确认几件事:目标通道 ID、编码格式、传输协议。GB28181 里点播用的对话由平台向设备发送 INVITE,SDP 中携带媒体描述和 GB28181 特有的y字段。

y字段是整个 SDP 里最能体现 GB28181 风格的地方。它包含了媒体流ID、SSRC信息,一般长这样:y=01000000001900000001。前 10 位是中心编码,后面是流 ID 和 SSRC 的编码组合。摄像头解析y字段后,就用里面的 SSRC 去组 RTP 包,所以这个值必须全局唯一,否则两台摄像头同时点播会把流搞混。

我自己习惯直接用 UUID 转数字后取一段,再拼上序号,保证唯一性。代码里生成 SSRC 的逻辑是:

String ssrc = String.format("%010d", System.currentTimeMillis() % 10000000000L);

但注意y字段不是纯 SSRC,它是设备 ID + 流类型 + SSRC 的组合,所以最好按国标规范组装。实际项目中我封装了一个SdpUtils来专门处理y字段的生成和解析,避免在多处手写字符串拼接。

2.2 构造 INVITE 请求的核心代码

平台向摄像头发送的 INVITE,SIP 请求行的格式和 KEEPALIVE 类似,但请求行里的 URI 是目标设备的国标编号。消息头需要带上To、From、Call-ID、CSeq、Contact、Content-Type等。

SDP 部分举个例子:

v=0 o=34020000002000000001 0 0 IN IP4 192.168.1.100 s=Play c=IN IP4 192.168.1.100 t=0 0 m=video 9500 RTP/AVP 96 a=recvonly a=rtpmap:96 PS/90000 y=01000000001900000001

逐行解释一下:

  • o=: 发起方用户名、会话 ID、版本、网络类型、地址类型、IP。
  • s=Play: 会话名固定为 Play,表示点播实时流。
  • c=IN IP4 192.168.1.100: 平台接收 RTP 的 IP,也就是 SDP 里告诉设备往哪里推流。
  • m=video 9500 RTP/AVP 96: 媒体类型、平台监听端口、传输协议、RTP payload 类型。这里是 UDP 方式。
  • a=rtpmap:96 PS/90000: 96 号 payload 对应 PS 封装,采样率 90000。
  • y=...: 扩展字段,携带媒体流 ID 和 SSRC。

如果是 TCP 推流,m=行就要变成m=video 9500 RTP/AVP/TCP 96,同时还要加一个a=setup:passive或a=setup:active字段,后面的章节会详细说。

2.3 INVITE 发送与等待 200 OK 的状态管理

INVITE 发出去之后,设备通常会在 2 到 5 秒内回 100 Trying,再回 200 OK。200 OK 里带着设备的 SDP,告诉平台它的媒体描述和 SSRC。平台收到 200 OK 后要做两件事:

  1. 解析 SDP 中的m=行,拿到设备实际推流的 IP 和端口。
  2. 向设备发送 ACK 确认。

ACK 不能省。GB28181 是三次握手确认,平台不发 ACK,设备会一直重传 200 OK,而且不会开始推流。很多新手在抓包的时候看到摄像头一直发 200 OK 不推流,就是 ACK 没发对。ACK 的请求行是ACK sip:xxx SIP/2.0,Call-ID 和 CSeq 必须与 INVITE 一致,只是 CSeq 方法名改成 ACK。

由于 SIP 对话是一个状态机,我建议在项目里维护一个SipSession对象,字段至少包括:

  • callId: 全局唯一会话标识
  • deviceId: 设备 ID
  • channelId: 通道 ID
  • inviteSeq: CSeq 编号
  • expectResponse: 当前期望的信令状态
  • createdAt: 会话创建时间
  • timeoutCount: 重试次数

INVITE 发出后,把这个 Session 保存到内存 Map,收到 200 OK 后更新状态,收到 ACK 后再次更新状态。后续 BYE、超时清理都依赖这个 Session。

2.4 信令走 UDP 还是 TCP,怎么选

GB28181 的信令传输方式支持 UDP 和 TCP。SIP 本身不强制,但实际项目里,IPC 默认走 UDP 的多,因为 UDP 轻量、实现简单。而平台侧如果是运营商级的应用,信令大概率会走 TCP,原因是运营商网络环境里 UDP 容易被限制,TCP 长连接更稳定。

我的建议是:自己能同时监听 UDP 5060 和 TCP 5060 端口。设备注册时,从 Via 或 Contact 头里能看到设备的传输类型,注册成功后按设备的类型回消息。比如设备从 UDP 发来 REGISTER,那后续 KEEPALIVE、INVITE 响应就往 UDP 回;如果设备注册时走的是 TCP,就维护 TCP 连接通道,响应走同一个连接。

这里有个经典坑:TCP 信令下,如果你检测不到 TCP 连接断开,设备可能已经离线了但平台还认为它在线。所以 TCP 模式的心跳超时判定要更严格,最好监听 Channel 的 close 事件,一旦连接关闭立刻标记设备离线。

3. RTP 流的接收:TCP 和 UDP 两种推流方式

3.1 UDP 推流:设备直接往 SDP 里的 IP 和端口发

UDP 方式最直观。平台在 SDP 里告诉设备接收端 IP 和端口,设备收到 ACK 后,就往那个端口发 RTP over UDP。RTP 包负载是 PS 封装后的视频流,也就是 H.264/H.265 裸流被封装在 PS 容器里,RTP 头部只是简单透传。

接收端需要做的事,就是监听 SDP 里指定的 UDP 端口,收 RTP 包,然后解析 PS。如果你只是想验证“能不能收到流”,可以先不解析 PS,直接统计收到的 RTP 包数量,确认端口通、SSRC 对得上。

用 Netty 接收 UDP 消息的代码大致是这样:

@Configuration public class RtpUdpServer { @Bean public NioEventLoopGroup rtpEventLoopGroup() { return new NioEventLoopGroup(4); } public void bind(int port) throws Exception { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(rtpEventLoopGroup()) .channel(NioDatagramChannel.class) .handler(new SimpleChannelInboundHandler<DatagramPacket>() { @Override protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) { ByteBuf buf = packet.content(); byte[] data = new byte[buf.readableBytes()]; buf.readBytes(data); // 转给PS解析模块或转发模块 rtpReceiver.processRtpPacket(data, packet.sender()); } }); bootstrap.bind(port).sync(); } }

需要注意:UDP 收流的端口不是 5060,而是 SDP 里m=video后面的端口。平台上要预先规划好端口池,每发起一个点播就分配一个空闲端口。用完归还,否则端口耗尽就只能干瞪眼。

3.2 TCP 推流:RFC4571 字节流分包

TCP 推流和 UDP 的报文模型完全不同。RTP over TCP 采用的是 RFC4571 封装:每个 RTP 包前面加两个字节的长度字段,表示紧随其后的 RTP 包长度。TCP 是流式传输,没有消息边界,所以必须自己按“2 字节长度 + RTP 包”切分。

SDP 协商时,如果平台向设备发送m=video 9500 RTP/AVP/TCP 96,意味着平台作为 TCP 服务端监听 9500 端口,设备主动连上来推流。这种模式叫passive,SDP 里应该带a=setup:passive。

也可以反过来:平台主动连接设备的某个端口,那就是a=setup:active。实际项目里 IPC 普遍支持平台监听、设备主动连的模式,所以我在实现时优先用 passive。

TCP 接收的 Netty 管道里,核心是自定义ByteToMessageDecoder:

public class RtpOverTcpDecoder extends ByteToMessageDecoder { @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { while (in.readableBytes() >= 2) { in.markReaderIndex(); int packetLength = in.readUnsignedShort(); if (in.readableBytes() < packetLength) { in.resetReaderIndex(); break; } byte[] rtpPacket = new byte[packetLength]; in.readBytes(rtpPacket); out.add(rtpPacket); } } }

这段代码的核心思想很简单:每次先读 2 字节拿到长度,如果当前缓冲不足一个包,就回退读指针,等待下一次 ChannelRead。TCP 收流比 UDP 麻烦的地方就在这里,分包逻辑一旦写错,收到的流就是花屏、卡顿、甚至直接起不了播。

3.3 收流之后的 PS 解复用与转封装思路

GB28181 设备推过来的流是 PS 封装,和 RTSP 的 RTP/AVP/H264 不一样。PS 包里有 PES 头,里面放着 PTS/DTS 时间戳,载荷是 H.264 或 H.265 的 Access Unit。你要做实时播放,必须先解析 PS 得到 ES 流,再封装成 FLV 或 fMP4 后转给播放端。

这个环节不一定是信令服务器的职责。很多架构里,信令服务器只负责协商,RTP 收流后通过内部接口转发给流媒体模块。如果你是自己写全套,那就必须处理 PS 头解析。

我一开始以为能跳过 PS 解析,直接把收到的 RTP 包丢给播放器,结果播放器完全无法识别。后来才明白,RTP 里的负载就是 PS 包,播放器如果支持 GB28181(比如某些 Web 播放器插件),可以直接解析,但通用播放器不行,必须转封装。

3.4 收流会话的并发控制:别把信令服务器做成流媒体服务器

这里要提醒一下:SpringBoot 信令服务器本身不应该承担大量的 RTP 收流和转码工作。它的核心竞争力是信令控制,流媒体转发应该交给独立的流媒体网关来处理。如果你只是自己搭一个测试平台,那用 Netty 收流没问题;如果要做并发 100 路以上的服务,最好把收流模块拆成单独进程,通过 IPC 或 HTTP 接口把 SDP 信息告诉流媒体进程。

我在第四篇这个阶段还是单品联调,所以信令服务和收流服务在一个进程里跑。但端口分配、会话管理、线程池隔离还是要做。线程池不能直接用默认的Executors.newCachedThreadPool(),RTP 是高 IO 场景,建议用NioEventLoopGroup,数量按 CPU 核心数乘 2 左右配。

4. 结束推流:BYE 信令与会话资源回收

4.1 平台主动停止点播:发送 BYE

停止点播是平台侧发起,向设备发送 BYE 请求。BYE 的格式和 INVITE 类似,但要复用原来的 Call-ID、From、To、CSeq 序号体系。CSeq 需要在原 INVITE 的序号基础上加一,方法名从 INVITE 变成 BYE。

我的代码里封装了一个发送 BYE 的方法:

public void sendBye(SipSession session) { String byeRequest = "BYE sip:" + session.getDeviceId() + "@3402000000 SIP/2.0\r\n" + "Via: SIP/2.0/UDP " + serverIp + ":5060;rport;branch=" + generateBranch() + "\r\n" + "From: " + session.getFromTag() + "\r\n" + "To: " + session.getToTag() + "\r\n" + "Call-ID: " + session.getCallId() + "\r\n" + "CSeq: " + (session.getCSeq() + 1) + " BYE\r\n" + "Max-Forwards: 70\r\n" + "Content-Length: 0\r\n\r\n"; sipSender.send(session.getDeviceRemoteAddress(), byeRequest); }

BYE 发出后,设备会回 200 OK。收到 200 OK 就说明设备已经停止推流,此时平台侧要立即关闭对应的 RTP 接收端口,清理会话信息。不要等到设备不推了才收端口,那样端口会一直被占。

4.2 设备主动断开:处理设备发来的 BYE

有些场景下摄像头会因为自身异常或人工重启,主动向平台发送 BYE。这种 BYE 的To是平台,From是设备。服务器收到后要遍历会话 Map,找到匹配的 Call-ID,把对应会话标记为结束,然后回 200 OK,并且让下游流媒体网关停止收流。

这里容易漏一点:设备主动 BYE 时,它的流可能已经停了,但平台的 RTP 接收端口还开着。如果你只回 200 OK,不管端口,就会造成泄漏。我的习惯是封装一个统一方法:releaseSession(callId),里面不管是谁发起的结束,都做同样的端口释放、线程停止、状态清理。

4.3 收流超时和信令重传:不能无限等

设备如果对 INVITE 无响应,或者 INVITE 的 200 OK 迟迟不来,信令服务器需要有一个超时重传机制。SIP 本身建议的 Timer B 是 64 秒,但实际点播场景里等 64 秒太折磨人,客户端点开一个画面 3 秒没出,早就重试了。

我做的策略是:

  • INVITE 发出后 5 秒内没有 100 Trying,重发一次。
  • 重发两次后仍无 200 OK,直接向调用方返回失败。
  • 收到 200 OK 后 3 秒内没有收到设备推流,视为 RTP 超时,发送 BYE 清理。

这些超时判断可以放在一个SipSessionTimeTask里,每秒钟扫描一次所有状态不为“已结束”的会话。任务执行时要小心:不能一边遍历一边清理 Map,否则并发修改直接抛异常。用ConcurrentHashMap且遍历entrySet()再标记清除,或者先把要清理的 key 收集到列表里再统一删除。

4.4 完整资源回收链路:端口、线程、进程内缓存

一次点播结束,需要回收的不仅是信令会话。总结一下我自己的资源回收清单:

  • 释放 SIP Call-ID 对应的会话对象,避免内存泄漏。
  • 释放 RTP 接收端口,归还端口池。
  • 关闭 TCP 推流的 Channel,断开与设备的 TCP 连接。
  • 停止 RTP 接收线程或 Netty Handler 的状态记录。
  • 如果做了 PS 解析、转封装,还需要清理转封装中间产物。
  • 更新设备的点播状态,标记当前通道空闲。

在代码里,这个链路对应一个很直白的方法:

public void stopPlay(String callId) { SipSession session = sessionManager.remove(callId); if (session == null) { return; } sendBye(session); portPool.release(session.getRtpPort()); rtpReceiver.stopReceive(session.getRtpPort()); streamGateway.stop(session.getStreamId()); }

注意顺序:先发 BYE,再释放端口。如果先释放端口再发 BYE,设备可能还在往旧的端口推流,导致 BYE 发出去之后设备推流的包被系统丢弃,而设备因为没有及时收到平台停止收流的反馈,会继续重传一段时间,造成网络浪费。

5. 实测中的排查链路:从“不通”到“稳定”

5.1 摄像头迟迟不上线,先别改代码

常见现象:REGISTER 发出后平台已经回了 200 OK,但设备一直接收不到 KEEPALIVE 的回复,或者设备根本没发 KEEPALIVE。排查的时候,先抓包看设备到底有没有发 KEEPALIVE,很多摄像头第一次接入时会因为注册服务器的地址配置不对,REGISTER 用的是配置好的域名,但 KEEPALIVE 用的是另一个回调地址。这是设备配置问题,不是代码问题。

如果确认 KEEPALIVE 确实到服务器了,但服务器没有回包,那大概率是 Content-Type 没匹配上。有些设备发送Content-Type: application/MANSCDP+xml,有些把大小写改成Application/MANSCDP+xml,服务端如果对头字段做了大小写敏感匹配就会漏。SIP 头字段名本身不区分大小写,但值区分大小写,我统一用equalsIgnoreCase去匹配。

5.2 INVITE 发出后没有 200 OK,检查 SDP 的 y 字段

INVITE 发出去后如果设备一直不回,可能是因为 SDP 格式不合法。要重点检查v=0和o=的格式是不是标准行,很多设备对o=行的 IP 格式很敏感,必须是合法的 IPv4 地址。另一个高频问题就是y=字段写错了长度,设备直接不解析 SDP。

还有一个我踩过的坑:a=rtpmap:96 PS/90000里的 payload type 不一定是 96。有些设备只认 98,有些只认 96,如果你用的 payload type 设备不认识,它会直接返回 488 (Not Acceptable Here)。最好的做法是先看设备注册或目录里带的能力信息,如果没有,就在配置里允许通过扩展字段配置 payload type,而不是写死在代码里。

5.3 RTP 包收到了,但画面花屏

能收到 RTP 包但画面花屏,问题绝大多数出在 PS 解析环节,而不是信令。RTP 的包序、时间戳、SSRC 需要先校验,再进入 PS 解析。如果收到的 RTP 包缺失较多,可能是 UDP 网络丢包。可以先用 TCP 推流对比一下,如果 TCP 方式不丢包、画面正常,那就是网络环境里 UDP 被限制了。

另外要看设备的 RTP 封装是遵循旧国标还是新国标。部分老摄像头 PS 封装里没有 PES 扩展头,解析时需要兼容。判断方法就是打印 PS 包头的前几个字节,对照标准头里的start_code_32和program_stream_pack_length来确认。

5.4 TCP 推流连不上:区分 active 和 passive 模式

TCP 推流最常见的坑是 SDP 里a=setup参数和设备实际行为不一致。我一开始做 TCP 推流时,以为设备会主动连接我监听的端口,结果设备一直不来连,后来抓包发现设备在等待平台去连接它。于是把 SDP 改成a=setup:active,平台主动向设备 SDP 里填的地址和端口发起 TCP 连接,才把流拉通。

所以实现时不要写死一种模式。要根据设备的实际能力做配置开关。某些设备固件对 passive 支持很差,但对 active 很稳定;另一些反过来。平台侧最好两种都支持,统一抽象成TcpMode.Active和TcpMode.Passive。

5.5 结束推流之后流的影子还在:检查 SIP 对话的 To tag

有一种情况特别隐蔽:平台发 BYE 后设备回了 200 OK,但播放端还能看到画面停顿了几秒才消失。这不是信令问题,而是流媒体网关没有及时断开 RTP 接收。BYE 只是信令层面让设备停止发送,网关如果还在等下一包数据缓冲,播放端就会一直转圈。

这时要去查网关在收到停止指令后,是否确实关闭了对应的 socket 和解析线程。我在一次调试中发现,因为网关把“停止收流”和“销毁 session”放在了两个线程里,销毁操作被线程池阻塞,导致 session 一直懒散地活着。最后我统一用同一个线程池按 callId 取模分配,让同一路会话的所有操作都落到同一个线程上,彻底解决了竞争问题。

最后再分享一点个人体会

GB28181 信令服务器做到这一篇,其实已经具备了一个“能点播”的平台雏形。心跳、推流、结束推流这三件事看起来只是三个 SIP 交互流程,但真正把它们做稳定,需要的是对状态机的敬畏。任何一个消息没落到正确的状态,都可能导致设备离线、花屏、端口泄漏。

我自己的建议是:先不要急着堆功能,把日志打好。每一次 REGISTER、KEEPALIVE、INVITE、200 OK、ACK、BYE 都要记录从收到消息到发出响应的时间戳和关键字段。有了这些日志,后面联调任何设备,都可以在几分钟内定位到是平台的问题还是设备的问题。这套“心法”比任何一行代码都重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询