DMC发送M-SEARCH请求,DMR响应流程

DMC发送M-SEARCH请求,DMR响应的完整函数调用链

概览

当DMC(Digital Media Controller)在网络中发送M-SEARCH多播搜索请求时,DMR(Digital Media Receiver)设备需要接收并响应该请求。下面是完整的函数调用链及详细说明。


1. 网络数据接收阶段

1.1 SSDP监听启动

css 复制代码
PLT_UPnP::Start()
  └─> PLT_DeviceHost::Start(PLT_SsdpListenTask* task)
      └─> m_TaskManager->StartTask(PLT_SsdpListenTask) 
          ├─> 创建UDP多播套接字,监听多播地址 239.255.255.250:1900
          ├─> 加入多播组
          └─> 持续监听SSDP数据包

1.2 M-SEARCH数据包接收

css 复制代码
PLT_SsdpListenTask::DoRun()(运行在任务线程中)
  └─> PLT_HttpServerSocketTask::Run()
      └─> 循环接收UDP多播数据包
          ├─> GetInputStream() 
          │   └─> 将UDP数据包转换为流 (PLT_InputDatagramStream)
          └─> NPT_HttpClient::ReadRequest()
              └─> 解析HTTP格式的M-SEARCH请求

2. M-SEARCH请求处理阶段

2.1 请求处理和监听器通知

css 复制代码
PLT_SsdpListenTask::SetupResponse()
  │
  ├─> 获取SSDP请求和请求上下文(包含源IP和端口)
  │
  └─> m_Listeners.Apply(PLT_SsdpPacketListenerIterator)
      │
      └─> 遍历所有已注册的SSDP监听器,每个监听器执行:
          │
          └─> PLT_SsdpPacketListenerIterator::operator()()
              │
              └─> listener->OnSsdpPacket(request, context)
                  │
                  └─> PLT_DeviceHost::OnSsdpPacket()

2.2 OnSsdpPacket - DMR设备接收处理

scss 复制代码
PLT_DeviceHost::OnSsdpPacket(request, context)
{
    // 1. 提取请求信息
    ├─ 获取请求者IP地址:context.GetRemoteAddress().GetIpAddress()
    ├─ 获取请求方法:request.GetMethod()      // 应该是 "M-SEARCH"
    ├─ 获取搜索目标:PLT_UPnPMessageHelper::GetST(request)
    ├─ 获取延迟时间:PLT_UPnPMessageHelper::GetMX(request)
    └─ 获取手册字段:PLT_UPnPMessageHelper::GetMAN(request)
    
    // 2. 验证请求格式
    ├─ 验证ST字段不为空
    ├─ 验证URL为 "*"
    ├─ 验证协议为 "HTTP/1.1"
    ├─ 验证MAN头为 "ssdp:discover"
    └─ 验证MX不为0
    
    // 3. 关键步骤:创建搜索响应任务
    ├─ 生成随机延迟时间(0到MX秒之间)
    │   └─> 目的:避免网络风暴(所有设备同时回应)
    │
    ├─ 创建 PLT_SsdpDeviceSearchResponseTask 对象
    │   └─> 参数:
    │       ├─ this (PLT_DeviceHost*)
    │       ├─ context.GetRemoteAddress() (请求者地址)
    │       └─ *st (搜索目标类型)
    │
    └─ m_TaskManager->StartTask(task, &timer)
        └─> 在任务管理器中注册任务,延迟时间后执行
}

关键逻辑说明:

  • M-SEARCH是一个多播请求,DMR需要检查该请求是否与自己的设备类型匹配
  • 为了避免网络风暴,DMR不是立即回应,而是等待一个随机的延迟时间(0到MX秒)
  • 每个设备都遵循这个规则,这样可以减少网络中的碰撞和拥塞

3. 搜索响应任务执行阶段

3.1 响应任务执行

arduino 复制代码
PLT_SsdpDeviceSearchResponseTask::DoRun()(在延迟后执行)
{
    // 1. 获取所有活跃的网络接口
    ├─ PLT_UPnPMessageHelper::GetNetworkInterfaces(if_list, true)
    │   └─> 获取所有活跃的网络接口列表
    │
    // 2. 对每个网络接口应用迭代器
    └─ if_list.Apply(PLT_SsdpDeviceSearchResponseInterfaceIterator)
        │
        └─> 使用该迭代器处理每个网络接口
            (详见下一步)
}

3.2 网络接口迭代和响应发送

scss 复制代码
PLT_SsdpDeviceSearchResponseInterfaceIterator::operator()(net_if)
{
    // ========== 第1步:接口验证 ==========
    ├─ 获取网络接口的地址列表
    └─ 如果接口没有有效地址,则跳过该接口 (return NPT_SUCCESS)
    
    // ========== 第2步:接口选择(核心逻辑) ==========
    ├─ 创建UDP套接字 (NPT_UdpSocket socket)
    │
    ├─ 连接到请求者地址
    │   └─> socket.Connect(m_RemoteAddr, 5000ms)
    │       
    │       作用:
    │       ├─ 让操作系统内核选择合适的出站网络接口
    │       ├─ 确定该接口的本地IP地址
    │       └─ 这个IP地址将用于响应中的Location头
    │
    └─ 获取套接字信息
        └─> socket.GetInfo(info)
            └─> info.local_address 就是该接口的本地IP
    
    // ========== 第3步:接口匹配验证 ==========
    ├─ 如果成功获取了本地地址信息
    │   └─> if (info.local_address.GetIpAddress().AsLong())
    │
    ├─ 验证内核选择的网络接口是否与当前遍历的接口匹配
    │   └─> if ((*niaddr).GetPrimaryAddress() != info.local_address.GetIpAddress())
    │       └─> 如果不匹配,跳过此接口 (return NPT_SUCCESS)
    │
    └─ 如果匹配,则设置remote_addr = NULL
        └─> 表示使用已连接的套接字,不需要再指定目标地址
    
    // ========== 第4步:构建SSDP响应 ==========
    ├─ NPT_HttpResponse response(200, "OK", NPT_HTTP_PROTOCOL_1_1)
    │   └─> 创建HTTP 200响应
    │
    ├─ PLT_UPnPMessageHelper::SetLocation(response, device_url)
    │   └─> 设置Location头,包含设备描述文档URL
    │       └─> URL使用本地IP地址:http://[local_ip]:port/device.xml
    │
    ├─ PLT_UPnPMessageHelper::SetLeaseTime(response, lease_time)
    │   └─> 设置CACHE-CONTROL max-age(设备信息有效期)
    │
    ├─ PLT_UPnPMessageHelper::SetServer(response, server_string)
    │   └─> 设置Server头,标识设备类型和版本
    │
    ├─ response.GetHeaders().SetHeader("EXT", "")
    │   └─> 设置EXT头(UPnP规范要求,但值为空)
    │
    └─ 【可选】根据DLNA规范,可能发送两次响应
        ├─ if (PLATINUM_UPNP_SPECS_STRICT)
        │   ├─ m_Device->SendSsdpSearchResponse() [第1次]
        │   ├─ NPT_System::Sleep(DLNA_DELAY) [延迟200ms]
        │   └─> [继续执行第2次发送]
        │
        └─> m_Device->SendSsdpSearchResponse() [第2次或唯一次]
}

4. 响应发送阶段

4.1 设置响应内容

rust 复制代码
PLT_DeviceHost::SendSsdpSearchResponse(response, socket, st, addr)
{
    // 1. 设置UPnP 1.1头信息
    ├─ PLT_UPnPMessageHelper::SetBootId(response, device->m_BootId)
    │   └─> 用于NTS (Notification Type)
    │
    ├─ PLT_UPnPMessageHelper::SetConfigId(response, device->m_ConfigId)
    │   └─> UPnP 1.1规范,配置ID
    │
    // 2. 根据搜索目标类型(ST)决定要发送哪些设备信息
    └─ if (ST == "ssdp:all" || ST == "upnp:rootdevice")
        ├─ 发送根设备信息
        └─ if (ST == "ssdp:all")
            ├─ 也发送服务信息
            └─ 也发送嵌入设备信息
}

4.2 发送SSDP搜索响应

arduino 复制代码
PLT_SsdpSender::SendSsdp(response, usn, st, socket, notify=false, addr)
{
    // 1. 格式化SSDP响应包
    ├─ FormatPacket(response, usn, st, socket, notify)
    │   ├─ PLT_UPnPMessageHelper::SetUSN(message, usn)
    │   ├─ PLT_UPnPMessageHelper::SetST(message, st)
    │   │   └─> notify为false时设置ST(搜索目标)
    │   └─> PLT_UPnPMessageHelper::SetDate(message)
    │
    // 2. 将响应序列化为字节流
    ├─ NPT_MemoryStream stream
    ├─ response.Emit(stream)
    │   └─> 将HTTP响应对象序列化为原始HTTP格式
    │
    // 3. 构造数据包并发送
    ├─ stream.GetSize(size)
    ├─ NPT_DataBuffer packet(stream.GetData(), size)
    │   └─> 从内存流创建数据缓冲区
    │
    └─ socket.Send(packet, addr)
        └─> 通过UDP套接字将响应发送给请求者
            ├─ 如果addr非NULL,发送到指定地址
            └─ 如果addr为NULL,发送到已连接的地址(socket.Connect前缀)
}

5. 完整的HTTP SSDP响应内容示例

makefile 复制代码
HTTP/1.1 200 OK
CACHE-CONTROL: max-age=1800
EXT:
LOCATION: http://192.168.1.100:8008/device.xml
SERVER: Linux/2.6 UPnP/1.0 Platinum/1.2.0
BOOTID.UPNP.ORG: 1234567890
CONFIGID.UPNP.ORG: 1
ST: upnp:rootdevice
USN: uuid:device-uuid::upnp:rootdevice

[空消息体]

6. 关键类和接口关系图

css 复制代码
┌─────────────────────────────────────────────────────────────┐
│  DMC发送M-SEARCH多播请求                                   │
│  UDP多播地址: 239.255.255.250:1900                          │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│  PLT_SsdpListenTask (运行在后台任务线程)                   │
│  - 监听SSDP多播套接字                                       │
│  - 接收M-SEARCH请求并解析                                  │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│  PLT_SsdpListenTask::SetupResponse()                        │
│  - 调用所有已注册的SSDP监听器                              │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│  PLT_DeviceHost::OnSsdpPacket() (DMR设备处理)               │
│  - 解析M-SEARCH请求                                         │
│  - 验证请求格式                                             │
│  - 创建搜索响应任务                                         │
│  - 注册到任务管理器(带随机延迟)                          │
└──────────────────────────┬──────────────────────────────────┘
                           │
         随机延迟(0~MX秒)   │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│  PLT_SsdpDeviceSearchResponseTask::DoRun() (延迟后执行)    │
│  - 获取所有活跃网络接口                                     │
│  - 应用迭代器处理每个接口                                   │
└──────────────────────────┬──────────────────────────────────┘
                           │
                    (对每个接口)
                           ▼
┌─────────────────────────────────────────────────────────────┐
│  PLT_SsdpDeviceSearchResponseInterfaceIterator::operator()  │
│  - 通过UDP连接确定出站接口                                  │
│  - 验证接口匹配性                                           │
│  - 构建HTTP 200响应(包含Location、缓存时间等)            │
│  - 【可选】DLNA规范下发送两次                              │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│  PLT_SsdpSender::SendSsdp()                                 │
│  - 格式化SSDP响应包                                         │
│  - 序列化HTTP响应                                           │
│  - 通过UDP套接字发送                                        │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│  UDP Socket.Send()                                          │
│  - 发送响应给请求者的IP和端口                              │
└─────────────────────────────────────────────────────────────┘
                           │
                           ▼
              ┌────────────────────────┐
              │   DMC接收响应          │
              │ 获取Location URL       │
              │ 下载设备描述文档      │
              └────────────────────────┘

7. 重要的设计考虑

7.1 为什么要延迟响应?

  • 防止网络风暴:多个设备同时收到M-SEARCH,如果立即回应会造成大量数据碰撞
  • 随机延迟:每个设备延迟 0 ~ MX 秒,这样响应会分散开来
  • MX参数:DMC在M-SEARCH中指定MX值,设备据此计算延迟

7.2 为什么要多次遍历网络接口?

  • 多网卡场景:一个DMR可能有多个网络接口(以太网、WiFi等)
  • 正确的Location IP:必须使用将数据发送给请求者的那个接口的IP地址
  • 内核选择:通过socket.Connect()让OS内核选择合适的出站接口

7.3 为什么要发送两次(DLNA规范)?

  • DLNA兼容性:某些DLNA设备需要接收两次响应来确保收到
  • 间隔延迟:两次响应之间延迟约200ms(PLT_DLNA_SSDP_DELAY_GROUP)
  • 可选条件:仅当编译时定义了PLATINUM_UPNP_SPECS_STRICT时

7.4 USN和ST的区别

  • ST(Search Target) :搜索目标,在M-SEARCH请求中指定
    • ssdp:all - 搜索所有设备和服务
    • upnp:rootdevice - 仅搜索根设备
    • urn:schemas-upnp-org:device:MediaRenderer:1 - 搜索特定设备类型
  • USN(Unique Service Name) :响应中的唯一服务名标识
    • uuid:device-uuid::upnp:rootdevice
    • uuid:device-uuid::urn:schemas-upnp-org:device:MediaRenderer:1
    • uuid:device-uuid::urn:schemas-upnp-org:service:RenderingControl:1

8. 代码流追踪总结

arduino 复制代码
网络接收
  ↓
PLT_SsdpListenTask 监听UDP多播
  ↓
接收到M-SEARCH请求
  ↓
PLT_SsdpListenTask::SetupResponse()
  ↓
通知所有监听器 → PLT_DeviceHost::OnSsdpPacket()
  ↓
验证请求+创建响应任务
  ↓
任务管理器→延迟执行
  ↓
PLT_SsdpDeviceSearchResponseTask::DoRun()
  ↓
遍历网络接口→PLT_SsdpDeviceSearchResponseInterfaceIterator
  ↓
选择正确接口+构建HTTP200响应
  ↓
PLT_SsdpSender::SendSsdp()
  ↓
UDP Socket发送响应
  ↓
DMC接收响应并解析

9. 参考的关键源文件

相关推荐
止语Lab1 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活1 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困1 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn1 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
用户298698530141 小时前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
用户77283104908401 小时前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端
Conan在掘金1 小时前
鸿蒙报错速查:Function lacks ending return statement,返回路径漏 return 就炸,根因 + 真解法
后端
人间凡尔赛2 小时前
AI-Native 云原生架构:2026 年从容器编排到智能体编排的范式革命
后端·云原生·架构
IT_陈寒2 小时前
Vue的响应式让我加班到凌晨3点,原来问题出在这
前端·人工智能·后端
卷无止境2 小时前
提升 Python 代码健壮性的方法大盘点
后端·python