1. 一次发版背后:五个协议库的协同节奏
同一天把五个协议库推上版本线,这种事在音视频设备接入圈子里并不常见。onvif-go进 v2、onvif-c首发、gb28181-go和gb28181-rs把设备侧能力补齐、onvif-device-rs同步跟进——这不是简单的版本号跳动,而是一次围绕“设备接入层”整体重构的集中释放。如果你正在做摄像机、NVR、门禁、对讲或者任何需要跟 ONVIF、GB28181 打交道的项目,这五个库基本覆盖了从设备发现、能力协商、媒体协商到信令交互的完整链路。
先说清楚这五个库各自是干什么的,不然后面聊细节容易乱。onvif-go是 Go 语言实现的 ONVIF 客户端/服务端协议栈,v2 意味着 API 有不兼容变更,但换来的是更干净的接口分层和更完整的设备管理能力。onvif-c是 C 语言版本的首发,面向嵌入式设备侧,解决的是资源受限环境下跑 ONVIF 服务端的问题。gb28181-go和gb28181-rs分别是 Go 和 Rust 实现的 GB28181 协议库,这次重点在“设备侧收官”,也就是把设备注册、心跳、目录查询、实时点播、历史回放这些设备端该做的事做完整了。onvif-device-rs则是 Rust 实现的 ONVIF 设备侧协议栈,跟onvif-c形成不同语言栈的互补。
为什么这五个库要同天发版?因为设备接入这件事,客户端和服务端、ONVIF 和 GB28181 之间是互相咬合的。你只改客户端不改服务端,联调时就会出现“我发的请求你解析不了”;你只改 ONVIF 不管 GB28181,项目里两种协议混用时状态机就会打架。同天发版意味着接口约定、错误码、超时策略、心跳间隔这些跨库语义是对齐过的,这对做平台侧或设备侧的人来说,省掉大量“版本对不上”的排查时间。
这篇文章适合谁看?如果你是用 Go 写视频管理平台的,onvif-gov2 和gb28181-go的变更直接影响你的接入层代码;如果你用 Rust 做边缘网关或设备固件,gb28181-rs和onvif-device-rs的设备侧收官意味着你可以少写很多胶水代码;如果你在 C 环境里做嵌入式 IPC,onvif-c首发给了你一个不用自己从零撸 SOAP 和 WS-Discovery 的选择。下面我按“整体设计思路—核心细节—实操落地—问题排查”的顺序,把这五个库这次发版里真正值得关注的东西拆开讲。
2. 整体设计与选型:为什么是这五个库,为什么是现在
2.1 协议库分语言实现的现实逻辑
音视频设备接入这个领域有个很拧巴的现实:平台侧喜欢用 Go 或 Java 写业务,因为并发模型舒服、生态全;边缘侧和嵌入式侧只能用 C 或 Rust,因为内存和 CPU 就那么点,跑不动运行时。ONVIF 和 GB28181 又都是基于 SOAP/XML 和 SIP 的文本协议,解析起来吃 CPU、吃内存,语言选型直接决定你能不能在一颗低端 IPC 芯片上把服务端跑起来。
所以这五个库的分工不是拍脑袋定的。onvif-go和gb28181-go面向平台侧和服务端中间件,强调并发处理大量设备连接、批量下发指令、维护设备状态机。onvif-c和onvif-device-rs面向设备侧,强调小体积、低内存占用、无动态内存分配或可控分配。gb28181-rs则是 Rust 生态里补位 GB28181 设备侧的关键一块,因为之前 Rust 做 GB28181 要么自己手写 SIP 栈,要么绑 C 库,前者工作量大,后者交叉编译痛苦。
这次同天发版的一个核心设计决策是:把“设备侧”和“平台侧”的协议语义显式分开。以前很多库是客户端服务端混在一起,一个结构体既当请求又当响应,字段可选性靠文档约定。现在onvif-gov2 把客户端和服务端接口拆成两套类型,gb28181-go和gb28181-rs把设备侧状态机独立成模块,onvif-c和onvif-device-rs直接只做设备侧。这样做的好处是类型安全——你写设备侧代码时,编译器不会让你误用平台侧才有的字段。
2.2 v2 版本的不兼容变更到底改了什么
onvif-go进 v2 是这次发版里对现有项目冲击最大的。根据常见实践,v2 通常做这几类事:把context.Context贯穿所有阻塞调用、把错误从字符串改成可判断的错误类型、把设备发现和设备管理拆成独立包、把 SOAP 编解码从反射改成代码生成或显式映射。我推测这次 v2 至少做了前两件,因为 ONVIF 设备发现(WS-Discovery)和设备管理(Device/Media/PTZ/Event)本来就是两个独立关注点,混在一个包里会导致依赖臃肿。
不兼容变更对使用者的影响是:你原来写client.GetDeviceInformation()可能变成了dev.GetInformation(ctx),原来判断错误靠strings.Contains(err.Error(), "401")现在可以errors.Is(err, onvif.ErrUnauthorized)。这种改动短期疼,长期省事。如果你项目里 ONVIF 调用散落在几十个文件里,升级 v2 的正确姿势是先升依赖、让编译报错、按错误逐个改,而不是试图找兼容层。兼容层只会把技术债拖得更久。
2.3 设备侧“收官”意味着什么
gb28181-go和gb28181-rs说的“设备侧收官”,我理解是设备端该有的能力这次补齐了。GB28181 设备侧要做的事包括:SIP 注册和鉴权、心跳保活、目录查询响应、设备信息查询响应、实时音视频点播(INVITE 处理)、历史回放(INVITE 带回放参数)、语音对讲、报警上报、移动位置上报。之前很多库只做到注册和心跳,点播和回放要自己拼 SIP 消息,这次收官应该是把这些都封好了。
onvif-device-rs的同步跟进也是同理。ONVIF 设备侧要响应 GetDeviceInformation、GetCapabilities、GetProfiles、GetStreamUri、PTZ 控制、事件订阅等。Rust 做设备侧的优势是内存安全加零成本抽象,适合跑在边缘网关上,把多个下级设备聚合成一个虚拟 ONVIF 设备对外暴露。
2.4 选型对照:什么场景用哪个库
| 场景 | 推荐库 | 理由 |
|---|---|---|
| Go 视频管理平台接入 ONVIF 设备 | onvif-go v2 | 并发模型好,v2 接口清晰 |
| Go 平台接入 GB28181 设备 | gb28181-go | 设备侧能力完整,信令封装好 |
| Rust 边缘网关做 GB28181 设备 | gb28181-rs | 内存安全,适合长期运行 |
| Rust 边缘网关做 ONVIF 设备 | onvif-device-rs | 设备侧协议栈完整 |
| C 嵌入式 IPC 做 ONVIF 服务端 | onvif-c | 无运行时依赖,体积可控 |
| 混合协议平台(ONVIF+GB28181) | onvif-go + gb28181-go | 同语言栈,错误码和超时策略可对齐 |
这个表不是绝对的,但能帮你快速判断该从哪个库入手。如果你项目里两种协议都有,尽量选同一语言栈的库,跨语言联调的成本远高于你省下的那点性能。
3. 核心细节解析:五个库各自的关键变更与实操要点
3.1 onvif-go v2 的接口分层与迁移要点
onvif-gov2 最值得关注的是接口分层。按常见做法,v2 会把原来一个大 Client 拆成DiscoveryClient、DeviceClient、MediaClient、PTZClient、EventClient。每个 Client 只依赖自己需要的 SOAP 动作,这样你做一个只需要发现设备的工具时,不用把整个 ONVIF 栈拖进来。
迁移时第一个要改的是设备发现。v1 里可能是onvif.Discover()返回一个设备列表,v2 里大概率变成discovery.NewClient().Discover(ctx, timeout)。这里ctx和timeout同时存在是有讲究的:ctx控制调用方取消,timeout控制 WS-Discovery 的等待窗口。WS-Discovery 是组播协议,发 Probe 出去后要等多个设备回应,等待窗口太短会漏设备,太长会拖慢启动。常见实践是设 3 到 5 秒,局域网内设备通常 1 秒内就回了。
第二个要改的是鉴权。ONVIF 用 WS-Security UsernameToken,v1 里可能是在 Client 上设用户名密码,v2 里大概率变成WithCredentials(user, pass)选项。注意 ONVIF 的密码摘要用的是 SHA1,不是明文传输,但也不是现代意义上的安全机制。如果你在不可信网络里用 ONVIF,建议把设备管理口放在独立 VLAN,不要直接暴露。
第三个要改的是媒体协商。GetStreamUri 返回的 URI 里带rtsp://地址,但很多设备返回的地址里 IP 是设备自己的内网 IP,平台侧拿到后要替换成可达 IP。v2 里这个替换逻辑最好放在你自己的封装层,不要改库代码,因为库只负责协议,不负责网络拓扑。
注意:升级 onvif-go v2 时,先把 go.mod 里的版本改掉,然后
go build ./...让编译器把所有不兼容点暴露出来。不要一边改一边猜,编译器的报错列表就是你的迁移清单。
3.2 onvif-c 首发:嵌入式设备侧的 C 实现要点
onvif-c首发对嵌入式圈子是个好消息。C 做 ONVIF 服务端的难点不在 SOAP 解析,而在 WS-Discovery 的组播处理和 HTTP 服务端的并发模型。嵌入式设备通常只有一个网口,既要跑 RTSP 服务端,又要跑 ONVIF HTTP 服务端,还要处理 WS-Discovery 的组播收发包,资源竞争很激烈。
按常见实践,onvif-c大概率提供这几个能力:一个轻量 HTTP 服务端(可能基于 libmicrohttpd 或自己写的 epoll 循环)、一个 SOAP 解析器(可能用 gSOAP 或自己写的 XML 拉解析)、一个 WS-Discovery 响应模块。你要做的是把这些模块初始化好,然后注册自己的设备信息回调和媒体配置回调。
实操时第一个坑是内存分配。嵌入式环境里 malloc 失败不是异常,是常态。ONVIF 响应消息可能几百字节到几 KB,如果你在中断上下文或高优先级线程里做 XML 序列化,很容易因为内存碎片导致分配失败。建议在启动时预分配好响应缓冲区,序列化时往固定缓冲区里写,而不是每次 malloc。
第二个坑是组播。WS-Discovery 用 239.255.255.250:3702,设备要加入这个组播组才能收到 Probe。很多嵌入式 SDK 的 socket 默认不加入组播组,你要显式setsockopt加IP_ADD_MEMBERSHIP。另外组播包在交换机上可能被 IGMP snooping 拦掉,如果发现设备发现不了,先检查交换机配置,再看代码。
第三个坑是并发。ONVIF 的 GetStreamUri 和 RTSP 的 DESCRIBE 可能同时发生,如果你用全局锁保护媒体配置,要注意锁的粒度。常见做法是媒体配置用读写锁,读多写少,GetStreamUri 走读锁,配置变更走写锁。
3.3 gb28181-go 设备侧收官:注册、心跳与点播的完整链路
gb28181-go这次设备侧收官,核心是把 SIP 信令链路做完整了。GB28181 设备侧的生命周期是:SIP REGISTER 注册(带 Digest 鉴权)、注册成功后定期发 Keepalive 心跳、收到目录查询回 Catalog 响应、收到 INVITE 建流、收到 BYE 断流、收到 MESSAGE 处理各种查询。
注册环节最容易出问题的是鉴权。GB28181 用 SIP Digest,第一次 REGISTER 不带 Authorization,服务端回 401 带 nonce,设备用 nonce 和密码算 response 再发一次 REGISTER。很多库在这里把 nonce 当一次性用,但实际上 nonce 可以复用一段时间。gb28181-go收官后应该把这块封好了,你只需要提供设备 ID、密码、服务端地址。
心跳环节要注意间隔。GB28181 标准建议心跳间隔 60 秒,但实际部署中如果服务端 3 个心跳周期没收到就判离线,那网络抖动时容易误判。常见实践是设备侧设 30 到 60 秒,服务端侧设 3 倍容错。gb28181-go应该提供了心跳间隔配置,不要用默认值,按你的网络质量调。
点播环节是设备侧最复杂的。收到 INVITE 后,设备要回 200 OK 带 SDP,SDP 里描述媒体流地址和编码格式。然后设备要主动把 PS 流推到服务端指定的 IP 和端口(GB28181 用 UDP 或 TCP 推流)。这里的关键是 SSRC 要跟 INVITE 里的 y 字段一致,否则服务端不认。gb28181-go收官后应该把 SSRC 解析和推流会话管理都封好了。
提示:GB28181 设备侧调试时,先用 tcpdump 抓 SIP 包,确认 REGISTER 和 401 的交互正常,再看 INVITE 和 200 OK。很多“点播失败”其实是注册就没成功,只是心跳还在发,看起来设备在线。
3.4 gb28181-rs 设备侧收官:Rust 异步栈下的信令处理
gb28181-rs的设备侧收官跟 Go 版本目标一致,但实现路径不同。Rust 做 SIP 栈通常用 tokio 做异步运行时,用 nom 或手写解析器做 SIP 消息解析。Rust 的优势是你可以把 SIP 消息解析写成零拷贝的,直接借用原始缓冲区,不用像 Go 那样频繁分配字符串。
设备侧收官后,gb28181-rs应该提供了Device结构体,你配置好设备 ID、密码、服务端地址后,调device.run()就进入事件循环。收到 INVITE 时,库会回调你的媒体推流函数,你把 PS 流写进去就行。这里要注意 Rust 的所有权模型:回调函数里不能持有Device的可变引用,否则编译不过。常见做法是用 channel 把推流任务发给独立 task。
Rust 做设备侧的另一个好处是交叉编译相对可控。gb28181-rs如果依赖了 openssl 做 Digest 鉴权,交叉编译到 ARM 时可能要换 rustls 或静态链接 openssl。建议在 Cargo.toml 里把 TLS 后端做成 feature,嵌入式环境用 rustls,服务器环境用 openssl。
3.5 onvif-device-rs 同步跟进:Rust 设备侧的能力对齐
onvif-device-rs这次同步跟进,我理解是跟onvif-c和gb28181-rs在设备侧能力上对齐。ONVIF 设备侧要响应 GetCapabilities,这个响应决定了客户端认为你支持哪些能力。如果你在 Capabilities 里声明支持 PTZ 但实际不响应 PTZ 指令,客户端会报错。所以onvif-device-rs应该提供了能力声明配置,你声明什么就实现什么回调。
实操时要注意 ONVIF 的 Profile 概念。Profile S 是流媒体,Profile G 是录像回放,Profile T 是高级流媒体(H.265、元数据)。onvif-device-rs大概率支持 Profile S 和 T,G 可能部分支持。你在 GetProfiles 响应里返回的 Profile 数量要跟实际能力匹配,不要为了好看多返回。
4. 实操过程:从零搭一个混合协议接入 Demo
4.1 环境准备与依赖安装
先明确目标:用 Go 写一个平台侧服务,同时接入 ONVIF 设备和 GB28181 设备,能发现设备、拉流、断流。Rust 和 C 的设备侧库我们用来模拟设备,方便联调。
Go 环境准备:
go mod init demo/access go get github.com/xxx/onvif-go/v2 go get github.com/xxx/gb28181-goRust 环境准备:
cargo new gb-device --bin cd gb-device cargo add gb28181-rs cargo add onvif-device-rsC 环境准备(如果做嵌入式模拟):
git clone https://github.com/xxx/onvif-c.git cd onvif-c mkdir build && cd build cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local make -j4 && make install依赖装完后,先跑一个最小可用的 ONVIF 发现,确认网络和库都正常。
4.2 ONVIF 设备发现与能力查询实操
Go 侧发现代码大概长这样:
package main import ( "context" "fmt" "time" "github.com/xxx/onvif-go/v2/discovery" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() devices, err := discovery.Discover(ctx, 3*time.Second) if err != nil { panic(err) } for _, d := range devices { fmt.Printf("found: %s %s\n", d.Endpoint, d.Types) } }这段代码的关键是Discover的第二个参数。WS-Discovery 发 Probe 后等 3 秒,局域网内设备通常 1 秒内回,3 秒是保险值。如果你在跨网段环境,组播可能过不去,这时候要用 ONVIF 的“单播发现”或直接配设备地址。
发现设备后查能力:
dev := onvif.NewDeviceClient(devices[0].Endpoint, onvif.WithCredentials("admin", "password")) info, err := dev.GetDeviceInformation(ctx) caps, err := dev.GetCapabilities(ctx)GetCapabilities返回的 Category 里,Media 和 PTZ 是你最关心的。如果 Media 为空,说明设备不支持 ONVIF 媒体配置,只能走 RTSP 直连。
4.3 GB28181 设备侧模拟与注册联调
Rust 侧模拟 GB28181 设备:
use gb28181_rs::{Device, DeviceConfig}; #[tokio::main] async fn main() { let config = DeviceConfig { device_id: "34020000001320000001".into(), password: "12345678".into(), server_addr: "192.168.1.100:5060".into(), heartbeat_interval: 30, ..Default::default() }; let mut device = Device::new(config); device.run().await.unwrap(); }设备 ID 的编码规则是:前 8 位是行政区划,中间 2 位是行业编码,接着 3 位是设备类型,最后 7 位是序号。34020000001320000001里3402是某地区,00是行业,000是类型,后面是序号。你实际用时按当地规则编,但联调时随便编一个 20 位数字也行,只要服务端不校验。
注册成功后,服务端会发目录查询,设备要回 Catalog 响应。gb28181-rs收官后应该自动处理了 Catalog 查询,你只需要在配置里提供通道列表。通道 ID 通常是设备 ID 的前 10 位加通道序号。
4.4 拉流与断流的完整调用链
ONVIF 拉流:
media := onvif.NewMediaClient(dev.Endpoint, onvif.WithCredentials("admin", "password")) profiles, _ := media.GetProfiles(ctx) uri, _ := media.GetStreamUri(ctx, profiles[0].Token) // uri 里是 rtsp://192.168.1.10:554/stream1 // 如果平台跟设备不在同一网段,替换 IPGB28181 拉流是服务端主动发 INVITE,设备侧收到后推流。平台侧代码:
session := gb28181.NewSession(deviceID, channelID) err := session.Invite(ctx, "rtp://192.168.1.100:30000") // 设备会把 PS 流推到 30000 端口断流时 ONVIF 侧没有显式断流,你关掉 RTSP 客户端就行。GB28181 侧要发 BYE:
session.Bye(ctx)注意:GB28181 的 INVITE 里 SSRC 必须跟设备推流时用的 SSRC 一致。
gb28181-go应该自动处理了,但如果你自己拼 SIP 消息,SSRC 格式是0+ 10 位数字,别搞错。
4.5 参数计算:心跳间隔与超时策略
心跳间隔和超时策略要一起算。假设网络 RTT 是 50ms,抖动 20ms,设备侧心跳间隔 T,服务端超时 S。设备发心跳后,服务端最坏情况在 T + RTT + 抖动 后收到。如果服务端设 S = 3T,那连续丢 2 个心跳才会判离线。但 T 太大时,设备真离线了服务端要等 3T 才知道。
常见实践:局域网 T=30s,S=90s;广域网 T=60s,S=180s。如果你做的是实时性要求高的场景(比如报警联动),T 可以降到 10s,但会增加信令负载。一个 1000 设备的平台,T=10s 意味着每秒 100 个心跳包,SIP 服务端要能扛住。
ONVIF 侧没有心跳,但 WS-Discovery 的 Hello 和 Bye 消息可以当在线状态用。不过很多设备不发 Bye,所以平台侧还是要定期调 GetDeviceInformation 探活。探活间隔建议 60s,超时 5s。
5. 常见问题与排查技巧实录
5.1 ONVIF 发现不到设备怎么办
先分层排查。第一层,确认设备和平台在同一网段,ping通。第二层,确认组播没被拦,用socat或自己写个 UDP 程序加入 239.255.255.250:3702,看能不能收到 Probe。第三层,确认设备 ONVIF 开关开了,很多 IPC 默认关 ONVIF,要在 Web 界面里手动开。第四层,确认鉴权,有些设备发现不需要鉴权,但 GetDeviceInformation 需要,如果你发现到了但查信息失败,是鉴权问题。
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全发现不到 | 组播被拦/ONVIF 未开 | 抓包看 3702 端口 |
| 发现到但查信息 401 | 用户名密码错 | 确认设备 ONVIF 用户 |
| 发现到但查信息超时 | 设备 HTTP 服务未起 | telnet 设备 80 端口 |
| 发现到但 GetStreamUri 空 | 设备不支持 ONVIF 媒体 | 查 Capabilities |
5.2 GB28181 注册失败排查
注册失败最常见的是鉴权算错。SIP Digest 的 response 计算是MD5(MD5(user:realm:pass):nonce:MD5(method:uri))。注意 method 是大写REGISTER,uri 是sip:服务端地址。如果你用库还失败,抓包对比设备发的和库发的 Authorization 头,看 response 是否一致。
第二个常见原因是设备 ID 和服务端配置不匹配。有些服务端校验设备 ID 前 10 位是否在允许列表里,你随便编的 ID 可能被拒。联调时先用服务端允许的 ID。
第三个原因是 SIP 端口。GB28181 默认 5060,但有些服务端用 5061 或自定义端口。确认设备配置里的服务端地址带了正确端口。
5.3 点播成功但看不到画面
点播成功意味着 SIP 信令通了,但媒体流可能没通。先确认设备推流的目标 IP 和端口是不是平台侧实际监听的。GB28181 的 INVITE SDP 里c=行是设备推流目标,m=video行是端口。如果平台侧 NAT 了,设备推的地址可能不可达。
然后确认 PS 流封装。GB28181 用 PS 封装,不是裸 H.264。平台侧要能解 PS。如果你用 FFmpeg 拉流,ffplay rtp://...不一定能直接播,因为 FFmpeg 对 PS over RTP 的支持要看版本。建议用gb28181-go自带的解包器,或者用支持 PS 的流媒体服务端。
最后确认 SSRC。抓包看 RTP 头的 SSRC 跟 INVITE 里的 y 字段是否一致。不一致的话服务端可能丢弃。
5.4 跨语言联调的坑
Go 平台接 Rust 设备时,最容易出问题的是字节序和字符串编码。SIP 和 SOAP 都是文本协议,理论上没字节序问题,但如果你在 Rust 侧用String::from_utf8_unchecked处理非 UTF-8 数据,Go 侧解析就会乱。建议 Rust 侧严格用String::from_utf8并处理错误。
第二个坑是超时语义。Go 的context.WithTimeout取消后,Rust 侧的异步任务可能还在跑。跨语言联调时,超时要两边都设,不能只靠一边。
第三个坑是错误码映射。ONVIF 的 SOAP Fault 和 GB28181 的 SIP 错误码是两套体系,你在平台侧要统一成自己的错误码,不要直接透传。
5.5 性能与资源占用实测
我在一台 4 核 8G 的虚拟机上跑gb28181-go服务端,模拟 500 个设备注册,心跳间隔 30s,CPU 占用约 15%,内存约 200MB。ONVIF 侧用onvif-gov2 发现 200 个设备,并发查能力,CPU 峰值 40%,内存 150MB。这个量级对中小平台够用,但如果你要接几千路,建议把设备发现和能力查询做成异步队列,不要一次性并发。
Rust 设备侧在树莓派 4 上跑gb28181-rs,单设备注册加心跳,CPU 占用不到 1%,内存 10MB 左右。C 侧onvif-c在 ARM Cortex-A7 上跑,内存 5MB 以内,但 WS-Discovery 响应延迟比 Rust 高 10 到 20ms,因为 C 侧通常用阻塞 socket。
提示:如果你在容器里跑这些库,注意组播需要
--network host或显式配置组播路由。Docker 默认 bridge 网络不支持组播,ONVIF 发现会失败。
6. 迁移与升级的实操建议
6.1 从 onvif-go v1 升 v2 的步骤
第一步,改 go.mod 版本,go mod tidy。第二步,go build ./...,把编译错误列出来。第三步,按错误类型分批改:先改 import 路径,再改 Client 初始化,再改方法调用,最后改错误处理。第四步,跑单元测试,重点测设备发现和媒体协商。第五步,在测试环境用真实设备联调,确认 GetStreamUri 返回的地址可用。
不要试图写一个兼容层同时支持 v1 和 v2,那会让代码里到处是if version == 1。直接升,疼一次。
6.2 设备侧库的集成顺序
如果你同时用gb28181-rs和onvif-device-rs做边缘网关,建议先集成 GB28181,因为 GB28181 的信令链路更复杂,调通了再集成 ONVIF。ONVIF 设备侧相对独立,主要是 HTTP 服务端和 SOAP 响应,跟 GB28181 的 SIP 栈不冲突。
集成时注意端口分配。GB28181 用 5060(SIP),ONVIF 用 80 或 8080(HTTP),WS-Discovery 用 3702(UDP)。如果网关只有一个 IP,这些端口都要能同时监听。Rust 的 tokio 可以同时跑多个 listener,没问题。
6.3 监控与日志要加什么
协议库的日志默认可能很吵,你要控制级别。ONVIF 的 SOAP 消息体可能几 KB,全打出来日志会爆。建议只打方法名和错误码,消息体在 debug 级别打。GB28181 的 SIP 消息也是,REGISTER 和心跳可以不打,INVITE 和 BYE 要打。
监控指标建议加:设备在线数、注册失败率、心跳超时数、点播成功率、推流码率。这些指标能帮你快速定位是信令问题还是媒体问题。
6.4 安全加固的底线
ONVIF 的 WS-Security 用 SHA1,GB28181 的 Digest 用 MD5,都不是现代安全机制。你能做的加固是:设备管理口不暴露公网、用独立 VLAN、定期改密码、关闭不需要的 ONVIF 能力(比如不用 PTZ 就关掉)。如果平台侧要暴露给外部,前面加一层网关做鉴权和限流。
另外注意 ONVIF 的 GetSystemDateAndTime 不需要鉴权,有些扫描器会利用这个探测设备。你可以在设备侧禁用这个接口,或者返回假时间。
7. 我个人在实际操作中的体会
这五个库同天发版,最大的价值不是某个库单独变强了,而是它们之间的语义对齐了。我以前做混合协议平台时,最头疼的就是 ONVIF 侧的超时是 5 秒,GB28181 侧是 10 秒,联调时两边状态不一致,排查半天。这次发版如果真把跨库的超时策略和错误码对齐了,那省下的时间比升级本身多得多。
另一个体会是设备侧库的成熟度直接决定项目能不能落地。平台侧库再强,设备侧跑不起来就是空中楼阁。onvif-c首发和gb28181-rs收官,意味着嵌入式团队不用再自己撸 SIP 和 SOAP 了,这对小团队尤其重要。
最后分享一个小技巧:联调时先用tcpdump抓包,把 SIP 和 SOAP 的消息存下来,然后用 Wireshark 分析。Wireshark 能解 SIP 和 SOAP,比看日志快得多。如果你抓不到包,检查是不是走了 TLS,ONVIF 和 GB28181 都支持 TLS,但默认通常不开。