找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3412

积分

0

好友

460

主题
发表于 2 小时前 | 查看: 5| 回复: 0

从一个尴尬的场景说起

设想你写了一个共享剪贴板程序——电脑上复制一段文字,手机上直接粘贴过来。你兴致勃勃地部署到服务器,配好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 的全部。

你在项目里有没有遇到过"设备发现"的场景?是用什么方案解决的?评论区聊聊。




上一篇:Swoole-Compiler AOT 编译器实现 PHP 自举详解
下一篇:beartype 运行时类型检查实战:零开销+复杂类型校验性能对比
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-24 20:18 , Processed in 0.793851 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表