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. 参考的关键源文件

相关推荐
小突突突4 小时前
Spring框架中的单例bean是线程安全的吗?
java·后端·spring
iso少年4 小时前
Go 语言并发编程核心与用法
开发语言·后端·golang
掘金码甲哥4 小时前
云原生算力平台的架构解读
后端
码事漫谈4 小时前
智谱AI从清华实验室到“全球大模型第一股”的六年征程
后端
码事漫谈4 小时前
现代软件开发中常用架构的系统梳理与实践指南
后端
Mr.Entropy4 小时前
JdbcTemplate 性能好,但 Hibernate 生产力高。 如何选择?
java·后端·hibernate
YDS8294 小时前
SpringCloud —— MQ的可靠性保障和延迟消息
后端·spring·spring cloud·rabbitmq
无限大65 小时前
为什么"区块链"不只是比特币?——从加密货币到分布式应用
后端
洛神么么哒5 小时前
freeswitch-初级-01-日志分割
后端
蝎子莱莱爱打怪5 小时前
我的2025年年终总结
java·后端·面试