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

相关推荐
浮尘笔记1 分钟前
Go语言并发安全字典:sync.Map的使用与实现
开发语言·后端·golang
淡泊if5 分钟前
RESTful API设计标准:单体 vs 微服务的最佳实践
后端·微服务·restful
金牌归来发现妻女流落街头22 分钟前
【Spring Boot注解】
后端·springboot
无心水32 分钟前
数据库字符串类型详解:VARCHAR、VARCHAR2、CHARACTER VARYING的区别与选择指南
数据库·后端·varchar·varchar2·character·字符串类型·2025博客之星
郑州光合科技余经理1 小时前
同城配送调度系统实战:JAVA微服务
java·开发语言·前端·后端·微服务·中间件·php
Dontla1 小时前
GraphQL介绍(声明式查询)文件上传GraphQL文件上传
后端·graphql
还在忙碌的吴小二1 小时前
Go-View 数据可视化大屏使用手册
开发语言·后端·信息可视化·golang
哪里不会点哪里.1 小时前
什么是 Spring Cloud?
后端·spring·spring cloud
树码小子2 小时前
Spring框架:Spring程序快速上手
java·后端·spring
hssfscv3 小时前
Javaweb学习笔记——后端实战7 springAOP
笔记·后端·学习