上文说过SSDP协议有两种报文,一类是广播通告(NOTIFY),另一类是搜寻(M-SEARCH)。广播报文已经讲过了,今天来讲搜寻报文。
广播报文是UPnP服务设备主动发出,控制设备可以默默监听并收集到网络内的UPnP设备。搜寻报文一般是由控制设备发出,符合搜寻条件的DLNA服务设备予以响应。下面是一个标准的SSDP搜寻报文:
M-SEARCH * HTTP/1.1 HOST: 239.255.255.250:1900 MAN: "ssdp:discover" MX: 5 ST: urn:schemas-upnp-org:service:AVTransport:1
第一行,M-SEARCH表明这是搜寻报文。前文说过,这里的”HTTP/1.1″并不是说明SSDP采用了HTTP协议,而仅是使用了类似HTTP 头的格式。
第二行HOST: 239.255.255.250:1900是SSDP协议使用的组播地址,不可变更。
第三行MAN是根据HTTP扩展规范设立的,这里是固定值“ssdp:discover”。注意,与其他行不同,标准要求这里必须包含双引号。
第四行MX是告诉收到搜寻报文的UPnP设备在多长时间(秒)内响应有效。也就是说,控制设备将等待多长时间以便收到其他设备发出的响应。虽然,控制设备自己等待时间可以超过这个值,但是其他设备大概率不会在超过MX确定的秒数后再发送响应报文。这个数字有效值是1秒到120秒。建议不要设置太小,否则会有很多设备由于比较忙,不会及时响应搜寻报文,造成控制设备发现不了这些UPnP服务设备。这样,就会发生用户确定网络中存在某UPnP设备,但是你的程序却始终无法发现这个设备,用户体验不好。
第五行ST是指定搜寻哪个类型的设备。这里的例子是搜寻具有AVTransport服务能力(即媒体播放)的设备。有效的值有:
- ssdp:all,搜寻所有遵循UPnP协议的服务设备
- upnp:rootdevice,只搜寻根设备。拥有子设备的设备就是根设备。比如,电脑可以拥有显示器、音箱、媒体文件服务器等子设备。
- uuid:device-UUID,搜寻具体的某个设备,这里就填该设备的UUID
- urn:schemas-upnp-org:device:deviceType:v,搜寻设备类型,后面的v是版本,设备类型和版本由UPnP论坛工作委员会定义。常见的有MediaRenderer(媒体播放)、MediaServer(媒体服务器),详细的设备类型及其相关标准在这里
- urn:schemas-upnp-org:service:serviceType:v,搜寻具备这种服务的设备,常见的服务类型有,AVTransport、RenderingControl(控制播放音量、亮度等)、ConnectionManager等。
- urn:domain-name:device:deviceType:v
- urn:domain-name:service:serviceType:v,这两个可以搜寻非标的设备,domain-name和deviceType、serviceType是厂商自定义。只不过域名中的“.”得换成“-”
当控制设备发搜寻报文后,符合搜寻条件的UPnP设备大概率会响应(未响应是因为繁忙)。下面是一个响应前面搜寻报文例子的一个响应报文:
HTTP/1.1 200 OK Location: http://192.168.124.10:1152/ Cache-Control: max-age=1800 Server: UPnP/1.0 DLNADOC/1.50 Platinum/1.0.5.13 EXT: BOOTID.UPNP.ORG: 1678443271 CONFIGID.UPNP.ORG: 4274641 USN: uuid:BA6900075320096ff79d::urn:schemas-upnp-org:service:AVTransport:1 ST: urn:schemas-upnp-org:service:AVTransport:1 Date: Fri, 10 Mar 2023 14:50:28 GMT
第一行,200 OK表明这个设备收到了正确的搜寻报文,并给出了成功响应。
第二行,Location给出了这个设备描述的地址。在这个地址会有这个设备基本能力的XML描述。
第三行,Server指出该设备支持UPnP版本1.0,DLNA 1.5,并包含设备自身版本
第四行,EXT是对搜寻报文中MAN的回应,没有具体内容。这是根据HTTP扩展规范要求的。
第五行、第六行是UPnP 2.0里的内容,在UPnP 1.0里是没有的。BOOTID.UPNP.ORG用于指示设备离线/入网次数。每次设备离线后(如重启)再次入网,该值将加1。如果两个设备的UUID相同,但是该值不同,说明设备要么在不同IP地址同时出现(多宿主设备),要么已经“重启“(这里的重启用引号表示不一定是真正的重启,也有可能是断线后再入网而已)。在这种情况下,该设备之前的状态已经丢失(不可用),我们的程序需要重新与其连接设置一些必要的参数(如订阅事件等)。同样,CONFIGID.UPNP.ORG表示该设备当前描述信息的序号(可以理解为版本号),当这个数字与我们程序之前掌握的不同时,理论上程序应该重新读取设备描述信息。
第七行,USN的内容在UPnP 1.0版本里只有设备的UUID。在UPnP 2.0 版本里,就包含了这个响应报文实际对应的设备或服务
第八行,ST的内容说明同搜寻报文里的一样,是这条响应报文对应的设备或服务。
通过第七、第八行的解释说明可以看出,由于一个物理设备可以包含多个物理子设备或逻辑子设备,每个子设备又包含多个服务,因此对于一个搜寻报文,一个物理设备将发出许多个响应报文。
虽然标准很清晰,但是实践中有一些设备并未完全遵照标准,如出现大小写、回车换行前出现空格等等。这里还是要建议程序员自己解析报文,不要使用标准的HTTP头解析库,否则很容易发生解析失败。
下面,来段示例程序可以更清楚地理解搜寻报文,与上文示例程序里相同的部分就不重复了。
const (
AvDeviceTypeName = "urn:schemas-upnp-org:service:AVTransport:1"
)
func makeMSearchString(searchType string) string {
//MX 最大等待时间 6秒
return fmt.Sprintf(
"M-SEARCH * HTTP/1.1\r\nHost:%s\r\nST:%s\r\nMan:\"ssdp:discover\"\r\nMX:6\r\n\r\n",
SSDP_ADDR.String(), searchType)
}
func searchAvDevices() error {
conn, err := net.ListenUDP(BROADCAST_VERSION, &net.UDPAddr{IP: net.IPv4zero, Port: 0})
if err != nil {
return err
}
defer conn.Close()
if _, err := conn.WriteTo([]byte(makeMSearchString(AvDeviceTypeName)), &SSDP_ADDR); err != nil {
return err
}
//等待6秒,收集6秒钟内返回的
endTime := time.NewTicker(6 * time.Second)
chrdr := readData(conn)
Loop:
for {
select {
case <-endTime.C:
break Loop //时间到,退出
case rdr := <-chrdr:
//解析SSDP报文,无关键技术,限于篇幅就不贴出
msg, err := readSSDPMessage(rdr)
if err != nil {
//错误处理
...
} else {
//添加设备,读取设备描述
...
}
}
}
return nil
}
现在我们已经知道了有两种方法获取网络中的DLNA设备。由于SSDP协议采用UDP报文,通讯方式是不可靠的,在实践中,这两种方法要结合使用,并且要多次搜寻,才会将网络内符合条件的DLNA设备收集全。同时DLNA设备经常会断电离线,或在程序搜寻后才开机。因此,建议要在后台定时获取刷新。