1, SSDP(Simple Service Discovery Protocol)即简单服务发现协议,它是 UPnP(Universal Plug and Play)框架的基础协议之一,主要用于在本地网络中发现设备和服务。
SSDP 基于 UDP 协议,工作在 IP 网络层。
其核心工作机制如下:
搜索请求(M - SEARCH):客户端向多播地址 239.255.255.250 的端口 1900 发送 M - SEARCH 请求,以搜索特定类型的设备或服务。请求中会包含 ST(Search Target)字段,用于指定搜索的目标类型。
响应(200 OK):当设备接收到 M - SEARCH 请求,并且其服务类型与 ST 字段匹配时,会向客户端发送一个包含自身信息的响应消息。
通告(NOTIFY):设备可以主动向多播地址发送 NOTIFY 消息,来宣告自己的存在、可用状态变化(如上线、下线)等信息。消息中会包含 NTS(Notification Sub - Type)字段,用于表示通告的类型。
核心特点:
多播地址:239.255.255.250:1900
无连接:基于 UDP 协议
消息类型:
NOTIFY(设备主动宣告存在)、M-SEARCH(客户端搜索请求)、HTTPU 响应(设备回应)
工作流程:
设备启动时发送 NOTIFY 宣告存在,客户端发送 M-SEARCH 搜索请求,匹配设备回复响应。
发现设备代码示例如下:
const val SSDP_MULTICAST_ADDRESS = "239.255.255.250"
const val SSDP_PORT = 1900
const val SSDP_SEARCH_TARGET = "ssdp:all"
const val SSDP_MX = 3
data class SsdpDevice(val ipAddress: String, val deviceInfo: String)
private fun discoverSsdpDevices(): List<SsdpDevice> {
// 获取 SSDP 协议规定的多播地址 239.255.255.250 的 InetAddress 对象。
val ssdpMulticastAddress = InetAddress.getByName("239.255.255.250")
val ssdpPort = 1900
// 创建一个 UDP 数据报套接字,用于发送和接收 UDP 数据包
val socket = DatagramSocket()
// 设置套接字支持广播功能,确保可以向多播地址发送数据包。
socket.broadcast = true
// Kotlin 中使用三重引号 """ 可以创建多行字符串,
// 这种方式允许字符串中包含换行符和特殊字符,而无需使用转义字符。
// M-SEARCH * HTTP/1.1 是 SSDP 请求的起始行,
// 表示这是一个搜索请求,使用的是 HTTP/1.1 协议。
// HOST: 239.255.255.250:1900 指定了 SSDP 多播地址和端口号。
// 239.255.255.250 是 SSDP 的标准多播地址,1900 是标准端口。
// 客户端会将请求发送到这个地址和端口,以便网络中的所有支持 SSDP 的设备都能接收到请求。
// MAN: "ssdp:discover" 表明这是一个 SSDP 发现请求。该字段用于指示请求的类型。
// MX: 3 表示设备在收到请求后,最多可以在 3 秒内响应。
// 这个值是一个建议值,设备可以根据自身情况在 0 到 MX 指定的时间内随机选择一个时间进行响应,以避免大量设备同时响应导致网络拥塞。
// ST: urn:schemas-upnp-org:device:ZonePlayer:1 指定了搜索目标。
// 这里的 urn:schemas-upnp-org:device:ZonePlayer:1 是一个统一资源名称(URN),表示要搜索的设备类型是 ZonePlayer 设备的版本 1。
// 代表要搜索符合 schemas-upnp-org 规范、设备类型为 ZonePlayer 且版本是 1 的所有设备。客户端通过这个字段来筛选感兴趣的设备。
// 若将 ST 字段设置为 ssdp:all,就会搜索本地网络里所有支持 SSDP 协议的设备,不管设备类型和版本如何。
val mSearchRequest = """
M-SEARCH * HTTP/1.1
HOST: 239.255.255.250:1900
MAN: "ssdp:discover"
MX: 3
ST: urn:schemas-upnp-org:device:ZonePlayer:1
"""
.trimIndent() // trimIndent()方法用于去除多行字符串中每行的前导缩进,确保字符串内容的格式正确。这在使用三重引号字符串时非常有用,因为它可以让代码更具可读性,同时不影响最终生成的字符串内容。
.toByteArray() // toByteArray() 方法将字符串转换为字节数组,因为在网络通信中,数据通常需要以字节数组的形式进行传输。这样处理后,mSearchRequest 就可以作为 UDP 数据包的内容发送出去。
// 创建一个 UDP 数据包,包含请求消息、消息长度、目标地址和端口
val sendPacket = DatagramPacket(mSearchRequest, mSearchRequest.size, ssdpMulticastAddress, ssdpPort)
// 通过套接字发送 UDP 数据包。
socket.send(sendPacket)
// 创建一个长度为 1024 的字节数组作为接收缓冲区。
val buffer = ByteArray(1024)
// 创建一个 UDP 数据包,用于接收设备的响应。
val receivePacket = DatagramPacket(buffer, buffer.size)
val ssdpDevices = mutableListOf<SsdpDevice>()
try {
// 设置套接字的超时时间为3秒,
// 即如果在3秒内没有接收到新的数据包,socket.receive(receivePacket) 方法将抛出 SocketTimeoutException 异常。
socket.soTimeout = 3000
while (true) {
// 从套接字接收 UDP 数据包。
socket.receive(receivePacket)
// 将接收到的字节数组转换为字符串
val response = String(receivePacket.data, 0, receivePacket.length)
// 检查响应消息中是否包含搜索目标 urn:schemas-upnp-org:device:ZonePlayer:1,
// 如果包含,则认为该设备是符合要求的设备。
if (response.contains("urn:schemas-upnp-org:device:ZonePlayer:1")) {
// 获取设备的 IP 地址。
val ip = receivePacket.address.hostAddress
Log.v(TAG, "discoverSsdpDevices...ip:$ip")
// 创建一个 SsdpDevice 对象,包含设备的 IP 地址和响应消息。
val device = SsdpDevice(ip, response)
ssdpDevices.add(device)
Log.v(TAG, "discover ssdp device:${response}")
}
}
} catch (e: Exception) {
Log.v(TAG, "discover ssdp device exception:$e")
} finally {
// 关闭 UDP 套接字,释放资源。
socket.close()
}
return ssdpDevices
}
对于上面的response,也就是从设备返回的数据信息如下:
// HTTP/1.1 200 OK:表示这是一个 HTTP/1.1 协议的响应,状态码为 200,意味着请求已成功处理,设备成功响应了客户端的查询请求。
HTTP/1.1 200 OK
// 缓存控制字段,指示客户端可以将该响应缓存 1800 秒(30 分钟)。在此期间,客户端可以直接使用缓存的响应,而无需再次向设备发送请求。
CACHE-CONTROL: max-age = 1800
// 一个空的扩展字段,在 SSDP 中,EXT 字段通常用于表示这是一个有效的 SSDP 响应,并且可能包含其他扩展信息,但在此处为空。
EXT:
// 提供了设备描述文件的 URL 地址。客户端可以通过访问这个 URL 来获取设备的详细描述信息,例如设备支持的服务、功能等,以 XML 格式呈现。
LOCATION: http://192.168.11.93:1400/xml/device_description.xml
// 表示设备运行的操作系统为 Linux,使用的 UPnP 协议版本为 1.0,设备品牌是 Sonos,版本号为 84.1-63110,设备型号为 ZPS18。
SERVER: Linux UPnP/1.0 Sonos/84.1-63110 (ZPS18)
// 搜索目标字段,与客户端发送的 M-SEARCH 请求中的 ST 字段相对应,表明该设备是符合 urn:schemas-upnp-org:device:ZonePlayer:1 类型的设备,即 Sonos 的 ZonePlayer 设备版本 1。
ST: urn:schemas-upnp-org:device:ZonePlayer:1
// 唯一服务名称(Unique Service Name),由设备的 UUID(Universally Unique Identifier,通用唯一识别码)和设备类型组成。这个字段用于唯一标识设备和其提供的服务。
USN: uuid:RINCON_F0F6C18B57FE01400::urn:schemas-upnp-org:device:ZonePlayer:1
// 这是 Sonos 设备特有的字段,可能用于标识设备所属的家庭网络或家庭组。
X-RINCON-HOUSEHOLD: Sonos_1r5qlk84JKZVqkesjdjfpBTsXi
// X-RINCON-BOOTSEQ: 17 和 BOOTID.UPNP.ORG: 17:这两个字段可能与设备的启动序列相关,用于标识设备的启动次数或启动状态。
X-RINCON-BOOTSEQ: 17
BOOTID.UPNP.ORG: 17
// 表示设备的 Wi-Fi 模式,0 可能代表某种特定的模式,具体含义需要参考 Sonos 设备的文档。
X-RINCON-WIFIMODE: 0
// 可能是设备的变体类型或配置信息,具体含义需参考 Sonos 设备的说明。
X-RINCON-VARIANT: 2
// 可能是与家庭智能音箱音频相关的标识,用于区分不同家庭网络中的音频设备或服务。
HOUSEHOLD.SMARTSPEAKER.AUDIO: Sonos_1r5qlk84JKZVqkesjdjfpBTsXi.f_YIlWyEmm0Mh__FDCML
// 可能是智能音箱音频位置的标识,用于定位音频设备在家庭网络中的位置。
LOCATION.SMARTSPEAKER.AUDIO: lc_18be903b5f6449b9996dad7b79a135e4
// 下面这两个字段提供了设备描述文件的安全 URL 地址,客户端可以通过 HTTPS 协议访问这些地址来获取设备的安全描述信息,端口号分别为 1443 和 1843。
SECURELOCATION.UPNP.ORG: https://192.168.11.93:1443/xml/device_description.xml
X-SONOS-HHSECURELOCATION: https://192.168.11.93:1843/xml/device_description.xml
2, UPnP(Universal Plug and Play)即通用即插即用协议,是一种用于在局域网中自动发现和配置设备的网络协议。旨在实现设备在网络中的自动发现、配置与交互,让设备间的通信和协作变得更加便捷。它基于 TCP/IP 协议和 HTTP 协议,广泛应用于智能家居、媒体设备等领域。
UPnP协议栈分层如下:
层级 协议/标准 作用
发现层 SSDP(基于 UDP) 设备服务发现(用户之前的 SSDP 实现)
描述层 HTTP + XML 设备/服务描述文档获取
控制层 SOAP(基于 HTTP) 服务动作调用
事件层 GENA(HTTP 订阅/通知) 事件订阅与通知
展示层 HTML(可选) 设备管理页面
工作原理如下描述。
发现阶段:
通过 SSDP 广播设备存在,即借助 SSDP(Simple Service Discovery Protocol),设备能够在网络中广播自身的存在,客户端也可通过广播消息搜索特定类型的设备。
广播搜索,即当一个设备接入网络或者一个应用程序需要查找特定类型的设备时,它会在局域网上发送一个广播消息,该消息使用特定的 UPnP 搜索格式,包含了要搜索的设备类型等信息。例如,一个智能音箱应用程序可能会发送搜索 “urn:schemas-upnp - org:device:ZonePlayer:1” 类型设备的消息,这是用于发现 Sonos 设备的特定搜索字符串。
设备响应,即网络中的 UPnP 设备会监听这些广播消息。当设备接收到与自己类型匹配的搜索消息时,会向发送者回复一个包含自身详细信息的消息,这些信息包括设备的位置(URL)、设备类型、唯一标识符(UUID)等。就像前面提到的设备返回数据中,包含了 “LOCATION”“ST”“USN” 等字段,分别表示设备描述文件的位置、设备类型和唯一服务名称。
描述阶段:
客户端获取设备的 XML 描述文件。即设备利用 XML 文件来描述自身的功能、服务及接口等信息,客户端可通过 HTTP 请求获取这些描述文件。
获取描述信息,即设备发现阶段后,请求方根据设备返回的位置信息(LOCATION 字段),通过 HTTP 协议获取设备的描述文件(通常是 XML 格式)。这个描述文件包含了设备的详细信息,如设备的功能、支持的服务、服务的操作方法以及相关的参数等。
解析描述文件,即请求方解析 XML 描述文件,了解设备提供的各种服务和功能,以便后续与设备进行交互。例如,一个智能灯泡的描述文件可能会说明它支持开关灯、调节亮度等服务,以及这些服务对应的操作接口和参数。
控制阶段:
即服务调用,通过 SOAP 调用服务动作。客户端依据设备描述文件中的信息,采用 SOAP(Simple Object Access Protocol)消息调用设备的服务。
服务请求,即应用程序根据设备描述文件中提供的信息,通过 SOAP(Simple Object Access Protocol)消息来调用设备的服务。SOAP 消息通常是通过 HTTP 协议发送到设备指定的服务端点。例如,要打开智能灯泡,应用程序会构造一个 SOAP 消息,包含 “urn:schemas - upnp - org:service:SwitchPower:1#SetTarget” 这样的操作信息,以及要设置的目标状态(打开或关闭)作为参数,发送到灯泡设备的相应服务端点。
服务执行与响应,即设备接收到 SOAP 消息后,解析并执行相应的服务操作,然后返回一个包含操作结果的 SOAP 响应消息。如果打开灯泡操作成功,设备会返回一个表示成功的响应消息,应用程序可以根据这个响应来更新界面或进行后续处理。
事件阶段:
即事件通知,订阅状态变化通知。客户端能够订阅设备的事件,当设备状态改变时,会向订阅者发送事件通知。
事件订阅,即应用程序可以订阅设备的某些事件,以便在设备状态发生变化时及时收到通知。例如,订阅智能灯泡的开关状态变化事件。
事件通知,即当设备的状态发生变化时,它会向订阅者发送事件通知消息,通过这种方式,应用程序能够实时了解设备的状态变化,而无需频繁地查询设备状态。例如,当智能灯泡被手动关闭时,设备会向订阅了开关状态变化事件的应用程序发送通知,应用程序可以立即更新界面显示灯泡已关闭。
UPnP协议的优缺点:
优点:
自动配置:设备接入网络后可自动被发现和配置,无需人工干预。
跨平台兼容:支持多种操作系统和设备类型,方便不同设备间的互联互通。
开放性:是开放的标准协议,便于开发者进行扩展和集成。
缺点:
安全性问题:默认情况下,UPnP 缺乏足够的安全机制,容易遭受攻击。
网络开销:设备发现和消息广播会产生一定的网络流量,在大型网络中可能影响性能。
3, SSDP和UPnP协议之间的联系与区别
3.1 SSDP(Simple Service Discovery Protocol,简单服务发现协议)是UPnP(Universal Plug and Play,通用即插即用)协议栈的核心组成部分,属于UPnP架构中的设备发现层。UPnP通过SSDP实现设备的自动发现功能,为后续设备描述、控制、事件通知等上层协议提供基础。也就是说,SSDP 是 UPnP 的组成部分。UPnP 是一个用于设备发现、配置和控制的体系架构,而 SSDP 是 UPnP 中用于设备发现和服务通告的核心协议。SSDP 为 UPnP 设备提供了一种在本地网络中相互发现和识别的机制。
3.2 基于相同的网络协议:两者都基于互联网协议(IP)和用户数据报协议(UDP),利用这些底层协议来实现设备之间的通信和信息交互。
3.3,协同工作实现设备功能:在 UPnP 网络中,设备使用 SSDP 来发布自己的存在和所提供的服务信息,其他设备则通过 SSDP 来搜索和发现感兴趣的设备及服务。一旦设备通过 SSDP 相互发现,它们就可以使用 UPnP 的其他协议和机制来进行进一步的交互,如设备控制、状态查询等,以实现各种功能。
也即是,对于设备发现,SSDP基于UDP多播机制,允许设备在局域网内广播自身服务信息,或控制点主动搜索特定设备。
对于设备描述与控制,UPnP通过HTTP协议获取设备描述文档(XML格式),并使用SOAP协议进行设备控制。
对于事件通知,UPnP通过GENA协议实现设备状态变化的事件订阅与通知。
3.4,从应用场景而言,两者共同服务于智能家居、物联网等场景,例如智能音箱通过SSDP发现网络中的媒体服务器,再通过UPnP协议实现媒体播放控制。SSDP 的应用场景主要集中在设备发现阶段,常用于家庭网络、企业局域网等环境中,以便设备能够快速找到彼此并建立连接。UPnP 则适用于更广泛的场景,包括家庭自动化、多媒体播放、网络设备配置等领域,它允许各种不同类型的设备相互协作和交互,实现更复杂的功能。
3.5, 从功能范围而言 SSDP仅负责设备发现,包括设备上线通知(ssdp:alive)与搜索响应(ssdp:discover)。而UPnP 涵盖设备从发现到控制的完整生命周期,包括设备描述(XML)、控制(SOAP)、事件(GENA)等。SSDP:不提供事件机制。
UPnP:通过GENA协议实现事件订阅,设备状态变化时主动推送通知。SSDP:不参与设备控制。
UPnP:通过SOAP协议发送控制指令(如SetVolume),设备返回执行结果。
3, 使用SOAP协议进行设备控制或数据获取
suspend fun getPlaybackProgress(ipAddress: String): String? {
Log.v(TAG, "getPlaybackProgress...ip:${ipAddress}")
// URL 表示要向该 IP 地址的设备的 1400 端口发送请求,
// 请求的路径是 /MediaRenderer/AVTransport/Control,
// 这通常是 UPnP 设备中用于控制媒体传输(如音频、视频播放控制)相关服务的地址。
val url = "http://$ipAddress:1400/MediaRenderer/AVTransport/Control"
// soapAction 定义了要执行的 SOAP 操作
// 在 UPnP 协议中,SOAP(简单对象访问协议)用于设备间的通信。
// 这个字符串是一个统一资源名称(URN),它标识了要执行的具体操作,即获取 AVTransport 服务(版本 1)的位置信息(GetPositionInfo)。
// 外层的双引号(")是 Kotlin 中字符串的界定符,由于字符串内部也包含双引号,所以内部的双引号需要进行转义(\"),这里为了避免转义,直接将整个字符串用双引号包裹,使得字符串内部的双引号可以正常显示。
val soapAction = "\"urn:schemas-upnp-org:service:AVTransport:1#GetPositionInfo\""
// soapRequest 使用三重引号字符串构建了符合 SOAP 协议的请求体,
// 包含了操作的具体参数(如 InstanceID 和 Channel)。
// <?xml version="1.0" encoding="utf-8"?>:这是 XML 声明,指定了 XML 的版本为 1.0,编码为 utf-8。
// <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">:定义了 SOAP 信封(Envelope)元素,xmlns:s 定义了命名空间,s:encodingStyle 指定了编码风格。
// <s:Body>:表示 SOAP 消息的主体部分,实际的操作请求和数据都包含在这个部分。
// <u:GetPositionInfo xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">:定义了要执行的具体操作 GetPositionInfo,xmlns:u 定义了操作所属的命名空间。
// <InstanceID>0</InstanceID> 和 <Channel>Master</Channel>:这是操作的具体参数,InstanceID 设置为 0,Channel 设置为 Master,用于指定获取哪个实例和通道的位置信息。
// </s:Envelope>:结束 SOAP 信封元素。
val soapRequest = """
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetPositionInfo xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<InstanceID>0</InstanceID>
<Channel>Master</Channel>
</u:GetPositionInfo>
</s:Body>
</s:Envelope>
"""
.trimIndent() // trimIndent():这是一个 Kotlin 字符串的扩展函数,用于去除多行字符串中每行的前导缩进,确保生成的 XML 字符串格式正确,去除因代码缩进导致的多余空格。
return try {
// 通过 URL 打开连接并转换为 HttpURLConnection
// URL(url):根据之前定义的 url 字符串创建一个 URL 对象,url 是目标服务器的地址,包含设备的 IP、端口和服务路径。
val connection = URL(url)
// 调用 URL 对象的 openConnection 方法,返回一个通用的 URLConnection 对象,用于与指定的 URL 建立连接。
.openConnection()
// 将返回的 URLConnection 对象强制转换为 HttpURLConnection,以便使用 HTTP 特定的功能和方法。
as HttpURLConnection
// 设置请求方法为 POST
// HttpURLConnection 的 requestMethod 属性用于设置 HTTP 请求方法,这里设置为 POST,表示要向服务器提交数据。
connection.requestMethod = "POST"
// 配置请求头(Content-Type 和 SOAPAction),表示请求体是 XML 格式且指定了 SOAP 动作。
// 设置请求头的 Content-Type 属性,表明请求体的数据格式是 XML,编码为 UTF - 8。
connection.setRequestProperty("Content-Type", "text/xml; charset=utf-8")
// 设置请求头的 SOAPAction 属性,值为之前定义的 soapAction,它指定了要执行的 SOAP 操作。
connection.setRequestProperty("SOAPAction", soapAction)
// 将 doOutput 设置为 true 以允许向连接中写入数据
// 将 HttpURLConnection 的 doOutput 属性设置为 true,表示该连接将用于向服务器发送数据(输出)。
// 如果不设置为 true,尝试写入数据时会抛出异常。
connection.doOutput = true
// 通过 connection.outputStream 获取一个 OutputStream 对象,用于向服务器写入数据。
val outputStream: OutputStream = connection.outputStream
// 然后将构建好的 SOAP 请求体写入输出流并关闭。
// 将之前构建的 SOAP 请求体(soapRequest)转换为字节数组,并写入到输出流中。
outputStream.write(soapRequest.toByteArray())
// 调用 flush 方法,强制将缓冲区中的数据发送到服务器,确保数据被真正传输。
outputStream.flush()
// 关闭输出流,释放资源。
outputStream.close()
val responseCode = connection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
Log.v(TAG, "getPlaybackProgress...HTTP_OK")
// 创建一个 BufferedReader 对象,用于读取服务器的响应数据。
// InputStreamReader 将 connection.inputStream(服务器响应的输入流)包装成字符流,以便 BufferedReader 能够读取。
val reader = BufferedReader(InputStreamReader(connection.inputStream))
// 创建一个 StringBuilder 对象,用于构建服务器的响应字符串。
val response = StringBuilder()
// 声明一个可空的字符串变量 line,用于存储每次读取的一行响应数据。
var line: String?
// 通过 BufferedReader 的 readLine 方法逐行读取响应数据,直到读取到 null(表示到达流的末尾)。
// also 函数用于在读取一行数据后将其赋值给 line 变量。
while (reader.readLine().also { line = it } != null) {
// 将读取到的每一行数据追加到 response 字符串构建器中。
response.append(line)
}
// 读取完成后,关闭 BufferedReader,释放资源。
reader.close()
Log.v(TAG, "getPlaybackProgress...response.toString(): ${response.toString()}")
parseRelTime(response.toString())
} else {
null
}
} catch (e: Exception) {
Log.e(TAG, "Error getting playback progress: ${e.message}")
null
}
}
private fun parseRelTime(xml: String): String? {
Log.v(TAG, "parseRelTime...xml:$xml")
try {
// 创建 XmlPullParserFactory 实例并获取一个 XmlPullParser,将传入的 XML 字符串设置为解析器的输入。
// 通过XmlPullParserFactory的newInstance静态方法创建一个XmlPullParserFactory实例。这个工厂类用于创建XmlPullParser对象。
val parserFactory = XmlPullParserFactory.newInstance()
// 使用XmlPullParserFactory实例创建一个XmlPullParser对象,该对象用于解析 XML 数据。
val parser = parserFactory.newPullParser()
// 将传入的 XML 字符串xml转换为Reader对象,并设置为XmlPullParser的输入源,以便后续解析。
parser.setInput(xml.reader())
// 获取XmlPullParser当前解析到的事件类型,并将其存储在eventType变量中。
// XmlPullParser通过不同的事件类型(如开始标签、结束标签、文本内容等)来驱动解析过程。
var eventType = parser.eventType
// 遍历 XML 文档,直到文档结束。
// while循环,只要当前解析的事件类型不是XmlPullParser.END_DOCUMENT(表示 XML 文档解析结束),就继续循环。
while (eventType != XmlPullParser.END_DOCUMENT) {
// 检查当前事件类型是否是开始标签(XmlPullParser.START_TAG),并且标签名称是否为RelTime。
if (eventType == XmlPullParser.START_TAG && parser.name == "RelTime") {
// 先调用findOne("<RelTime>([0-9]*:[0-9]*:[0-9]*)</RelTime>", xml),
// 在xml字符串中查找符合<RelTime>([0-9]*:[0-9]*:[0-9]*)</RelTime>正则表达式的内容,并返回第一个捕获组的内容(即时间戳部分)。
// 然后将这个返回的时间戳作为参数传递给formatTimestampToSeconds函数,尝试将其转换为总秒数,并将结果赋值给relTime变量。
val relTime = formatTimestampToSeconds(
findOne("<RelTime>([0-9]*:[0-9]*:[0-9]*)</RelTime>", xml)
)
Log.v(TAG, "parseRelTime...relTime:$relTime")
return relTime.toString()
}
eventType = parser.next()
}
} catch (e: Exception) {
Log.e(TAG, "Error parsing RelTime from XML: ${e.message}")
}
return null
}
private fun formatTimestampToSeconds(timestamp: String?): Int? {
timestamp?.let {
// 将传入的时间戳字符串it(it是let函数块内表示timestamp的参数)按冒号:进行分割,返回一个字符串数组parts。
val parts = it.split(":")
// if (parts.size == 3)检查分割后的数组长度是否为 3,即确保时间戳字符串格式为HH:MM:SS。
if (parts.size == 3) {
// toIntOrNull方法会尝试将字符串转换为整数,如果转换失败(例如字符串不是有效的数字),则返回null。如果任何一个转换失败,函数会立即返回null。
val hours = parts[0].toIntOrNull() ?: return null
val minutes = parts[1].toIntOrNull() ?: return null
val seconds = parts[2].toIntOrNull() ?: return null
// 如果所有转换都成功,将小时、分钟和秒转换为总秒数
return hours * 3600 + minutes * 60 + seconds
}
}
// 如果timestamp为null或者时间戳格式不正确,函数最终返回null。
return null
}
private fun findOne(pattern: String, input: String): String? {
// 使用Pattern.compile方法将传入的正则表达式pattern编译为Pattern对象。
val r = Pattern.compile(pattern)
// 使用编译后的Pattern对象创建一个Matcher对象,用于在输入字符串input中进行匹配操作。
val m = r.matcher(input)
// m.find()尝试在输入字符串中查找与正则表达式匹配的子序列。
// 如果找到匹配项,m.group(1)会返回正则表达式中第一个捕获组(即括号内的部分)匹配的内容;如果未找到匹配项,则返回null。
return if (m.find()) m.group(1) else null
}
上面的代码通过SOAP协议获取音乐播放的进度,下面整理了关于媒体播放相关的soap请求。
//音乐播放的进度
val url = "http://$ipAddress:1400/MediaRenderer/AVTransport/Control"
val soapAction = "\"urn:schemas-upnp-org:service:AVTransport:1#GetPositionInfo\""
val soapRequest = """
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetPositionInfo xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<InstanceID>0</InstanceID>
<Channel>Master</Channel>
</u:GetPositionInfo>
</s:Body>
</s:Envelope>
"""
.trimIndent()
// 获取音量
val url = "http://$ipAddress:1400/MediaRenderer/RenderingControl/Control"
val soapAction = "\"urn:schemas-upnp-org:service:RenderingControl:1#GetVolume\""
val soapRequest = """
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetVolume xmlns:u="urn:schemas-upnp-org:service:RenderingControl:1">
<InstanceID>0</InstanceID>
<Channel>Master</Channel>
</u:GetVolume>
</s:Body>
</s:Envelope>
"""
.trimIndent()
// 设置音量
val url = "http://$ipAddress:1400/MediaRenderer/RenderingControl/Control"
val soapAction = "\"urn:schemas-upnp-org:service:RenderingControl:1#SetVolume\""
val soapRequest = """
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:SetVolume xmlns:u="urn:schemas-upnp-org:service:RenderingControl:1">
<InstanceID>0</InstanceID>
<Channel>Master</Channel>
<DesiredVolume>$volume</DesiredVolume>
</u:SetVolume>
</s:Body>
</s:Envelope>
"""
.trimIndent()
//播放上一首
val url = "http://$ipAddress:1400/MediaRenderer/AVTransport/Control"
val soapAction = "\"urn:schemas-upnp-org:service:AVTransport:1#Previous\""
val soapRequest = """
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:Previous xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<InstanceID>0</InstanceID>
</u:Previous>
</s:Body>
</s:Envelope>
"""
.trimIndent()
//播放下一首
val url = "http://$ipAddress:1400/MediaRenderer/AVTransport/Control"
val soapAction = "\"urn:schemas-upnp-org:service:AVTransport:1#Next\""
val soapRequest = """
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:Next xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<InstanceID>0</InstanceID>
</u:Next>
</s:Body>
</s:Envelope>
"""
.trimIndent()
//获取播放模式
val url = "http://$ipAddress:1400/MediaRenderer/AVTransport/Control"
val soapAction = "\"urn:schemas-upnp-org:service:AVTransport:1#GetTransportSettings\""
val soapRequest = """
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetTransportSettings xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<InstanceID>0</InstanceID>
</u:GetTransportSettings>
</s:Body>
</s:Envelope>
"""
.trimIndent()
//设置播放模式
val url = "http://$ipAddress:1400/MediaRenderer/AVTransport/Control"
val soapAction = "\"urn:schemas-upnp-org:service:AVTransport:1#SetPlayMode\""
val soapRequest = """
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:SetPlayMode xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<InstanceID>0</InstanceID>
<NewPlayMode>$mode</NewPlayMode>
</u:SetPlayMode>
</s:Body>
</s:Envelope>
"""
.trimIndent()
// 获取播放状态
val url = "http://$ipAddress:1400/MediaRenderer/AVTransport/Control"
val soapAction = "\"urn:schemas-upnp-org:service:AVTransport:1#GetTransportInfo\""
val soapRequest = """
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetTransportInfo xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<InstanceID>0</InstanceID>
</u:GetTransportInfo>
</s:Body>
</s:Envelope>
"""
.trimIndent()
//暂停播放
val url = "http://$ipAddress:1400/MediaRenderer/AVTransport/Control"
val soapAction = "\"urn:schemas-upnp-org:service:AVTransport:1#Pause\""
val soapRequest = """
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:Pause xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<InstanceID>0</InstanceID>
</u:Pause>
</s:Body>
</s:Envelope>
"""
.trimIndent()
//开始播放
val url = "http://$ipAddress:1400/MediaRenderer/AVTransport/Control"
val soapAction = "\"urn:schemas-upnp-org:service:AVTransport:1#Play\""
val soapRequest = """
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:Play xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<InstanceID>0</InstanceID>
<Speed>1</Speed>
</u:Play>
</s:Body>
</s:Envelope>
"""
.trimIndent()
SOAP(Simple Object Access Protocol)即简单对象访问协议,是一种基于 XML 的协议,用于在分散或分布式的环境中交换结构化和类型化的信息。它允许应用程序通过 HTTP、SMTP 等协议在不同的平台和编程语言之间进行通信,是 Web 服务中常用的通信协议之一。SOAP是Web服务(尤其是基于WSDL的Web服务)的重要实现方式 SOAP消息是XML格式的,具有良好的可读性和扩展性。
SOAP协议结构:
SOAP 消息本质上是一个 XML 文档,主要由以下几个部分组成。
信封(Envelope):
是 SOAP 消息的根元素,它定义了消息的开始和结束,所有其他的 SOAP 元素都必须包含在这个信封中。
信封的存在使得 SOAP 消息具有了明确的结构边界,接收方可以通过解析信封来确定消息的有效性和处理方式。它是 SOAP 消息的基础,没有信封,SOAP 消息就无法构成一个完整的、可被理解和处理的单元。
头部(Header):
可选部分,包含一些与消息处理相关的元数据,例如身份验证信息、消息路由信息等。这些信息不是消息的核心业务数据,但对于消息的正确处理和传递可能是必要的。
头部的存在使得 SOAP 消息能够携带额外的控制和管理信息,增强了消息的灵活性和扩展性。通过在头部添加不同的元素,发送方可以传达各种与消息处理相关的指令和上下文信息,接收方可以根据这些信息进行相应的处理,如验证身份、跟踪事务等。
主体(Body):
必选部分,包含了实际要传输的数据和请求 / 响应的操作内容。无论是客户端发送的请求消息,还是服务端返回的响应消息,核心的业务数据和操作指令都在主体中。
主体是 SOAP 消息的核心部分,它承载了消息的主要目的和内容。发送方通过在主体中构造合适的操作和参数来请求服务端执行特定的任务,接收方通过解析主体来理解请求并返回相应的结果。主体的正确构造和解析是实现 SOAP 消息有效通信的关键。
错误信息(Fault):
可选部分,当 SOAP 消息处理过程中出现错误时,会在Body中包含一个Fault元素来描述错误信息。使用 soap:Fault 结构返回标准错误信息。
Fault元素的存在使得 SOAP 消息在处理失败时能够向发送方传达清晰的错误信息。发送方可以根据这些错误信息进行相应的处理,如重试请求、提示用户等。它提高了 SOAP 协议的可靠性和可维护性,使得通信双方能够更好地处理异常情况。
SOAP协议工作流程。
客户端构建 SOAP 请求:客户端根据要调用的服务和操作,构建一个符合 SOAP 协议的 XML 请求消息。
客户端发送请求:客户端通过 HTTP 等协议将 SOAP 请求消息发送到服务端。
服务端接收并处理请求:服务端接收到请求后,解析 SOAP 消息,执行相应的操作,并生成 SOAP 响应消息。
服务端发送响应:服务端将生成的 SOAP 响应消息通过 HTTP 等协议返回给客户端。
客户端接收并解析响应:客户端接收到响应后,解析 SOAP 消息,获取所需的数据。