DLNA UPnP学习(二):SSDP协议之广播报文

上文说过UPnP是DLNA的“设备发现控制”和“媒体管理控制”协议。要发现控制DLNA媒体设备,就需要学习UPnP体系。

UPnP体系使用SSDP协议发现、搜寻设备。下面先摘抄一段网上关于SSDP协议的描述:

“简单服务发现协议(SSDP,Simple Service Discovery Protocol)是一种应用层协议,是构成通用即插即用(UPnP)技术的核心协议之一。简单服务发现协议提供了在局部网络里面发现设备的机制。控制点(也就是接受服务的客户端)可以通过使用简单服务发现协议,根据自己的需要查询在自己所在的局部网络里面提供特定服务的设备。设备(也就是提供服务的服务器端)也可以通过使用简单服务发现协议,向自己所在的局部网络里面的控制点宣告它的存在”。

看完上述描述,估计大多数人还是不知所云。需要通过实践来进一步了解。

发现设备是UPnP体系运转的第一步。有些中文文章把IP寻址作为UPnP体系运转的第一步,这是错误的。IP地址获取分配是网络基础,不属于UPnP体系的一部分。在UPnP官方文档里,明确将IP寻址设为第0步。第一步是发现设备。

任何一个支持UPnP协议的服务设备(以下简称UPnP设备)加入网络时,该设备会使用SSDP协议向网络广播通告其可提供的服务。实际上,这些设备每隔一段时间(缺省是30分钟)就会使用SSDP协议向网络广播其可提供的服务。与此同时,任何其他设备都可以使用SSDP协议主动查找网络中存在的UPnP设备。

因此,SSDP协议共有两类报文,一类是广播通告(NOTIFY),另一类是搜寻(M-SEARCH)。今天先讲广播通告报文。

下面是一个标准的实际广播报文:

NOTIFY * HTTP/1.1
HOST: 239.255.255.250:1900
CACHE-CONTROL: max-age=30
Location: http://192.168.124.1:5431/dyndev/uuid:00000000-0000-0000-0000-0000000010a0
NT: urn:schemas-upnp-org:service:WANPPPConnection:1
NTS: ssdp:alive
SERVER:LINUX/2.6 UPnP/1.0
USN: uuid:00000000-0000-0000-0000-000002001008::urn:schemas-upnp-org:service:WANPPPConnection:1

第一行NOTIFY表明这是广播通告报文。HTTP/1.1似乎在说这是HTTP/1.1标准的报文,实际上是误导。SSDP协议采用UDP报文,不是HTTP协议,只不过其参数表达方式与HTTP类似。实践中发现,很多设备并不严格遵循HTTP头格式,编程中如果直接使用HTTP标准库来解析报文,就会出错。因此,建议程序员自行解析SSDP报文。

第二行的IP地址 239.255.255.250:1900,是SSDP协议使用的组播地址。

第三行max-age=30告诉接收者,这条消息的有效时间为30分钟。如果30分钟后,接收者没有收到来自这个设备的更新信息(或通过其他操作确认设备存活,如调整音量等),接收者应默认该设备已离线不可用。

第四行Location最重要,这是设备描述的地址。在这个地址会有这个设备基本能力的XML描述。

第五行是这个设备的类型。WANPPPConnection表明这个设备是一台具有广域网拨号功能的设备(如家庭网关路由器)。这种类型的设备不属于DLNA设备。

第六行ssdp:alive表明这个通告是存活类型。类似的,还有离开类型ssdp:byebye。由于很多UPnP设备不存在PC的关机操作,一般都是直接断电或拔网线,这些设备根本来不及发出ssdp:byebye类型的广播通告。因此,编程不能依赖ssdp:byebye报文

第七行SERVER:LINUX/2.6 UPnP/1.0表明这个设备是采用Linux 2.6内核,支持UPnP/1.0版本。这个编程中一般不予理睬。

第八行USN表明这个设备提供的服务名称。

下面用Go语言来编程获取网络中的这些SSDP广播报文

const (
	BROADCAST_VERSION   = "udp4"
	SSDP_IP             = "239.255.255.250"
	SSDP_PORT           = 1900
	SSDP_ALIVE          = "ssdp:alive"
	SSDP_BYEBYE         = "ssdp:byebye"
	UDP_MAX_PACKET_SIZE = 65536
)
var SSDP_ADDR = net.UDPAddr{IP: net.ParseIP(SSDP_IP), Port: SSDP_PORT}

func readData(conn *net.UDPConn) chan *bufio.Reader {
	ch := make(chan *bufio.Reader)
	go func() {
		msg := make([]byte, UDP_MAX_PACKET_SIZE)
		for {
			if n, err := conn.Read(msg); err != nil {
				//conn 已经不可用,说明连接已关闭,则退出循环
				break 
			} else {
				ch <- bufio.NewReaderSize(bytes.NewBuffer(msg), n)
			}
		}
		close(ch)
	}()
	return ch
}

func listenSSDP() error {
	var err error
	//监听239.255.255.250组播地址1900端口的UDP广播报文
	ssdpListenConn, err = net.ListenMulticastUDP(BROADCAST_VERSION, nil, &SSDP_ADDR)
	if err != nil {
		return err
	}

	p := ipv4.NewPacketConn(ssdpListenConn)
	if err := p.SetMulticastTTL(4); err != nil {
		return err
	}
	if err := p.SetMulticastLoopback(true); err != nil {
		return err
	}

	chrdr := readData(ssdpListenConn)

	go func() {
		Loop:
		for {
			select {
			case <-stopListenSig:
				ssdpListenConn.Close()
				break Loop //程序退出
			case rdr := <-chrdr:
				if rdr == nil {
					//通道已关闭
					break Loop
				}
				
				//解析SSDP报文,无关键技术,限于篇幅就不贴出
				msg, err := readSSDPMessage(rdr)
				if err != nil {
					continue
				}
				//只关心通告消息
				if msg.Type != NOTIFY {
					continue
				}
                                
                                deviceType, ok := msg.KeyValuePairs["NT"]
				if !ok {
					//没有NT 域
					continue
				}
				//这里只关心查找MediaRenderer:1类型设备
				if !strings.Contains(deviceType, "MediaRenderer") {
					//不是MediaRenderer类型设备
					continue
				}

				//消息类型
				nts, ok := msg.KeyValuePairs["NTS"]
				if !ok {
					//没有NTS域,非标准设备
					continue
				}
				switch nts {
				case SSDP_ALIVE:
					//增加设备
					...
				case SSDP_BYEBYE:
					//删除设备
					...
					
				}

			}
		}
	}()

	return nil
}

发表评论