做 Flutter 开发这几年,mDNS 这种局域网服务发现的活儿我没少折腾。智能家居配网、投屏设备搜索、文件互传、会议室设备发现,本质上都绕不开“在同网段里快速找到对方”这一步。之前用 mdns_dart 这个纯 Dart 三方的库在 Android 和 iOS 上跑得很顺,代码量小、逻辑清晰、完全不需要写原生代码。但自从开始做鸿蒙 Flutter 应用适配,问题就来了——鸿蒙的运行时环境、网络权限模型、底层 socket 实现都和安卓那边不完全一样,mdns_dart 能不能直接跑?多播能不能收到包?权限怎么配?如果不支持又该怎么办?这篇文章就聊聊我把 mdns_dart 适配到鸿蒙的完整思路和实操过程,适合正在做鸿蒙 Flutter 生态迁移、或者想在自己的物联网/全场景应用里实现设备发现能力的朋友参考。
1. 项目背景与适配思路
1.1 这个库到底解决什么问题
先别急着谈适配,说清楚 mdns_dart 是干嘛的。mDNS,全称 Multicast DNS,中文叫多播 DNS。它解决的问题很朴素:在一个没有中心服务器的局域网里,设备怎么互相发现、怎么通过名字访问对方。
传统 DNS 需要你配置一个域名服务器,设备访问某个服务之前必须去问 DNS 服务器。但家里、办公室里没有这玩意,总不能为了投个屏专门搭一台服务器。mDNS 的思路是:我不依赖任何中心节点,而是把 DNS 查询包通过 UDP 多播的形式发给局域网内所有设备,跑的端口是 5353,IPv4 的多播地址是 224.0.0.251,IPv6 对应地址是 FF02::FB。支持 mDNS 的设备收到查询后直接把答案回复给请求方,整个过程走的是链路层广播域,不需要任何额外配置。用大白话说,就是“你在楼道里喊一嗓子,整层楼都能听见,谁家有你要的东西谁就应一声”。
mdns_dart 则是这套协议在 Dart 生态里的实现。它的核心价值在于完全用 Dart 写成,底层只依赖 dart:io 的 UDP socket,理论上任何能跑 Dart VM 的环境都能直接复用。在 Flutter 跨端项目里,这意味着服务发现逻辑可以一套代码跑遍 Android、iOS、桌面端,现在又多了一个鸿蒙。
1.2 为什么从一堆方案里选中了它
当时在项目里做设备发现模块,我其实对比过好几条路。第一是直接用平台原生能力,安卓有 NSD(Network Service Discovery)和 WifiP2P,iOS 有 Bonjour,鸿蒙也有自己的分布式设备发现能力。这样做的优点是系统级支持、稳定、省电,缺点同样明显——三个平台各写一套原生逻辑,业务代码还得通过 MethodChannel 来回倒腾,维护成本指数级上升。
第二是引入 AllJoyn、BLE Mesh 这类重量级物联网协议,能力是强,但对一个只需要“发现同一 Wi-Fi 下的几个投屏设备”的功能来说,明显是杀鸡用了牛刀。
第三就是 mdns_dart。它最大的优势是“纯 Dart + 零原生依赖”,在 Android、iOS、Linux、macOS 上都验证过没问题。这意味着我只需要维护一套 Dart 代码,平台差异几乎可以忽略。这正是我做鸿蒙适配的底气——如果 Dart 侧的逻辑能被鸿蒙 Flutter 运行时支持,那么迁移成本就只是“环境适配”,而不是“逻辑重写”。事实也证明,这个选择让后续的排障路径清晰了很多。
1.3 鸿蒙 Flutter 适配的特殊性在哪
聊到鸿蒙适配,必须先搞清楚一个事实:鸿蒙上的 Flutter 并不是谷歌官方分支,而是 OpenHarmony 社区维护的一个 fork,跟着上游 Flutter 版本做同步演进。这意味着 dart:io 的大部分能力在鸿蒙上都有对应实现,但细节上可能存在差异,尤其是涉及到多播、广播这类偏底层的网络能力时,谁都不敢拍胸脯保证一定没问题。
在 ArkTS 侧,鸿蒙的 socket 能力和标准 TCP/UDP 基本类似,提供了一套基于 Promise 和回调风格的异步 API。但鸿蒙应用的多播权限、组播成员管理(也就是 joinGroup)、防火墙策略、以及系统对 UDP 5353 端口的占用情况,都可能成为服务发现跑不起来的隐患。所以适配工作的核心就一个字:验。先验证纯 Dart 方案能不能跑通,跑不通就拆解是哪一层出了问题,再针对性做桥接。这套流程就是我下面要详细展开的内容。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. mDNS 协议与 mdns_dart 核心机制
2.1 mDNS 的协议骨架
直接把 mDNS 当成“没有服务器版的 DNS”是不够的,因为它在协议细节上和普通 DNS 有一些差异,这些差异恰恰主导了适配时要踩的坑。
mDNS 的报文格式和 DNS 报文完全一致,也是 Header、Question、Answer、Authority、Additional 五段式结构。但它最核心的两个特点:一是使用链路本地多播地址 224.0.0.251/FF02::FB 和固定端口 5353 通信;二是它的域名都带 .local 后缀,比如 _http._tcp.local 代表“局域网内的 HTTP 服务”。服务实例名则由三部分组成:实例名 + 服务类型 + .local,例如 LivingRoomTV._airplay._tcp.local。
在记录类型上,mDNS 常用的有 PTR(服务发现,告诉你能提供什么服务)、SRV(拿到实例名后查端口和主机名)、TXT(携带元数据,比如设备型号、固件版本)、A/AAAA(查 IP 地址)。整个发现流程就是一个 PTR 查询打头,然后顺着 SRV、TXT、A 这些资源记录逐级展开的过程。
这里有一个大家容易忽略的细节:mDNS 的 TTL 管理。普通 DNS 记录 TTL 失效后客户端重新查询,而 mDNS 的设备在 TTL 快到期时会被要求主动续期,也叫“主动宣告”,设备下线时则发送一个 TTL 为 0 的响应告诉全网“我走了”。这套机制保证了 mDNS 设备列表是实时刷新的,但也对我们的状态治理代码提出了更高要求——不能只看“响应过就去掉了”,还得盯着缓存过期时间和下线的 goodbye 包。
2.2 mdns_dart 的发现流程拆解
mdns_dart 的 API 设计很简洁,核心就几个类:MDnsClient 负责启动、查询和资源记录解析;MDnsResolver 负责把字节流解析成结构化消息;另外就是一堆资源记录类型,从 `PtrResourceReco
