一、什么是 python-zeroconf?
python-zeroconf 是一个纯 Python 实现的 多播 DNS(mDNS)服务发现库,让应用程序能够在局域网中自动宣告和发现服务,无需任何中心化 DNS 服务器。它完整实现了 RFC 6762(mDNS 协议)和 RFC 6763(DNS-SD 服务发现规范),与 Apple 的 Bonjour 和 Linux 的 Avahi 生态完全兼容。
简单来说:你的设备和服务不再需要手动配置 IP 和端口,它们会在局域网上自动“喊话”,其他设备自动“听见”——这就是零配置网络的魔力。
v1.0 里程碑:python-zeroconf 在 2025 年正式发布了 v1.0 版本,标志着 API 的完全稳定和生产就绪。在此之前,MAJOR 版本号长期保持在 0,v1.0 的到来意味着向后兼容性的承诺(后续 MINOR 版本递增时才会出现不兼容变更)。
核心能力一览:
📡 服务注册:将你的应用宣告到局域网,让其他设备能够发现
🔍 服务发现:自动扫描和追踪网络中特定类型的服务
🏠 名称解析:在局域网中解析主机名到 IP 地址
🌍 双栈支持:同时支持 IPv4 和 IPv6
⚡ 异步原生:内置基于 asyncio 的异步接口,轻松集成现代 Python 应用
🪶 无外部依赖:仅依赖 Python 标准库 + 轻量的 ifaddr 包
🚀 可选 Cython 加速:可选安装 Cython 扩展以显著提升性能
二、系统架构总览
先上一张核心组件关系图,帮你快速建立全局认知:
架构解读:Zeroconf 是唯一入口,它内部管理着多播套接字、DNS 缓存和查询引擎。ServiceBrowser 通过 QueryScheduler 按 RFC 6762 规定的指数退避策略发送查询,收到响应后通过 ServiceListener 回调通知上层。异步层(AsyncZeroconf 等)对同步组件做了完整的 asyncio 适配。
三、服务发现全流程
理解服务发现的数据流是掌握整个库的关键。下图展示了从浏览器启动到获取完整服务信息的完整链路:
关键细节:QueryScheduler 先在 20-120ms 范围内随机延迟后发出首个 QU(单播)查询,然后追加 QM(多播)查询作补充。发现 PTR 记录后,浏览器会自动拉取 SRV(端口)、TXT(属性)和 A/AAAA(IP),最后组装为一个完整的 ServiceInfo 对象返回。此后的持续监控阶段,系统会在每条记录 TTL 的 75%、85%、95% 处安排三次抢救性刷新查询——全部失败才判定服务离线。
四、安装和基础设置
安装
最低要求:Python 3.9+,仅自动安装 ifaddr >= 0.1.7 作为依赖。
组件导入速查
# 同步 API(核心)from zeroconf import ( Zeroconf, # 主入口类 ServiceInfo, # 服务信息描述 ServiceBrowser, # 服务浏览器 ServiceListener, # 监听器基类 ServiceStateChange, # 服务状态变更枚举 ZeroconfServiceTypes, # 浏览当前网络的所有服务类型 IPVersion, # IPv4 / IPv6 选择)# 异步 API(asyncio 集成)from zeroconf.asyncio import ( AsyncZeroconf, AsyncServiceBrowser, AsyncServiceInfo,)
五、实战代码:注册一个 HTTP 服务
这是让服务“被找到”的核心。下面演示如何在局域网宣告一个运行在 8080 端口的 Web 服务:
from zeroconf import Zeroconf, ServiceInfoimport socket# ============================================================# 第一步:获取本机 IP 地址# ============================================================def get_local_ip() -> str: """获取本机局域网 IP 地址""" s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: # 不需要真的连接,只是让系统选择出站接口 s.connect(("8.8.8.8", 80)) return s.getsockname()[0] finally: s.close()# ============================================================# 第二步:构造服务信息# ============================================================local_ip = get_local_ip()service_type = "_http._tcp.local." # HTTP 服务类型service_name = f"MyWebServer.{service_type}" # 完整服务名info = ServiceInfo( type_=service_type, # 服务类型,必须是 "_<协议>._<传输>.local." 格式 name=service_name, # 完整服务实例名 addresses=[socket.inet_aton(local_ip)], # IP 地址列表(二进制格式) port=8080, # 服务端口 properties={ # TXT 记录(键值对,key 和 value 必须是 bytes) b"version": b"1.0.0", b"author": b"YourName", b"path": b"/api", }, server=f"MyHost.local.", # 服务器主机名)# ============================================================# 第三步:注册服务# ============================================================zc = Zeroconf()try: zc.register_service(info) print(f"✅ 服务已注册: {service_name} @ {local_ip}:8080") print(" 按 Ctrl+C 退出...") input() # 保持运行,等待用户中断finally: zc.unregister_service(info) zc.close() print("🛑 服务已注销")
运行效果
执行后,同一局域网内的 Bonjour/Avahi 浏览器(或你自己的发现脚本)就能看到:
MyWebServer._http._tcp.local. → 地址: 192.168.1.100 → 端口: 8080 → TXT: version=1.0.0, author=YourName, path=/api
六、实战代码:发现网络中的 HTTP 服务
有服务宣告,自然要有服务发现。以下演示如何扫描局域网内的 _http._tcp.local. 服务:
from zeroconf import Zeroconf, ServiceBrowser, ServiceListenerfrom typing import castimport socketclass MyServiceListener(ServiceListener): """ 服务发现监听器。 三个核心回调:add_service、remove_service、update_service。 """ def __init__(self): self.discovered: dict[str, dict] = {} # 用 name 索引已发现服务 def add_service(self, zc: Zeroconf, type_: str, name: str) -> None: """新服务被发现""" info = zc.get_service_info(type_, name) if info is None: return # 解析 IP 地址(二进制 → 可读字符串) addresses = [ socket.inet_ntoa(cast(bytes, addr)) for addr in info.addresses ] service_data = { "name": info.name, "type": info.type, "server": info.server, "addresses": addresses, "port": info.port, "txt": { k.decode("utf-8"): v.decode("utf-8") for k, v in (info.properties or {}).items() }, } self.discovered[name] = service_data print(f"🟢 发现服务: {name}") print(f" 地址: {addresses}") print(f" 端口: {info.port}") print(f" TXT: {service_data['txt']}") def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None: """服务离线""" self.discovered.pop(name, None) print(f"🔴 服务离线: {name}") def update_service(self, zc: Zeroconf, type_: str, name: str) -> None: """服务信息变更(端口、IP 或 TXT 更新)""" print(f"🔄 服务更新: {name}") # 重新拉取最新信息 self.add_service(zc, type_, name)# ============================================================# 启动发现# ============================================================zc = Zeroconf()listener = MyServiceListener()browser = ServiceBrowser(zc, "_http._tcp.local.", listener)print("🔍 开始扫描局域网 _http._tcp.local. 服务...")print(" 按 Enter 退出\n")try: input() # 阻塞等待finally: browser.cancel() zc.close() print("\n🛑 浏览器已关闭") print(f"共发现 {len(listener.discovered)} 个服务:") for name, data in listener.discovered.items(): print(f" · {name} → {data['addresses'][0]}:{data['port']}")
输出示例
🔍 开始扫描局域网 _http._tcp.local. 服务... 按 Enter 退出🟢 发现服务: MyWebServer._http._tcp.local. 地址: ['192.168.1.100'] 端口: 8080 TXT: {'version': '1.0.0', 'author': 'YourName', 'path': '/api'}🟢 发现服务: RaspberryPi._http._tcp.local. 地址: ['192.168.1.200'] 端口: 80 TXT: {'platform': 'raspberry-pi'}
七、扫描局域网所有服务类型
如果你不确定要找什么,可以先“扫一遍”网络上有哪些服务类型:
from zeroconf import ZeroconfServiceTypesprint("📡 正在扫描局域网中所有 mDNS 服务类型...\n")all_types = ZeroconfServiceTypes.find()for i, stype in enumerate(all_types, 1): print(f" {i:>3}. {stype}")print(f"\n共发现 {len(all_types)} 种服务类型")
典型输出:
1. _adb-tls-connect._tcp.local. 2. _airplay._tcp.local. 3. _esphomelib._tcp.local. 4. _hap._tcp.local. ← HomeKit 5. _http._tcp.local. 6. _printer._tcp.local. 7. _ssh._tcp.local. 8. _workstation._tcp.local.
八、异步 API:拥抱 asyncio 生态
对于使用 asyncio 的现代 Python 应用(如 Home Assistant、FastAPI 服务),异步 API 是更自然的选择:
import asynciofrom zeroconf import Zeroconf, ServiceStateChangefrom zeroconf.asyncio import AsyncZeroconf, AsyncServiceBrowser, AsyncServiceInfoasync def async_browser_callback( zeroconf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange,) -> None: """异步回调 —— 在事件循环中直接处理服务事件""" if state_change == ServiceStateChange.Added: print(f"🟢 [async] 发现: {name}") # 异步请求完整服务信息,不阻塞事件循环 info = AsyncServiceInfo(service_type, name) if await info.async_request(zeroconf, timeout=3000): addrs = [f"{a}" for a in info.parsed_addresses()] print(f" 地址: {addrs}, 端口: {info.port}") elif state_change == ServiceStateChange.Removed: print(f"🔴 [async] 离线: {name}")async def main(): """异步入口""" aio_zc = AsyncZeroconf() # 创建异步浏览器 browser = AsyncServiceBrowser( aio_zc.zeroconf, "_http._tcp.local.", handlers=[async_browser_callback], # 支持多个回调 ) print("🔍 [async] 开始扫描(10 秒后自动停止)...") await asyncio.sleep(10) await browser.async_cancel() await aio_zc.async_close() print("🛑 [async] 扫描结束")if __name__ == "__main__": asyncio.run(main())
异步关键点:AsyncServiceBrowser 直接接收协程回调;AsyncServiceInfo.async_request() 以非阻塞方式拉取完整服务信息;AsyncZeroconf 管理着与同步 Zeroconf 共享的引擎实例。
九、进阶:同时监控多种服务类型
ServiceBrowser 支持同时监控多种服务类型,这在构建智能家居网关或多协议发现面板时非常实用:
from zeroconf import Zeroconf, ServiceBrowser, ServiceListenerclass MultiTypeListener(ServiceListener): def add_service(self, zc, type_, name): print(f"🟢 [{type_}] 新服务: {name}") def remove_service(self, zc, type_, name): print(f"🔴 [{type_}] 离线: {name}") def update_service(self, zc, type_, name): print(f"🔄 [{type_}] 更新: {name}")zc = Zeroconf()listener = MultiTypeListener()# 同时监控 HTTP、SSH 和 ESPHome 设备types_to_monitor = [ "_http._tcp.local.", "_ssh._tcp.local.", "_esphomelib._tcp.local.", # ESPHome 固件设备的专属类型]browsers = []for stype in types_to_monitor: browser = ServiceBrowser(zc, stype, listener) browsers.append(browser)try: input("按 Enter 停止监控...\n")finally: for b in browsers: b.cancel() zc.close()
十、mDNS 协议内幕:查询调度与流量优化
python-zeroconf 严格遵循 RFC 6762 规定的流量压缩策略,在网络流量和发现速度之间进行精细平衡。
核心机制详解:
已知应答抑制:查询时附带已知的 PTR/SRV/TXT/A 记录列表,响应方跳过这些重复信息,大幅减少多播风暴
重复问题抑制:短时间内收到相同查询时,仅处理首次,避免重复触发响应
指数退避查询:启动时密集查询(1 → 2 → 4+ 秒间隔),稳定后拉长周期,降低网络负载
TTL 分级抢救:在记录到期前 75% → 85% → 95% 三个时间点分别尝试刷新,全部失败才移出缓存
TC 位处理:DNS 响应过大时设置截断位(TC),库自动适配处理多包传输的情形
缓存刷新位:记录更新时利用 cache flush 位将旧记录 TTL 置为 1 秒,保证新记录立即生效
十一、v1.0 关键变化与升级指南
python-zeroconf v1.0.0 在 2025 年正式发布,以下是值得关注的改动:
API 稳定性承诺:v1.0 标志着 API 进入成熟期,后续仅 MAJOR 版本号变更时才可能出现不兼容改动——这是从 0.x 时代“MINOR 即破坏性变更”规则的重大转折。
异步 API 完善:AsyncServiceBrowser、AsyncServiceInfo、AsyncZeroconf 自 v1.0 起成为一级支持组件,回调原生支持协程。
Cython 3.1 支持:可选安装 Cython 加速层,在保留纯 Python 灵活性的同时显著提升 DNS 解析和缓存操作的性能。
IPv6 双栈改进:v1.0 对双栈套接字(Dual-Stack)的处理更为稳健,虽然在某些 BSD 变体上仍有限制。
流量抑制增强:查询调度器进一步优化,将网络占用压缩到协议允许的理论下限。
十二、延伸资源
📂 GitHub 仓库:python-zeroconf/python-zeroconf
📘 RFC 6762 - Multicast DNS:https://datatracker.ietf.org/doc/html/rfc6762
📙 RFC 6763 - DNS-Based Service Discovery:https://datatracker.ietf.org/doc/html/rfc6763
🐍 PyPI 页面:https://pypi.org/project/zeroconf/
🧠 DeepWiki 技术文档:https://deepwiki.com/python-zeroconf/python-zeroconf
python-zeroconf 将复杂的 mDNS 协议细节封装成简洁优雅的 Python API,让你三分钟搞定局域网服务发现。无论是构建 IoT 设备仪表盘、微服务注册中心,还是个人自动化脚本,它都是不可或缺的工具。让设备会说话,让网络懂你心——从一行 pip install zeroconf 开始。 🚀
编辑:余文彬
审校:余雨馨