用 mDNS 和 DNS-SD 发现局域网服务
Last modified:
打开打印机列表,电脑就能列出附近的打印机;设备换了 IP,列表往往仍能找到它。应用通过服务发现取得打印机当前的地址,用户无需提前填写固定 IP。
mDNS 和 DNS-SD 配合完成服务发现:mDNS 让同一链路上的设备直接交换 DNS 查询和响应,DNS-SD 规定怎样用 DNS 记录列出服务,并取得主机、端口和属性。本文用一个 Python HTTP 服务演示发布、发现和连接,再与 LocalSend 的自定义多播方案对照。
从多播理解 mDNS
普通 DNS 查询通常发给配置好的 DNS 服务器。mDNS(Multicast DNS,多播 DNS)把查询发到一个约定的多播组,由知道答案的设备响应。
多播(Multicast,也叫组播)表示把一个数据包发给一组接收者。单播指定一个接收者;广播面向同一广播域中的设备;多播则使用组地址,接收程序在对应网卡上加入这个组。组地址不属于某一台机器,也不需要配置成机器自己的 IP。
交换机是否只向特定端口转发多播流量,取决于 IGMP snooping(监听组成员关系)等网络配置。加入多播组不提供加密或访问控制。
mDNS 使用 UDP 5353,IPv4 组地址是 224.0.0.251,IPv6 是 ff02::fb。.local. 是 mDNS 使用的特殊域;这些查询在本地链路上完成。相关规定见 RFC 6762 的介绍与名称解析规则。
假设局域网里有一台设备使用 printer.local.:
| |
响应通常也是多播,查询者也可以请求单播响应。RFC 6762 查询规则
设备会缓存记录、探测名称冲突,并主动公告记录。正常离开时,可以发送 TTL 为 0 的 goodbye 报文,让其他设备删除缓存。冲突探测、goodbye 报文
DNS 记录的 TTL(Time to Live)表示缓存寿命,与 IP 数据包中用于限制转发的 TTL 是两个字段。设备异常断电时,无法发送 goodbye,客户端仍可能短暂保留旧记录。
DNS-SD 怎样发现服务
DNS-SD 的 SD 是 Service Discovery,即服务发现;完整名称是 DNS-Based Service Discovery,意为基于 DNS 的服务发现。
查询 printer.local. 的 IP,前提是已经知道这台打印机的名字。服务发现处理的是更早一步的问题:客户端只知道自己需要哪类服务,例如打印服务,要找出网络里有哪些实例,再取得每个实例的连接信息。
DNS-SD 约定了服务名称和 DNS 记录的组织方式。服务端按约定发布记录,客户端按同一约定查询,就能从服务类型找到实例,再从实例找到主机、端口和属性。它复用已有 DNS 记录,无需新增报文格式或记录类型。RFC 6763 介绍
mDNS 负责把查询和响应送到本地链路上的设备;DNS-SD 负责规定查询什么名称、怎样从回答中取得服务信息。一个程序即使能用 mDNS 解析 .local 主机名,也需要发布 DNS-SD 服务记录,才能出现在按服务类型查询的列表里。
以本文的 HTTP 示例为例:
| 记录 | 查询名称 | 回答的含义 |
|---|---|---|
| PTR | _mdnsdemo._tcp.local. | 有一个实例叫 Demo._mdnsdemo._tcp.local. |
| SRV | Demo._mdnsdemo._tcp.local. | 服务位于 mdnsdemo-c0a802e2.local.,端口为 8000 |
| TXT | Demo._mdnsdemo._tcp.local. | 属性为 path=/hello、version=1 |
| A | mdnsdemo-c0a802e2.local. | 主机 IPv4 地址为 192.168.2.226 |
PTR 用于枚举实例,SRV 给出目标主机和端口,TXT 保存服务约定的属性,A/AAAA 解析目标主机地址。DNS-SD 的记录结构见 RFC 6763。
_mdnsdemo._tcp.local. 中,_mdnsdemo 是服务名称,_tcp 表示业务连接使用 TCP,local. 是发现域。发现报文本身仍通过 mDNS 的 UDP 5353 发送。实例名称 Demo 用于区分同类服务,和 SRV 中的目标主机名是两个概念。
本文的 mdnsdemo 名称只用于实验。公开协议应按 RFC 6763 的服务名称规则选择已有名称或申请登记,避免不同应用碰巧使用相同类型。
以客户端寻找本文的 HTTP 示例为例,发现过程按下面的关系展开:
- 客户端预先知道服务类型
_mdnsdemo._tcp,查询_mdnsdemo._tcp.local.的 PTR 记录,取得实例列表,例如Demo._mdnsdemo._tcp.local.。 - 客户端查询选中实例的 SRV 和 TXT 记录,得到目标主机
mdnsdemo-c0a802e2.local.、端口8000和路径/hello。 - 客户端查询目标主机的 A 记录,取得
192.168.2.226,再通过 HTTP 请求http://192.168.2.226:8000/hello。
客户端与服务端需要使用相同的服务类型,但客户端无需预先知道实例名称、IP 或端口。上面列的是记录之间的依赖关系;响应可以同时携带多种记录,减少后续查询。发现完成后,HTTP 请求直接发给服务端。
DNS-SD 也能运行在普通单播 DNS 上,由 DNS 服务器保存服务记录。跨网段的服务目录可以使用这种方式。RFC 6763 介绍
客户端仍需要知道查询什么
服务发现需要一个预先约定的查询入口。应用通常知道自己支持的服务类型:打印客户端查找 _ipp._tcp,本文示例查找 _mdnsdemo._tcp。这个类型可以写在程序中,或由配置提供;具体设备的名称、IP 和端口则从发现结果中取得。
前面的 printer.local. 示例演示的是已知主机名时的名称解析。发现打印机时,客户端先查询 _ipp._tcp.local. 的 PTR 记录,得到打印服务实例列表。用户选中某个实例后,客户端查询它的 SRV 记录,才知道目标主机是 printer.local.、端口是 631,随后再解析主机地址。printer.local. 无需写死在客户端中。
DNS-SD 还提供了枚举服务类型的公共入口。客户端查询 _services._dns-sd._udp.local. 的 PTR 记录,可以取得该域内公告的服务类型,例如 _ipp._tcp.local. 或 _http._tcp.local.,再分别查询这些类型的实例。此时预先约定的是枚举入口,无需先列出每一种具体服务类型。RFC 6763 服务类型枚举
枚举类型适合网络浏览和诊断工具。发现一种类型,只能告诉客户端这种服务存在;要使用它,客户端仍需理解对应的业务协议和属性含义。例如找到 IPP 打印服务后,还需要支持 IPP 的客户端发送打印请求。DNS-SD 解决的是服务位置的发现,业务协议由应用实现。
一个能发布和发现服务的程序
示例用 Python 标准库启动 HTTP 服务,用 python-zeroconf处理 mDNS 报文和 DNS-SD 记录。依赖固定为本次验证的 zeroconf==0.151.5,DNS 编码、缓存和冲突探测由库处理。
下载这两个文件,放进同一个目录:
- discovery.py:发布或发现服务。
- check.py:启动两个独立进程,发现服务后请求
/hello并检查响应。
需要 Python 3.10+ 和 uv。下面的命令在文件所在目录执行。uv run --with 会准备依赖环境,无需修改系统 Python。
先确认网卡地址
服务端和客户端都要传入自己连接局域网的 IPv4 地址。程序用这个地址选择 mDNS 网卡,服务端还用它绑定 HTTP 监听地址、发布 A 记录。
macOS 可以先查看硬件端口和接口,再查对应接口的地址:
| |
en0 只是例子,Wi-Fi 或有线接口不一定叫这个名字。Linux 可以使用 ip -4 addr,Windows 可以使用 ipconfig。选择局域网网卡的地址,避免误用 VPN 或 Docker 网桥地址。
发布 HTTP 服务
在设备 A 上运行,替换为 A 的实际地址:
| |
终端出现 Published ... 后,服务保持运行。HTTP 仅监听指定 IP 的 8000 端口,GET /hello 返回固定文本,其他路径返回 404。
发布记录的核心代码是:
| |
SERVICE_TYPE 是 _mdnsdemo._tcp.local.。库根据 ServiceInfo 中的数据生成 PTR、SRV、TXT 和 A 记录,再通过 register_service 注册并公告服务。
allow_name_change=True 允许库在实例重名时调整名称,终端打印的是实际注册后的名称。
按 Ctrl+C 停止服务时,程序调用 unregister_service 撤销公告,再关闭 HTTP 和 mDNS socket。
发现服务并连接
在同一局域网的设备 B 上运行,--ip 填 B 自己的地址:
| |
程序浏览 _mdnsdemo._tcp.local.,发现首个能够解析的实例后输出 JSON 并退出。它不会自动访问收到的地址。没有发现服务时,超时退出并返回非零状态。
核心调用是:
| |
ServiceBrowser 监听服务变化,回调把实例名称放进队列。主线程再调用 get_service_info,取得实例的主机、地址、端口和属性。完整的超时等待与清理代码在 discovery.py 中。
确认输出对应自己的设备后,使用发现结果中的地址和端口连接:
| |
预期输出:
| |
这里直接连接解析出的 IP,因此不依赖操作系统是否支持 .local 名称解析。mDNS 的发现端口是 UDP 5353,HTTP 的业务端口是 TCP 8000,两者可以不同。
运行结果
同机检查程序
2026-10-07,在一台 macOS 主机上使用局域网网卡地址 192.168.2.226,运行:
| |
检查程序让服务端使用 --port 0,由操作系统分配空闲 HTTP 端口。发现进程等到服务记录后,再请求该端口的 /hello。实际输出如下,实例后缀和端口每次可能不同:
| |
检查通过表明,发现进程取得了服务端发布的地址、端口和 TXT 属性,并成功请求 /hello。服务进程在检查结束后已停止。另外检查了无服务时的超时退出,以及多播地址、非正超时参数的拒绝行为。
同机检查用于验证程序的发布、解析和连接行为。设备间的多播传播还需要双机验证。
macOS 与 m6 双机验证
2026-10-08,继续使用两台设备验证:macOS 的局域网地址为 192.168.2.226,Ubuntu 主机 m6 的地址为 192.168.2.100。两台机器都使用 zeroconf==0.151.5,HTTP 端口由 --port 0 自动分配。
先在 m6 发布 M6-Demo,再在 macOS 执行发现命令。本机成功输出:
| |
随后在 macOS 请求解析出的地址:
| |
实际返回 Hello from DNS-SD demo!。
反向验证时,先在 m6 运行发现命令,再启动 macOS 服务。m6 收到服务启动时的公告,成功输出:
| |
m6 随后请求 http://192.168.2.226:56179/hello,也返回了预期文本。
双机实验验证了服务信息在设备之间的传播,以及根据发现结果建立 HTTP 连接的过程。结束后已停止示例进程,并删除 m6 临时目录中的脚本。
LocalSend 使用的是哪种方案
LocalSend 同样利用多播发现设备,但使用自己的 JSON 消息格式。官方 v2.2 协议定义的默认组地址是 224.0.0.167,UDP 端口是 53317。
启动时,设备多播公告名称、类型、服务端口等信息。其他设备优先通过 HTTP/HTTPS 的 /api/localsend/v2/register 接口回复,也可以用多播回复作为备用方式。文件传输随后使用 HTTP/HTTPS。
| 项目 | 本文示例 | LocalSend v2.2 |
|---|---|---|
| 发现消息 | DNS 报文中的服务记录 | 自定义 JSON |
| 默认多播地址 | 224.0.0.251 | 224.0.0.167 |
| 默认发现端口 | UDP 5353 | UDP 53317 |
| 服务连接信息 | SRV、TXT、A 记录 | JSON 字段 |
| 发现后的业务通信 | HTTP | HTTP/HTTPS |
LocalSend 官方解释,选用 224.0.0.0/24 是为了兼容部分拒绝其他多播组的 Android 设备。这是具体产品的兼容性选择,不能据此随意占用该地址段。LocalSend 默认配置
自己设计多播协议时,应检查地址分配。IPv4 的 239.0.0.0/8 是管理域多播空间,239.255.0.0/16 定义为本地范围;仍要避开已有用途并在网络内协调地址。地址的传播范围需要通过网络配置限制,仅选用管理域地址不会自动阻止跨网段转发。RFC 2365、IANA 多播地址表
如果应用只需要发布名称、端口和少量属性,mDNS + DNS-SD 已经提供了对应机制。自定义多播协议还要自己约定消息格式、重试、去重和设备离线处理。
发现不到服务时查什么
先在服务端直接请求 /hello。如果 HTTP 本身不可用,检查发布进程、绑定 IP 和业务端口;本机 HTTP 可用但另一台机器连接失败,再检查 TCP 端口、防火墙和设备间隔离。
HTTP 可以直连,但服务发现失败时,重点检查 UDP 5353、选用的网卡以及网络对多播的处理。两台设备连着同一个 Wi-Fi 名称,也可能因为访客网络或客户端隔离而不能互通。
mDNS 默认局限于本地链路。跨 VLAN 发现需要专门的 mDNS 网关或反射器;这类设备只解决发现报文的传播,发现后的业务连接仍需要路由和防火墙放行。也可以使用单播 DNS-SD,让服务目录由 DNS 服务器提供。
发现结果不能证明服务身份。示例的发现命令只展示信息,HTTP 也没有认证或加密;实际应用仍需在连接阶段验证服务身份、控制权限。TXT 记录适合放路径、版本等公开属性,不应放密码或访问令牌。RFC 6763 安全考虑