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:rootdeviceuuid:device-uuid::urn:schemas-upnp-org:device:MediaRenderer:1uuid: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. 参考的关键源文件
- PltSsdp.h - SSDP类定义
- PltSsdp.cpp - SSDP实现(含搜索响应迭代器)
- PltDeviceHost.h - 设备主机接口
- PltDeviceHost.cpp - OnSsdpPacket实现
- PltDeviceData.h - 设备数据结构