OS X 和 iOS 为 Bonjour 服务应用程序提供了多层应用程序编程接口 (API): Foundation 框架中的 NSNetService 和 NSNetServiceBrowser 类; CFNetServices,Core Services 中 CFNetwork 框架的一部分; Java 的 DNS 服务发现(仅限 OS X);以及围绕 BSD 套接字构建的低级 DNS 服务发现 API。所有三个 API 集都为网络服务的发布、发现和解析提供便利。图 3-1 说明了 API 层的结构。如您所见,多播 DNS 响应程序(或其他 DNS 服务器)位于最低级别,因此您的软件不必直接与 DNS 交互。
NSNetService 和 NSNetServiceBrowser 类是 Cocoa 中基础框架的一部分,为服务发现和发布提供了面向对象的抽象。 NSNetService 对象表示 Bonjour 服务的实例,用于发布或由客户端发现的服务,而 NSNetServiceBrowser 表示用于特定类型服务的浏览器。大多数 Cocoa 程序员应该会发现这些类足以满足他们的需要。如果您需要更详细的控制,您可以使用来自 Cocoa 应用程序的 DNS 服务发现 API。
NSNetService 和 NSNetServiceBrowser 被调度在默认的 NSRunLoop 对象上,以异步方式执行发布、发现和解析。 NSNetService 和 NSNetServiceBrowser 对象返回的所有结果都由委托对象处理。这些对象必须与一个运行循环相关联才能运行,但它不必是默认的。
Core Services 框架中声明的 CFNetServices API 提供了 Core Foundation 风格的类型和函数,用于管理服务和服务发现。 CFNetServices 定义了三种 Core Foundation 对象类型,CFNetService、CFNetServiceBrowser 和 CFNetServiceMonitor。 CFNetService 是服务实例的抽象表示,可用于发布或使用。关联函数为发布和解析服务提供支持。 CFNetServiceBrowser 表示特定域中特定类型服务的浏览器。通常只有在 OS X 或 iOS 的核心基础层编写代码时才应使用此 API。
CFNetService 和 CFNetServiceBrowser 对象通常在 CFRunLoops 中提供服务。为检索结果,应用程序实施回调函数来处理事件,例如新服务出现或消失、正在解析的实例以及发生的错误。与 NSNetService 和 NSNetServiceBrowser 不同,CFNetServices 类型不需要运行循环,并且可以在需要此行为时同步运行。但是,使用这些函数的同步模式是不好的做法。
在 /usr/include/dns_sd.h 中声明的 DNS 服务发现 API 为 Bonjour 服务提供低级 BSD 套接字通信。 DNS 服务发现充当您的软件和多播 DNS 响应程序或 DNS 服务器之间的中间层。它为您管理多播 DNS 响应程序,让您根据服务和服务浏览器而不是 DNS 资源记录来编写程序。
因为 DNS 服务发现 API 是 Darwin 开源项目的一部分,所以您应该在编写跨平台代码(适用于 iOS 和 OS X 以外的平台)或需要使用更高版本中不可用的低级功能时使用它级 API,例如 NSNetService。
如果为 Windows、Linux 或 FreeBSD 开发 Bonjour 服务应用程序,DNS 服务发现也是应该使用的 API。
## Bonjour Operations
本章描述了作为三个网络服务 API 层和 API 层本身基础的服务发布、浏览和解析的 Bonjour 操作。如果你想编写一个发布或发现网络服务的应用程序或工具,你应该阅读本章。
Bonjour 中的网络服务架构包括一个易于使用的机制,用于发布、发现和使用基于 IP 的服务。 Bonjour 支持三个基本操作,每个操作都是零配置网络服务的必要部分:
## 出版物
要发布服务,应用程序或设备必须通过高级 API 或直接与响应器 (mDNSResponder) 通信,向多播 DNS 响应器注册该服务。 Bonjour 还支持使用动态 DNS 更新在传统 DNS 服务器上存储记录。
注册服务时,会创建三个相关的 DNS 记录:服务 (SRV) 记录、指针 (PTR) 记录和文本 (TXT) 记录。 TXT 记录包含解析或使用服务所需的附加数据,尽管它通常也是空的。
SRV 记录将服务实例的名称映射到客户端实际使用该服务所需的信息。然后,客户端将服务实例名称存储为访问服务的持久方式,并在需要连接时对主机名和端口号执行 DNS 查询。这种额外的间接级别提供了两个重要的特性。首先,该服务由人类可读的名称而不是域名和端口号来标识。其次,即使服务的端口号、IP 地址或主机名发生变化,只要服务名称保持不变,客户端也可以访问该服务。
SRV 记录包含两条信息来标识服务:
主机名是当前可以找到该服务的域名。给出主机名而不是单个 IP 地址的原因是它可能是具有多个 IP 地址的多宿主主机,或者它可能具有 IPv6 地址和 IPv4 地址,等等。通过名称识别主机可以优雅地处理所有这些情况。
端口号标识服务的 UDP 或 TCP 端口。
SRV 记录根据以下约定命名:
<Instance Name>.<Service Type>.<Domain>
<实例名称>.<服务类型>.<域>
,服务实例的名称,可以是任何 UTF-8 编码的 Unicode 字符串,并且旨在供人类阅读。
是一个标准的 IP 协议名称,前面有一个下划线,后面是主机到主机的传输协议(TCP 或 UDP),前面也有一个下划线。例如,在 UDP 上运行的普通 FTP 服务将具有 _tftp._udp 服务类型,而在 TCP 上运行的 IPP 打印服务将在 _ipp._tcp 服务类型下注册。官方协议名称列表由 IANA 维护;有关详细信息,请参阅域命名约定。
是一个标准的 DNS 域。这可能是特定域,例如 apple.com.,或通用后缀 local。对于只能在本地链接上访问的服务。
以下是在 TCP 端口 515 上运行的名为 PrintsAlot 的后台打印程序的 SRV 记录示例(采用标准 DNS 记录格式):PrintsAlot._printer._tcp.local。 120 IN SRV 0 0 515 blackhawk.local。
该记录将在名为 blackhawk.local 的打印机的多播 DNS 响应器上创建。在本地链接上。初始 120 表示用于缓存的生存时间 (TTL) 值。两个零是权重和优先级值,在传统 DNS 中用于在与给定名称匹配的多个记录之间进行选择;对于多播 DNS,这些值将被忽略。
PTR 记录通过将服务类型映射到该类型服务的特定实例的名称列表来启用服务发现。此记录添加了另一层间接寻址,因此只需查找标有服务类型的 PTR 记录即可找到服务。
该记录仅包含一条信息,即服务实例的名称(与 SRV 记录的名称相同)。 PTR 记录相应地命名为 SRV 记录,但没有实例名称:<Service Type>.<Domain>
以下是名为 PrintsAlot 的后台打印程序的 PTR 记录示例_printer._tcp.local。 28800 PTR PrintsAlot._printer._tcp.local。
同样,28800 是生存时间 (TTL) 值,以秒为单位。
TXT 记录与对应的 SRV 记录同名,可以包含少量关于服务实例的附加信息,通常最多不超过 100-200 字节。此记录也可能为空。例如,网络游戏可以通告多人游戏中使用的地图名称,聊天程序可以通告用户的可用性(例如,闲置、离开或可用)。如果需要传输的数据量较大,主机应与客户端建立连接,直接发送数据。
从历史上看,此记录已用于在同一 IP 地址的同一端口上运行的多个服务,例如在同一打印服务器上运行的多个打印队列。在这种情况下,TXT 记录中的附加信息可用于识别预期的打印队列,如本例所示:
这种做法是必要的,因为服务类型历来与众所周知的端口相关联。鼓励新的 Bonjour 协议的设计者在不同的动态分配的端口号上运行他们服务的每个实例,而不是试图在同一个众所周知的端口号上运行它们并使用额外的信息来指定客户端正在尝试交谈的实例到。
TXT 记录中数据的性质和格式是特定于每种服务类型的,因此每种新服务类型还需要为其关联的 TXT 记录(如果有)定义数据格式,并将此格式发布为协议规范。
举一个具体的例子,假设有一个通过本地网络共享音乐的设备——支持 IP 的自动点唱机。假设它的传输协议是 TCP,它的应用协议名为 music。当有人将设备插入以太网集线器时,会发生许多事情,如图 4-1 所示。
在步骤 1 中,设备从 IPv4 链路本地范围 169.254.0.0 中随机选择子网掩码为 255.255.0.0 的链路本地 IP 地址 169.254.150.84,并将其公布到网络中。因为没有设备响应通知,所以设备将地址作为自己的地址。在第 2 步中,它启动自己的多播 DNS 响应程序,请求主机名 eds-musicbox.local.,验证其可用性,并将该名称作为自己的名称。接下来,在步骤 3 中,设备在 TCP 端口 1010 上启动音乐共享服务。最后,在步骤 4 中,它在本地以名称 Ed’s Party Mix 发布类型为 _music._tcp 的服务。域,首先确保不存在同名服务。这将创建两个记录:
注意:发布服务时,如果域名或服务名称不可用,没有接口的设备应选择一个新名称。遇到这种情况的应用软件应该呈现一个用户界面,通知用户该名称不可用,并允许用户选择一个不同的名称。
服务发现利用在服务发布期间注册的 DNS 记录来查找特定类型服务的所有命名实例。为此,应用程序通常通过更高级别的 API 查询与服务类型(例如 _http._tcp)匹配的 PTR 记录。在每台设备上运行的多播 DNS 响应程序返回带有服务实例名称的 PTR 记录。
图 4-2 说明了浏览音乐共享服务的客户端应用程序。在第 1 步中,客户端应用程序发出对本地 _music._tcp 类型服务的查询。域到标准多播地址 224.0.0.251。网络上的每个多播 DNS 响应程序都会听到请求,但只有音乐共享设备会使用 PTR 记录进行响应(第 2 步)。生成的 PTR 记录包含服务实例名称 Ed’s Party Mix._music._tcp.local。在这种情况下。然后,客户端应用程序可以从 PTR 记录中提取服务实例名称,并将其添加到屏幕上的音乐服务器列表中。
服务发现通常只偶尔发生一次——例如,当用户第一次选择打印机时。此操作保存服务实例名称,即任何给定服务实例的预期稳定标识符。端口号、IP 地址,甚至主机名每天都在变化,但用户不需要每次都重新选择打印机。因此,从服务名称到套接字信息的解析直到服务被实际使用时才会发生。
为了解析服务,应用程序使用服务名称执行 SRV 记录的 DNS 查找。多播 DNS 响应器使用包含当前信息的 SRV 记录进行响应。
图 4-3 说明了音乐共享示例中的服务解析。解析过程从向多播地址 224.0.0.251 请求 Ed’s Party Mix._music._tcp.local 的 DNS 查询开始。 SRV 记录(步骤 1)。在第 2 步中,此查询返回服务的主机名和端口号 (eds-musicbox.local., 1010)。在第 3 步中,客户端发出 IP 地址的多播请求。在第 4 步中,此请求解析为 IP 地址 169.254.150.84。然后客户端可以使用 IP 地址和端口号连接到服务。每次使用服务时都会发生此过程,因此总能找到服务的最新地址和端口号。