资讯动态

PHP实现GB28181 SIP信令服务器:UDP socket与状态机实战

发布时间:2026/9/14 4:51:39 来源:尧图企业网站定制
简介本资源是一套基于PHP语言实现的GB28181国标网络视频平台源码完整支持SIP协议解析与信令服务器功能面向计算机科学、信息安全、物联网、人工智能等专业在校学生及教师适用于课程设计、大作业、毕业设计及初期项目原型开发。压缩包共490个文件主体为339个PHP后端逻辑文件含SIP信令处理、设备注册/心跳/媒体流控制等核心模块辅以44个JavaScript前端交互脚本、37个CSS样式文件及41个source map调试支持文件另有Dockerfile、SQL建表语句、测试脚本及README说明文档整体体积15.77MB结构清晰、模块解耦度高。目前已有287人学习下载代码经实际验证可稳定运行提供从设备接入、信令交互到Web端视频预览的全链路参考实现特别适合深入理解GB28181协议栈在PHP生态中的落地实践。1. 用 PHP 搭建 GB28181 视频平台不是“写个 Web 页面”而是要让 SIP 信令在 Web 服务层真正跑通很多人看到“PHP GB28181”第一反应是PHP 不是做网站的吗怎么能处理实时音视频信令这恰恰是本项目最值得深挖的实践价值——它不依赖 Go/Java/C 等传统信令语言而是用 PHP 实现了完整的 SIP 协议栈非简单封装 cURL 或 shell 调用并构建了可承载设备注册、心跳保活、目录查询、实时流媒体邀请与应答的轻量级信令服务器。它解决的是中小安防集成商、边缘计算场景下快速验证 GB28181 接入能力的实际问题无需部署独立 SIP 服务如 Kamailio/Freeswitch不强依赖 Docker 或复杂编译环境直接在 Windows 10 Nginx PHP 环境下即可启动核心信令逻辑。适合已有 PHP 运维能力但缺乏 VoIP 协议经验的团队也适合作为 GB28181 客户端如大疆 M300 RTK、Mavic 2 行业版对接测试的最小可行信令端。关键在于它把 SIP 的 UDP 事务管理、SDP 解析、状态机迁移、ACK/BYE 时序控制等底层逻辑全部用 PHP 原生 socket 和事件循环落地而非仅做 HTTP 接口代理。2. PHP 实现 SIP 协议栈的核心设计为什么不用 cURL而必须用 stream_socket_serverGB28181 标准要求设备通过 SIP over UDP 向平台发起 REGISTER、MESSAGE、NOTIFY 等信令交互且对事务 ID、CSeq、Via 分支、Call-ID 一致性、重传机制、超时响应有严格定义。cURL 本质是 HTTP 客户端无法构造原始 SIP 请求头字段如Contact: sip:34020000001320000001192.168.1.100:5060;expires3600、无法监听 UDP 端口接收设备主动发来的 BYE 或 ACK、更无法维护每个设备的对话Dialog生命周期。因此本项目采用 PHP 原生 socket 扩展实现双协议栈支持并通过 select() 非阻塞轮询管理多路 UDP 连接。2.1 UDP socket 初始化与多路复用结构// sip_server.php $socket stream_socket_server(udp://0.0.0.0:5060, $errno, $errstr, STREAM_SERVER_BIND); if (!$socket) { die(UDP socket 创建失败: $errstr ($errno)\n); } stream_set_blocking($socket, false); // 关键设为非阻塞 // 维护设备注册表内存级生产环境需替换为 Redis $registeredDevices []; // key: device_id, value: [ip, port, expires, contact_uri, last_heartbeat] // 主循环select 监听 socket 定时器触发心跳检查 while (true) { $read [$socket]; $write $except null; $tv_sec 0; $tv_usec 50000; // 50ms 精度 $n stream_select($read, $write, $except, $tv_sec, $tv_usec); if ($n 0 in_array($socket, $read)) { $buf ; $peer []; $len stream_socket_recvfrom($socket, 1024, 0, $peer); if ($len 0) { $rawSip trim($buf . substr($buf, 0, $len)); handleSipMessage($rawSip, $peer[address], $peer[port]); } } // 每秒检查一次设备过期状态简化版实际需定时器队列 if (time() % 1 0) { checkDeviceExpiry(); } }提示stream_socket_server(udp://...)是 PHP 7.4 对 UDP 支持的稳定接口stream_select()替代了已废弃的socket_select()兼容性更好。不要用fsockopen()—— 它只支持 TCP且无法复用同一 socket 处理多个设备请求。2.2 SIP 消息解析器从 raw 字符串到结构化数组GB28181 设备发送的 REGISTER 请求示例截断REGISTER sip:34020000002000000001192.168.1.100 SIP/2.0 Via: SIP/2.0/UDP 192.168.1.200:5060;branchz9hG4bK1234567890 From: sip:34020000001320000001192.168.1.200:5060;tagabcd1234 To: sip:34020000002000000001192.168.1.100 Call-ID: 1234567890192.168.1.200 CSeq: 1 REGISTER Contact: sip:34020000001320000001192.168.1.200:5060;expires3600 Max-Forwards: 70 User-Agent: HIKVISION-GB28181 Content-Length: 0对应解析函数需提取关键字段并校验合法性function parseSipMessage(string $raw): array { $lines explode(\r\n, $raw); $method strtok($lines[0], ); $requestUri strtok( ); $version strtok( ); $headers []; $body ; $inBody false; foreach ($lines as $line) { if ($line !$inBody) { $inBody true; continue; } if ($inBody) { $body . $line . \r\n; } else { if (strpos($line, :) ! false) { [$key, $val] explode(:, $line, 2); $headers[strtolower(trim($key))] trim($val); } } } // 提取 Contact 中的 IP:PORT用于后续 RTP 流地址推导 $contact $headers[contact] ?? ; preg_match(/sip:(.*?)(.*?):(\d)/i, $contact, $m); $contactIp $m[2] ?? ; $contactPort (int)($m[3] ?? 5060); return [ method $method, uri $requestUri, version $version, headers $headers, body $body, contact_ip $contactIp, contact_port $contactPort, from $headers[from] ?? , to $headers[to] ?? , call_id $headers[call-id] ?? , cseq $headers[cseq] ?? , via $headers[via] ?? , ]; } // 示例调用 $msg parseSipMessage($rawSip); if ($msg[method] REGISTER) { $deviceId extractDeviceIdFromUri($msg[from]); // 如从 sip:34020000001320000001... 提取 34020000001320000001 registerDevice($deviceId, $msg[contact_ip], $msg[contact_port], $msg[headers][expires] ?? 3600); }注意extractDeviceIdFromUri()必须按 GB28181 第 5.1.2 条规范提取 20 位数字编码前 2 位为行政区划代码不能简单正则匹配任意数字串registerDevice()需校验Expires是否在 300–3600 秒范围内否则返回423 Interval Too Brief。2.3 设备注册状态机与心跳保活策略GB28181 要求设备每 30–60 秒发送一次 REGISTER带相同 Call-ID 和 From tag平台需更新expires时间戳并返回 200 OK。若连续两次未收到心跳则视为离线。本项目采用内存数组模拟状态机但生产环境必须替换为 Redis Hash 存储字段类型说明device_idstring20 位国标编码主键ipstring设备当前公网/内网 IP来自 Contactportint设备 SIP 端口默认 5060expires_atinttime() expires 值用于过期判断last_heartbeatint最后一次收到 REGISTER 的时间戳contact_uristring完整 Contact URI用于构造 NOTIFY 或 INVITEfunction registerDevice(string $deviceId, string $ip, int $port, int $expires): void { global $registeredDevices; $now time(); $expiresAt $now $expires; // 允许设备刷新注册同一 device_id 更新 $registeredDevices[$deviceId] [ ip $ip, port $port, expires_at $expiresAt, last_heartbeat $now, contact_uri sip:{$deviceId}{$ip}:{$port}, ]; } function checkDeviceExpiry(): void { global $registeredDevices; $now time(); foreach ($registeredDevices as $id $dev) { if ($dev[expires_at] $now) { // 设备已过期触发离线事件如写日志、发 MQTT 通知 error_log(Device {$id} expired at {$dev[expires_at]}); unset($registeredDevices[$id]); } } }提示checkDeviceExpiry()若放在主循环中每秒执行会随设备数线性增长 CPU 开销。高并发场景应改用 Redis Sorted SetZADD timestamp device_id用ZRANGEBYSCORE 0 $now批量清理再ZREM删除。3. 构建 GB28181 信令服务器从单文件脚本到可部署服务进程仅靠php sip_server.php在前台运行无法满足生产需求无守护进程、无日志分级、无平滑重启、无资源隔离。本项目提供supervisord配置模板与信号处理机制使 PHP SIP 服务具备工业级稳定性。3.1 使用 pcntl 实现信号捕获与优雅退出PHP 默认忽略SIGTERM需显式注册处理器确保在收到 kill 命令时完成最后一条 REGISTER 响应再关闭 socketdeclare(ticks 1); function signalHandler(int $signo): void { global $socket; switch ($signo) { case SIGTERM: case SIGINT: echo 收到终止信号正在关闭 SIP socket...\n; if ($socket) { fclose($socket); } exit(0); default: break; } } pcntl_signal(SIGTERM, signalHandler); pcntl_signal(SIGINT, signalHandler); pcntl_signal_dispatch(); // 启用信号分发注意pcntl扩展在 Windows 下不可用若需 Windows 部署应改用win32service扩展或直接使用nohup php sip_server.php 进程名 grep 方式管理。3.2 supervisord 配置文件/etc/supervisor/conf.d/gb28181-sip.conf[program:gb28181-sip] command/usr/bin/php /var/www/gb28181/sip_server.php autostarttrue autorestarttrue startretries3 userwww-data redirect_stderrtrue stdout_logfile/var/log/gb28181/sip.log stdout_logfile_maxbytes10MB stdout_logfile_backups5 environmentPHP_IDE_CONFIGserverNamelocal执行生效命令sudo supervisorctl reread sudo supervisorctl update sudo supervisorctl start gb28181-sip sudo supervisorctl status gb28181-sip提示stdout_logfile路径需提前创建并赋权sudo mkdir -p /var/log/gb28181 sudo chown www-data:www-data /var/log/gb28181日志中应记录每条 REGISTER 的device_id、ip、expires便于排查防火墙 SIP ALG 干扰问题常见现象设备注册成功但无法收到平台 NOTIFY。3.3 防火墙与 SIP ALG 冲突规避方案家用/企业路由器常启用 SIP ALGApplication Layer Gateway它会篡改 SIP 包中的 Via、Contact 字段的 IP:PORT导致设备无法正确回包。验证方法抓包对比设备发出的 REGISTER 与平台收到的Contact字段是否一致。现象原因解决方案设备注册成功但平台收不到心跳SIP ALG 修改了 Contact 中的端口关闭路由器 SIP ALG 功能或强制设备使用固定端口如 5060并映射到公网平台发送 NOTIFY 后设备无响应SIP ALG 丢弃了非标准端口的 UDP 包在Contact中声明;transportudp并在Via头显式指定 transport设备频繁掉线ALG 重置了 UDP 连接状态启用Keep-Alive: timeout30头非标准部分设备支持或缩短 REGISTER expires 至 60 秒本项目在handleSipMessage()中增加 ALG 检测逻辑if (isset($msg[headers][via])) { $via $msg[headers][via]; if (preg_match(/;received(\d\.\d\.\d\.\d)/, $via, $m)) { // 检测到 received 字段说明经过 NAT/ALG需用该 IP 替换 Contact 中的 IP $msg[contact_ip] $m[1]; } }4. 实现 GB28181 设备目录查询与实时流媒体邀请用 PHP 构造 SDP 并响应 INVITEGB28181 平台需支持上级平台如省平台通过 SIP MESSAGE 查询下级设备列表或通过 SIP INVITE 发起实时视频点播。PHP 无法直接生成 RTP 流但可作为信令中枢将 INVITE 请求转发至流媒体服务如 SRS、FFmpeg再将 SDP 回传给发起方。4.1 解析 MESSAGE 请求并返回设备目录 XML上级平台发送的 MESSAGE 请求体为 XML 格式?xml version1.0 encodingUTF-8? Query CmdTypeCatalog/CmdType SN12345/SN DeviceID34020000001320000001/DeviceID /QueryPHP 需解析该 XML 并构造响应function handleCatalogQuery(string $xmlBody): string { $xml simplexml_load_string($xmlBody); if ((string)$xml-CmdType ! Catalog) { return ; } $devices getRegisteredDevices(); // 返回 $registeredDevices 数组 $response new SimpleXMLElement(?xml version1.0 encodingUTF-8?Response/Response); $response-addChild(CmdType, Catalog); $response-addChild(SN, (string)$xml-SN); $response-addChild(DeviceID, (string)$xml-DeviceID); $response-addChild(SumNum, count($devices)); $itemList $response-addChild(Item); foreach ($devices as $dev) { $item $itemList-addChild(Item); $item-addChild(DeviceID, $dev[device_id]); $item-addChild(Name, Camera- . $dev[device_id]); $item-addChild(Manufacturer, HIKVISION); $item-addChild(Status, ON); $item-addChild(Online, ON); $item-addChild(Type, IPC); $item-addChild(ParentID, 34020000002000000001); // 平台自身 ID } return $response-asXML(); }注意simplexml_load_string()要求 XML 格式严格需用libxml_use_internal_errors(true)捕获解析错误asXML()输出含 XML 声明需确保响应头Content-Type: application/xml。4.2 处理 INVITE 请求并生成 SDP Offer/Answer当上级平台发起点播时发送 INVITEINVITE sip:34020000001320000001192.168.1.200 SIP/2.0 ... Content-Type: application/sdp Content-Length: ... v0 oZLMediaKit 0 0 IN IP4 127.0.0.1 sZLMediaKit cIN IP4 127.0.0.1 t0 0 mvideo 10000 RTP/AVP 96 artpmap:96 PS/90000 afmtp:96 profile-level-id000000;packetization-mode1PHP 不生成 RTP 流但需提取设备 ID 与目标流通道由Content-Disposition:录像或asendrecv判断调用本地流媒体服务 API如 SRS/api/v1/streams获取流地址构造 SDP Answer 并返回 200 OK。function handleInvite(array $msg): string { $deviceId extractDeviceIdFromUri($msg[uri]); $sdpOffer $msg[body]; // 提取目标流通道GB28181 中 channel_id 通常为 1~16 $channelId 1; if (preg_match(/asendrecv.*?channel(\d)/i, $sdpOffer, $m)) { $channelId (int)$m[1]; } // 调用 SRS 获取流地址假设 SRS 运行在 localhost:1985 $srsUrl http://127.0.0.1:1985/api/v1/streams?applivestream{$deviceId}_{$channelId}; $ch curl_init(); curl_setopt($ch, CURLOPT_URL, $srsUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $srsResp curl_exec($ch); curl_close($ch); if (!$srsResp || json_decode($srsResp, true)[code] ! 0) { return generateSdpError(480 Temporarily Unavailable); } // 构造 SDP Answer简化版实际需匹配 Offer 中的 codec、port、ip $localIp getLocalIp(); // 获取本机内网 IP $sdpAnswer EOD v0 oGB28181-Platform 0 0 IN IP4 {$localIp} sGB28181-Stream cIN IP4 {$localIp} t0 0 mvideo 10000 RTP/AVP 96 artpmap:96 PS/90000 afmtp:96 profile-level-id000000;packetization-mode1 acontrol:trackID1 asendrecv EOD; return $sdpAnswer; }提示getLocalIp()应避免$_SERVER[SERVER_ADDR]可能为 127.0.0.1推荐用gethostbyname(gethostname())或读取/etc/hostsSDP 中的mvideo端口需与流媒体服务实际监听端口一致如 SRS 默认 10000否则设备无法建立 RTP 连接。4.3 关键参数表GB28181 SIP 信令必调字段对照字段名出现场景PHP 实现要点常见错误Call-ID所有请求/响应全局唯一建议uniqid() . bin2hex(random_bytes(8))多设备共用同一 Call-ID 导致事务混乱CSeqREGISTER/INVITE/MESSAGE方法名 递增数字如1 REGISTER→2 REGISTERCSeq 未递增设备拒绝响应Via所有请求必须含branchz9hG4bK...且每次新请求 branch 不同branch 重复导致 483 错误ContactREGISTER必须含;expiresNN ∈ [300,3600]expires0 或超限返回 423Content-TypeMESSAGE/INVITEapplication/xml或application/sdp类型错误导致上级平台解析失败Max-Forwards所有请求初始值 70每经一跳减 1小于 1 时平台应返回 4835. 生产环境调优与排错技巧如何快速定位 GB28181 设备无法注册问题GB28181 对接中最常见的不是代码 bug而是网络层与协议层的隐性冲突。以下为一线工程师验证流程每步均可在 2 分钟内完成。5.1 三步网络连通性验证法UDP 端口可达性在平台服务器执行# 检查 5060 是否监听 ss -uln | grep :5060 # 从设备所在网络 ping 平台 IP确认路由通 ping -c 3 192.168.1.100 # 用 socat 模拟设备发 REGISTER绕过设备固件 echo -e REGISTER sip:34020000002000000001192.168.1.100 SIP/2.0\r\nVia: SIP/2.0/UDP 192.168.1.200:5060;branchz9hG4bK123\r\nFrom: sip:34020000001320000001192.168.1.200:5060;tag123\r\nTo: sip:34020000002000000001192.168.1.100\r\nCall-ID: 123192.168.1.200\r\nCSeq: 1 REGISTER\r\nContact: sip:34020000001320000001192.168.1.200:5060;expires3600\r\nMax-Forwards: 70\r\nContent-Length: 0\r\n\r\n | socat - udp-datagram:192.168.1.100:5060若平台日志出现REGISTER from 192.168.1.200说明 UDP 通。SIP ALG 干预检测在平台抓包sudo tcpdump -i any -n udp port 5060 -w sip.pcap用 Wireshark 打开过滤sip.Request-Line contains REGISTER对比设备发出的Contact与平台收到的Contact是否 IP:PORT 一致。不一致即 ALG 干预。防火墙规则检查# Ubuntu/Debian sudo ufw status verbose # CentOS/RHEL sudo firewall-cmd --list-all # 确保放行 UDP 5060 及 RTP 端口范围如 10000–200005.2 PHP 运行时关键配置项核查表配置项推荐值检查命令作用max_execution_time0不限制php -i | grep max_execution_time防止长连接被中断memory_limit256Mphp -i | grep memory_limitSIP 解析与 XML 生成需内存default_socket_timeout60php -i | grep default_socket_timeout影响 stream_socket_client 超时extensionsocketsenabledphp -m | grep socketsUDP socket 必需扩展extensionpcntlenabledLinuxphp -m | grep pcntl信号处理必需5.3 设备端典型日志关键词速查日志片段含义应对措施Register failed: 401 Unauthorized平台未返回 WWW-Authenticate检查平台是否实现 Digest 认证本项目默认关闭需注释掉requireAuth()Register failed: 423 Interval Too BriefExpires 值小于 300修改设备配置或平台registerDevice()中的校验阈值No response to REGISTER平台未发 200 OK检查handleSipMessage()是否遗漏sendResponse()调用Received BYE but no dialog foundCall-ID 不匹配检查设备是否复用旧 Call-ID或平台未持久化对话状态提示大疆 M300 RTK 默认使用transportudp若平台Via头未声明;transportudp设备可能静默丢包在buildViaHeader()函数中强制添加该参数可解决。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价