MDNS学习笔记

MDNS学习笔记

[TOC]

🚥引言


1️⃣ 为什么需要 DNS?

在计算机网络中,设备之间真正通信依赖的是 IP 地址,比如:

1
192.168.0.100

但对人类来说,记住一堆数字显然并不友好。
于是 DNS(Domain Name System) 应运而生,它的作用非常简单:

把人类可读的域名,转换成机器可用的 IP 地址

例如: www.example.com93.184.216.34

2️⃣ 传统 DNS 是如何工作的?

在一个典型的网络环境中,DNS 的工作流程是这样的:

  1. 设备想访问 www.example.com
  2. 向配置好的 DNS 服务器发起查询
  3. DNS 服务器返回对应的 IP 地址
  4. 设备使用该 IP 建立连接

这里有一个前提条件

网络中 必须存在一个 DNS 服务器
并且所有设备都 知道它的地址

在互联网环境下,这完全不是问题:

  • 运营商
  • 路由器
  • 公共 DNS(8.8.8.8、1.1.1.1)

3️⃣ DNS 在局域网中遇到的问题

但如果我们把场景换到 局域网(LAN),问题就来了。 比如:

  • 家庭内网
  • 实验室
  • 开发板 + PC
  • IoT 设备
  • 临时搭建的小网络 在这些场景中,往往会出现:
  • ❌ 没有专门的 DNS 服务器
  • ❌ 设备 IP 由 DHCP 动态分配
  • ❌ IP 经常变化
  • ❌ 手动维护 hosts 文件不现实

你可能会遇到这种尴尬情况:

“我知道设备在局域网里,但我不知道它现在的 IP 是多少。”

4️⃣ 问题来了:有没有一种「零配置」的名字解析?

理想的情况是:

  • 不需要配置 DNS 服务器
  • 不需要手动指定 IP
  • 设备一上线就能被访问
  • 用名字而不是 IP 比如我希望直接访问:
1
http://客厅灯.local

而不是去路由器后台翻 IP。

这正是 mDNS 要解决的问题。

🔋工作原理


1️⃣.mDNS 的核心思想

mDNS(Multicast DNS)的核心思想可以用一句话概括:

没有 DNS 服务器,所有设备既是客户端,也是服务器。

可以理解为:

一种在局域网中,不依赖 DNS 服务器的域名解析机制

在传统 DNS 中:

  • 查询请求发给 DNS 服务器
  • 服务器返回结果

而在 mDNS 中:

  • 查询请求通过 组播 发送到局域网
  • 所有设备都能收到这个请求
  • 只有“名字匹配”的设备才会回应

核心特点是:

  • 📡 使用 组播 而不是单播
  • 🌐 作用范围仅限 本地链路 不会被路由转发
  • ⚙️ 完全 零配置
  • 🏷️ 常见域名后缀是 .local

2️⃣. mDNS 使用的网络基础

mDNS 并没有重新发明一套协议,而是建立在现有网络机制之上:

项目 mDNS 取值
传输层协议 UDP
端口号 5353
IPv4 组播地址 224.0.0.251
IPv6 组播地址 ff02::fb
作用范围 本地链路(Link-Local)

3️⃣. 一个 mDNS 查询是如何发生的?

以我的wb2模组模拟的一个灯服务为例:

Step1:发起查询

我在浏览器输入:

1
http://mywb2light.local

系统发现:

  • 后缀是 .local
  • 这是一个 mDNS 域名

于是构造一个 DNS 格式的数据包(注意:格式还是 DNS):

  • 查询名:mywb2light.local
  • 查询类型:A / AAAA
  • 目标地址:224.0.0.251:5353

并通过 UDP 发送到组播地址。

Step2: 局域网内设备接收查询

此时,局域网内所有加入组播的设备都会收到消息,比如你的打印机,文件服务器nas

但每个设备都会检查,查询的名字是不是 “我”

显然,只会有我的wb2模组查到,哎呀,有人找我!

Step 3:目标设备回应

我的WB2开启了http灯的服务,且已经加入了组播 注册的主机名就是:

1
mywb2light.local

它会:

  • 构造一条 DNS Response
  • 包含自己的 IP 地址, 比如:192.168.0.103
  • 同样发送到 224.0.0.251:5353

而不是直接单播给请求者。

Step 4:请求方接收并缓存结果

请求设备收到响应后:

  • 提取 IP 地址
  • 缓存一段时间(TTL)
  • 后续访问无需再次查询

至此,一次 mDNS 解析完成。

4️⃣.为什么回应也要用组播?

你可能会问:

“既然已经知道是谁在问了,为什么不直接单播回应?”

这是 mDNS 的一个重要设计点

  • 所有设备都看到这个响应
  • 其他设备可以:
    • 更新缓存
    • 避免重复查询
    • 降低整体网络流量

这种机制被称为:

共享响应(Shared Response)

当然,在某些情况下(如冲突处理),也可以使用单播回应。

5️⃣.名字冲突是如何解决的?

既然没有中心服务器,就不可避免会出现冲突:

两台设备都想叫 卧室灯.local 怎么办?

mDNS 的解决方式是:

  1. 启动时“自我查询” 设备上线后,会先查询:

    1
    卧室灯.local

    看有没有人已经在用。

  2. 冲突检测 如果收到响应,说明名字已被占用:

    • 自动修改名字

    • 常见策略:

      1
      2
      3
      卧室灯.local
      卧室灯-2.local
      卧室灯-3.local
  3. 冲突广播

    如果真的发生冲突(两个设备同时声明):

    • 会广播冲突信息
    • 其中一个设备必须让步并改名

    整个过程完全自动完成。

💉如何发布一条记录


在 mDNS 中,“发布一条记录”并不是向某个服务器注册,而是:

在合适的时机,向局域网主动广播自己拥有的 DNS 记录

这些记录通常包括:

  • 主机名 → IP(A / AAAA)
  • 服务信息(配合 DNS-SD)

mDNS 中的“记录”是什么?

mDNS 使用的仍然是标准 DNS Resource Record(RR),常见的有:

记录类型 作用
A 主机名 → IPv4
AAAA 主机名 → IPv6
PTR 服务类型枚举
SRV 服务所在主机与端口
TXT 服务附加信息

例如最简单的一条主机记录:

1
my-device.local. A 192.168.1.50

发布记录的基本原则

mDNS 有几个非常重要的原则:

  1. 没有注册中心
  2. 记录由“拥有者”负责发布
  3. 通过组播发送
  4. 其他设备被动缓存

主机名记录(A / AAAA)的发布流程

Step 1:名称探测(Probe)

设备启动后,不能立刻宣布自己,而是先探测:

“有没有人已经在用 my-device.local?”

做法是:

  • 224.0.0.251:5353 发送 Query
  • 查询名:my-device.local
  • 查询类型:A / AAAA

如果在规定时间内 没有收到回应,说明名字可用。

Step 2:宣布(Announce)

确认名字可用后,设备会主动发送 Announcement

  • 本质:一条 DNS Response
  • 不针对某个查询
  • 包含:
    • my-device.local 的 A / AAAA 记录
  • 发送到组播地址

通常会:

  • 连续发送 2~3 次
  • 间隔逐渐增大(防丢包)

这一步就是正式发布 mDNS 记录

Step 3:周期性刷新

为了防止缓存过期:

  • 每条记录都有 TTL(通常 120 秒)
  • 设备会在 TTL 过半时再次广播
  • 刷新仍然使用 Response(Announcement)

记录撤销(Goodbye)

当设备下线或服务停止时,不能直接消失。 正确做法是发送一条 Goodbye 包

  • 同样是 DNS Response

  • TTL = 0

  • 表示:

    “这条记录马上失效”

其他设备收到后会立刻清除缓存。

服务记录的发布(DNS-SD)

如果你要发布的不只是主机名,而是一个服务(比如 HTTP):

【示例目标】 发布一个 HTTP 服务: my-device.local:80

需要发布的记录(成组出现)

mDNS 服务发布通常包含 四类记录

  • PTR

    (服务类型 → 实例名)

    1
    _http._tcp.local. → My Web Server._http._tcp.local.
  • SRV

    (实例名 → 主机名 + 端口)

    1
    My Web Server._http._tcp.local. SRV my-device.local:80
  • TXT

    (服务属性)

    1
    My Web Server._http._tcp.local. TXT "path=/"
  • A / AAAA

    (主机名 → IP)

    1
    my-device.local. A 192.168.1.50

mDNS 要求这些记录一起发布,否则服务发现会不完整。

什么时候该“发布”?

mDNS 并不是一直广播,而是事件驱动

场景 是否发布
设备启动
IP 地址变化
服务启动
收到相关查询
TTL 即将过期
设备下线 ✅(Goodbye)
1
2
3
4
5
6
7
8
9
10
11
设备启动

Probe: my-device.local ?
↓ (无人回应)
Announce: my-device.local = 192.168.1.50

正常运行(响应查询 / 刷新 TTL)

发送 Goodbye(TTL=0

设备下线

🔍服务发现的核心


Typing SVG

在 mDNS 中,A/AAAA 解决的是“你在哪”
PTR + SRV 解决的是“你提供什么服务,以及怎么连”

这也是 DNS-SD(DNS Service Discovery)的核心设计。

1. 为什么不能只用 A 记录?

假设局域网中有多台设备:

  • dev1.local
  • dev2.local
  • dev3.local 它们可能分别提供:
  • HTTP
  • SSH
  • MQTT
  • 打印服务

如果只有 A 记录,你只能做到:

“我知道这个名字对应哪个 IP”

但你不知道

  • 哪些设备提供 HTTP?
  • 服务监听在哪个端口?
  • 一台设备是否有多个同类服务?

PTR 和 SRV 正是为了解决这些问题而存在的。

2. PTR:服务“目录索引”

PTR 记录的作用

列出“某一类服务”在局域网中有哪些实例

PTR 的查询对象不是主机名,而是“服务类型” 例如:

1
_http._tcp.local.

这并不是某台设备,而是一个服务类别

PTR 记录的含义: 一条 PTR 记录长这样:

1
_http._tcp.local. PTR My Web Server._http._tcp.local.

含义是:

  • 在本地网络中,存在一个 HTTP 服务实例
  • 名字叫 My Web Server

一个服务类型可以对应多个 PTR

1
2
_http._tcp.local. PTR Web1._http._tcp.local.
_http._tcp.local. PTR Web2._http._tcp.local.

表示:局域网中有两个 HTTP 服务实例

3. SRV:告诉你“怎么连”

真正的连接信息 是由 SRV 提供的。

SRV 记录包含什么? 一条 SRV 记录包含:

  • 目标主机名
  • 端口号
  • 优先级(priority)
  • 权重(weight)
1
2
My Web Server._http._tcp.local.
SRV 0 0 80 my-device.local.

表示:

  • HTTP 服务实例 My Web Server
  • 运行在 my-device.local 的 80 端口

4. 为什么端口不能直接写在 PTR 里

设计目标是:

  • 一个服务实例 = 一个逻辑实体
  • 它可以:
    • 换端口
    • 换主机
    • 多主机负载 而 PTR 只负责索引,SRV 负责定位

5. 服务发现的完整流程

step1:查询服务类型(PTR) 客户端发送:

1
Query: _http._tcp.local. PTR ?

得到:

1
PTR → My Web Server._http._tcp.local.

step2:查实例定位(SRV) 客户端继续查询:

1
Query: My Web Server._http._tcp.local. SRV ?

得到:

1
SRV → my-device.local:80

step3: 解析主机名(A / AAAA)

1
Query: my-device.local. A ?

得到 IP

step4: (可选):读取 TXT

1
Query: My Web Server._http._tcp.local. TXT ?

获取额外属性(如路径、版本、能力)。

6. DNS-SD工程化的设计

记录 责任
PTR 发现(有哪些)
SRV 定位(在哪 + 端口)
TXT 描述(属性能力)
A/AAAA 地址解析

这种拆分带来的好处:

  • 一个设备可发布多个服务
  • 一个服务可迁移主机
  • 服务实例名可读性强
  • 客户端无需硬编码端口

7. 完整的服务记录发布示例

1
2
3
4
_http._tcp.local. PTR My Web Server._http._tcp.local.
My Web Server._http._tcp.local. SRV 0 0 80 my-device.local.
My Web Server._http._tcp.local. TXT "path=/"
my-device.local. A 192.168.1.50