从一个尴尬的场景说起
设想你写了一个共享剪贴板程序——电脑上复制一段文字,手机上直接粘贴过来。你兴致勃勃地部署到服务器,配好IP、端口,把地址发到群里。
然后:
- 路由器重启了 → IP变了 → 你的程序失联了
- 换个网络环境 → 192.168.1.x 变成 10.0.0.x → 又要改配置
- 同事想用 → 你得把IP端口全告诉他 → 他配了半天连不上
你崩溃了。
能不能像微信分享那样——我不管你在哪个网段,扫一扫或者点一下就自动连上了?
这就是 Zeroconf 要解决的问题。
Zeroconf 是什么
Zeroconf(Zero Configuration Networking),中文叫"零配置网络协议"。它的核心理念就一句话:
设备接入网络后,不需要任何人手动配置IP、端口、DNS,就能自动发现和被其他设备发现。
就像你走进一个微信群,群里的人会自动看到你——不用你挨个去报户口。
在局域网内(同一个WiFi或路由器下),这个过程是自动完成的。设备上线时会在局域网内广播:
"我是一个叫 XXX 的设备,提供 YYY 服务,我在 192.168.1.XX:ZZ,有需要来找我!"
Python 里对应这个功能的库,就叫 zeroconf。
先安装
pip install zeroconf
场景一:做一个「局域网共享剪贴板」
需求: 你在公司内网部署了一个共享剪贴板服务,手机和电脑都能访问。但每次换网络环境,IP都不一样,不想每次都改配置。
第一步:发布服务(告诉全网:我在这儿)
import socket
from zeroconf import ServiceInfo, Zeroconf
# 创建 Zeroconf 实例
zeroconf = Zeroconf()
# 定义服务的元信息
# 这是服务的"自我介绍"
service_info = ServiceInfo(
"_clipboard._tcp.local.", # 服务类型:剪贴板服务
"MyClipBoard._clipboard._tcp.local.", # 服务实例名,全网唯一
addresses=[socket.inet_aton("192.168.1.100")], # 当前IP
port=8765, # 端口
properties={ # 额外属性,可以放任意信息
"version": "1.0",
"owner": "大龙虾"
},
server="myclipboard.local." # 服务的hostname
)
# 向局域网广播这个服务
zeroconf.register_service(service_info)
print("🟢 剪贴板服务已发布,其他设备可以自动发现我了!")
# 保持服务运行
input("按 Enter 停止服务...\n")
# 清理
zeroconf.unregister_service(service_info)
zeroconf.close()
运行效果:
🟢 剪贴板服务已发布,其他设备可以自动发现我了!
第二步:发现服务(主动找到所有在线的剪贴板)
import socket
import time
from zeroconf import ServiceBrowser, Zeroconf
class ServiceDiscover:
"""监听局域网内的服务"""
def __init__(self):
self.found_services = []
def add_service(self, zeroconf, service_type, name):
"""
关键回调:当发现新服务时,Zeroconf自动调用这个方法
"""
# 根据服务类型和名称,获取详细信息
info = zeroconf.get_service_info(service_type, name)
if info:
# addresses 是二进制格式,转成可读IP
ip = socket.inet_ntoa(info.addresses[0])
self.found_services.append({
"name": name,
"ip": ip,
"port": info.port,
"properties": {k.decode(): v.decode() for k, v in info.properties.items()}
})
print(f"🆕 发现服务:{name}")
print(f" → {ip}:{info.port}")
print(f" → 属性:{self.found_services[-1]['properties']}")
print()
def remove_service(self, zeroconf, service_type, name):
"""服务离线时调用"""
print(f"❌ 服务下线:{name}")
self.found_services = [s for s in self.found_services if s['name'] != name]
# 启动发现
zeroconf = Zeroconf()
listener = ServiceDiscover()
# 开始监听剪贴板服务
browser = ServiceBrowser(zeroconf, "_clipboard._tcp.local.", listener)
print("🔍 正在扫描局域网内的剪贴板服务...")
print(" 按 Ctrl+C 退出\n")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
zeroconf.close()
运行效果:
🔍 正在扫描局域网内的剪贴板服务...
按 Ctrl+C 退出
🆕 发现服务:MyClipBoard._clipboard._tcp.local.
→ 192.168.1.100:8765
→ 属性:{'version': '1.0', 'owner': '大龙虾'}
这就是 Zeroconf 的核心逻辑:发布者广播,发现者监听。各干各的,互不依赖。
场景二:做一个「局域网公告牌」
需求: 公司内部部署了一个公告板服务,新员工入职后打开网页就能自动看到,不用问任何人地址。
服务发布者(后台服务)
import socket
from zeroconf import ServiceInfo, Zeroconf
zeroconf = Zeroconf()
announce_service = ServiceInfo(
"_http._tcp.local.", # 标准HTTP服务类型,浏览器可以直接访问
"CompanyBoard._http._tcp.local.",
addresses=[socket.inet_aton("192.168.1.50")],
port=8080,
properties={
"name": "公司公告板",
"department": "IT部"
},
server="board.local."
)
zeroconf.register_service(announce_service)
print("📋 公司公告板已上线,员工可自动发现")
input()
zeroconf.unregister_service(announce_service)
zeroconf.close()
服务发现者(员工浏览器端)
用户只需要知道服务类型 _http._tcp.local.(这是标准HTTP服务类型),就能找到所有提供Web服务的设备。配合 DNS-SD,可以让浏览器直接发现并列出所有可访问的内网网站。
场景三:模拟「智能音箱发现」
小米音箱配网后,手机App不用输入任何IP,就能自动发现音箱。这个过程用 Zeroconf 来实现非常合适。
音箱固件(发布服务)
import socket
from zeroconf import ServiceInfo, Zeroconf
zeroconf = Zeroconf()
speaker_info = ServiceInfo(
"_xiaomi._tcp.local.", # 自定义协议类型
"小米音箱-客厅._xiaomi._tcp.local.",
addresses=[socket.inet_aton("192.168.1.99")],
port=9000,
properties={
"model": "小米音箱Pro",
"location": "客厅",
"version": "2.4.1"
},
server="speaker-livingroom.local."
)
zeroconf.register_service(speaker_info)
print("🔊 音箱已上线,等待手机发现...")
input()
zeroconf.unregister_service(speaker_info)
zeroconf.close()
手机App(发现服务)
import socket
from zeroconf import ServiceBrowser, Zeroconf
class SpeakerFinder:
def __init__(self):
self.speakers = []
def add_service(self, zeroconf, service_type, name):
info = zeroconf.get_service_info(service_type, name)
if info and b"xiaomi" in info.properties.get(b"model", b""):
ip = socket.inet_ntoa(info.addresses[0])
props = {k.decode(): v.decode() for k, v in info.properties.items()}
print(f"🔈 发现小米音箱:{props.get('location', '未知位置')}")
print(f" 型号:{props.get('model')}")
print(f" 地址:{ip}:{info.port}")
self.speakers.append({"ip": ip, "port": info.port, **props})
zeroconf = Zeroconf()
finder = SpeakerFinder()
browser = ServiceBrowser(zeroconf, "_xiaomi._tcp.local.", finder)
print("🔍 正在搜索附近的小米音箱...")
# ... 配合UI,搜到就显示在列表里
核心概念:用「朋友圈」来理解
把 Zeroconf 的几个关键概念套到朋友圈上:
| 概念 |
朋友圈对应 |
说明 |
| ServiceInfo |
你的个人资料卡 |
包含昵称、头像、签名——你的服务叫什么、提供什么、在哪 |
| Zeroconf |
微信服务器 |
负责把你的资料广播出去,也负责接收别人的广播 |
| ServiceBrowser |
朋友圈列表 |
监听所有好友的动态,有新人进来就通知你 |
| mDNS |
局域网内的广播 |
不用经过微信服务器,直接在局域网内喊话 |
完整项目:局域网设备仪表盘
把上面的场景串起来,做一个自动发现并管理内网服务的仪表盘:
import socket
import time
import json
from zeroconf import ServiceInfo, ServiceBrowser, Zeroconf
# ============ 发布端 ============
def publish_clipboard_service():
"""发布剪贴板服务"""
zc = Zeroconf()
info = ServiceInfo(
"_clipboard._tcp.local.",
"MyClipBoard._clipboard._tcp.local.",
addresses=[socket.inet_aton("192.168.1.100")],
port=8765,
properties={"version": "1.0", "owner": "大龙虾"},
server="myclipboard.local."
)
zc.register_service(info)
return zc, info
# ============ 发现端 ============
class ServiceDashboard:
"""服务仪表盘"""
def __init__(self):
self.services = {} # {service_type: [service_list]}
def add_service(self, zeroconf, service_type, name):
info = zeroconf.get_service_info(service_type, name)
if info:
# 解析IP
ip = socket.inet_ntoa(info.addresses[0])
props = {}
for k, v in info.properties.items():
try:
props[k.decode()] = v.decode()
except:
props[k.decode()] = str(v)
# 按类型分类存储
if service_type not in self.services:
self.services[service_type] = []
self.services[service_type].append({
"name": name,
"address": f"{ip}:{info.port}",
"properties": props
})
def print_dashboard(self):
"""打印仪表盘"""
print("\n" + "=" * 60)
print("🌐 局域网服务仪表盘")
print("=" * 60)
if not self.services:
print("(暂无发现服务)")
for svc_type, items in self.services.items():
print(f"\n📦 类型:{svc_type}")
for item in items:
print(f" [{item['name']}]")
print(f" 地址:{item['address']}")
print(f" 属性:{item['properties']}")
print("\n" + "=" * 60)
# ============ 主程序 ============
if __name__ == "__main__":
# 1. 发布自己的服务
print("[1] 发布剪贴板服务...")
zc_pub, _ = publish_clipboard_service()
print(" ✅ 已发布:MyClipBoard @ 192.168.1.100:8765")
# 等待广播生效
time.sleep(1)
# 2. 启动发现
print("\n[2] 启动服务发现...\n")
zeroconf = Zeroconf()
dashboard = ServiceDashboard()
browser = ServiceBrowser(zeroconf, "_clipboard._tcp.local.", dashboard)
# 3. 持续监控
try:
for _ in range(5): # 扫描5秒
time.sleep(1)
dashboard.print_dashboard()
except KeyboardInterrupt:
pass
finally:
zeroconf.close()
zc_pub.unregister_service(_)
zc_pub.close()
print("\n👋 扫描结束")
运行效果:
[1] 发布剪贴板服务...
✅ 已发布:MyClipBoard @ 192.168.1.100:8765
[2] 启动服务发现...
============================================================
🌐 局域网服务仪表盘
============================================================
📦 类型:_clipboard._tcp.local.
[MyClipBoard._clipboard._tcp.local.]
地址:192.168.1.100:8765
属性:{'version': '1.0', 'owner': '大龙虾'}
============================================================
👋 扫描结束
什么场景该用 / 不该用
✅ 用 Zeroconf 的场景
| 场景 |
为什么适合 |
| 智能家居设备发现 |
设备品类多、数量多,不可能每个都手动配IP |
| 办公室内部工具 |
员工换网络、不记IP,打开就用 |
| 联机游戏大厅 |
玩家上线就出现在列表里,不用手动添加服务器IP |
| IoT 传感器网络 |
传感器"即插即用",自动被发现 |
| 开发/测试环境 |
本地微服务之间互相发现,避免硬编码端口 |
❌ 别用 Zeroconf 的场景
| 场景 |
为什么不适合 |
| 公网服务 |
Zeroconf 是纯局域网协议,不能跨网段 |
| 需要固定IP的生产服务 |
生产环境应该有固定的IP和端口,用DNS解析 |
| 高并发实时通信 |
Zeroconf 只负责"发现",不负责通信 |
| 跨Internet的设备 |
需要穿透 NAT,用 WebRTC/TURN 等方案 |
一句话总结
Zeroconf 的本质就是一个局域网内的「自动广播-接收」机制:
- 发布服务 →
ServiceInfo 定义服务长什么样 → zeroconf.register_service() 广播出去
- 发现服务 →
ServiceBrowser 监听 → 回调 add_service() 收到通知
- 清理 →
unregister_service() + close() 释放资源
记住这三条,就记住了 Zeroconf 的全部。
你在项目里有没有遇到过"设备发现"的场景?是用什么方案解决的?评论区聊聊。