做 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 负责把字节流解析成结构化消息;另外就是一堆资源记录类型,从 PtrResourceRecord、SrvResourceRecord、TxtResourceRecord 到 IPAddressResourceRecord。
用起来大概是这种感觉:
dart复制final client = MDnsClient(rawSocket: RawDatagramSocket.bind(InternetAddress.anyIPv4, 0));
await client.start();
await for (final serviceType in client.listServiceTypes()) {
// 列出网段内所有服务类型
}
await for (final ptr in client.lookup<PtrResourceRecord>(
ResourceRecordQuery(serviceType: '_http._tcp.local'),
)) {
final serviceName = ptr.domainName;
final srvRecords = await client.lookup<SRVResourceRecord>(
ResourceRecordQuery(serviceName: serviceName),
);
}
底层的工作机制值得多说一嘴,因为这决定了适配的突破口。MDnsClient 内部会维护一个 UDP socket,启动时绑定网卡和端口;发送查询时把 DNS 查询报文打包发给多播组;收到响应后用一个队列把所有数据包收集起来,由 MDnsResolver 逐条解码,然后根据查询类型分配给对应的 Stream 消费者。
换句话说,mdns_dart 的“应用层逻辑”和“网络收发层逻辑”其实是可以拆开的。它之所以需要在构造时传入 rawSocket,就说明作者已经考虑到底层 socket 的可替换性。这个设计后来成了我做鸿蒙桥接方案的关键。如果鸿蒙的 dart:io 多播能力不可用,我们要替换的只是最底层收发包的这一层,而不是把整库推倒重来。
2.3 鸿蒙网络栈对 mDNS 的兼容性分析
这部分是我适配前的预判分析,也是整篇文章最核心的判断逻辑。
首先,鸿蒙 Flutter fork 对 dart:io 里的 RawDatagramSocket 是有实现的,UDP 收发基本可控。但 mDNS 要求我们必须加入一个多播组,udp 本身支持绑定到 224.0.0.251:5353 后直接收发多播包,但更标准的做法是先绑定本地端口,然后调用 joinGroup 加入多播组,这样才能稳定收到组内所有设备的主动宣告包和响应包。joinGroup这个行为在 Android 上走的是 POSIX 标准的 IP_ADD_MEMBERSHIP,鸿蒙的内核同样支持,但问题往往出在 Flutter engine 有没有把这一层 syscall 封装透传出来。
其次,权限模型有差异。Android 的 Flutter 应用想要访问网络,在 AndroidManifest 里声明 INTERNET 权限即可。鸿蒙应用则是在 module.json5 里声明 ohos.permission.INTERNET。这个权限不声明,socket 就算建起来,send 和 receive 也会报权限异常。如果漏掉这一步,表现就是“没报错但收不到任何设备”。
最后是系统服务和端口占用。鸿蒙系统本身可能已经在 5353 端口上跑了自己的 mDNS 服务,比如分布式软总线里的设备发现模块。如果系统进程占用了 5353,我们 bind 的时候报 address already in use;如果系统只监听不独占,那我们的应用也能绑到同一个多播组,但要留意系统服务可能已经消费了一部分查询响应,导致自己的 SDK 看到的设备列表不完整。这些实际表现我在后面的调试章节会结合问题清单详细讲。
3. 鸿蒙化适配实操全过程
3.1 环境准备与工程配置
开始之前,先把环境和工程配好。当前鸿蒙 Flutter 开发建议使用 DevEco Studio 和鸿蒙 SDK 构建原生工程,再用 flutter_flutter 的鸿蒙 fork 分支跑 Flutter 侧代码。
创建完 Flutter 工程后,先打开 harmony 目录下的 module.json5,检查网络权限。这是最容易漏的环节。
json5复制{
"module": {
"name": "entry",
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "用于局域网内的设备服务发现与连接"
}
]
}
}
如果项目之后还要做动态权限申请或申请精确定位权限(部分 Wi-Fi 扫描场景会需要),再按需补。这里要提醒一点:只做 mDNS 服务发现,INTERNET 权限基本就够用了,有些开发者会顺手加上各种权限,这没有必要,过度声明权限在鸿蒙应用上架审核时反而容易引起不必要的关注。
3.2 方案一:纯 Dart 方案直接跑通
配置完权限,第一步一定是先用最纯净的方式验证 mdns_dart 能不能跑。直接在鸿蒙 Flutter 工程里加依赖:
yaml复制dependencies:
mdns_dart: ^0.5.0
然后写一段最小验证代码:
dart复制import 'dart:async';
import 'dart:io';
import 'package:mdns_dart/mdns_dart.dart';
Future<void> main() async {
final socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
final client = MDnsClient(rawSocket: socket);
await client.start();
await for (final ptr in client.lookup<PtrResourceRecord>(
ResourceRecordQuery(serviceType: '_http._tcp.local'),
)) {
print('发现服务: ${ptr.domainName}');
}
}
把这段代码放到鸿蒙工程里跑到真机上,你可能会看到三种结果:第一种是完美跑通,设备列表持续输出,那这篇文章到这就是最完美结局;第二种是能正常启动但始终等不到任何 PTR 响应,这种就基本断定多播包没发出去,或者发出的包被系统策略拦截了;第三种是启动就抛异常,一般集中在 socket bind 或多播组加入的操作上。
我们项目实测的结果是第二种,并且加上了超时日志后,UDP 的 send 接口确实返回了成功,但局域网内另一端设备反馈从未收到任何 mDNS 查询包。判断依据很明确:问题不是出在 Dart 编译层,而是鸿蒙 runtime 对多播报文的发送没有完整透传,也可能受限于外层网络策略。排查到此,纯 Dart 方案在目标设备上不可用,就要考虑桥接方案了。
注意: 这里的结论只代表我测试的具体鸿蒙设备,不代表所有鸿蒙版本和机型。建议你务必在自己的目标设备上跑一遍最小验证,因为不同芯片方案对组播能力的底层实现确实存在差异。盲信任何一个博主的结论都是不负责任的。
3.3 方案二:Platform Channel 桥接鸿蒙原生能力
既然 dart:io 的多播发送不可靠,那最简单直接的思路就是:把 mDNS 的“网络收发层”从 Dart 侧换掉,让鸿蒙原生侧去负责多播报文的收发,Dart 侧继续负责协议编解码和业务逻辑。这就是 Platform Channel 方案。
架构上分三步走:
- MethodChannel 负责启动、停止、发送数据;
- EventChannel 负责把原生侧收到的 mDNS 应答字节流持续推送给 Dart;
- Dart 侧拿到字节流后,复用 mdns_dart 的
MDnsResolver做解析,或者干脆自己实现一个轻量 DNS message parser。
先看 Dart 侧的封装,我写了一个原生链路类,和 mdns_dart 原本的 socket 能力做兼容对齐:
dart复制import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/services.dart';
class NativeMDnsHost {
static const MethodChannel _methodChannel = MethodChannel('mdns_host');
static const EventChannel _eventChannel = EventChannel('mdns_host_events');
Stream<Uint8List>? _receiveStream;
Future<void> start() async {
await _methodChannel.invokeMethod('start');
}
Future<void> stop() async {
await _methodChannel.invokeMethod('stop');
}
Future<void> send(Uint8List data, String address, int port) async {
await _methodChannel.invokeMethod('send', {
'data': data,
'address': address,
'port': port,
});
}
Stream<Uint8List> receive() {
if (_receiveStream == null) {
_receiveStream = _eventChannel
.receiveBroadcastStream()
.map((event) => Uint8List.fromList((event as List<dynamic>).cast<int>()));
}
return _receiveStream!;
}
}
这里我特意把接收到的原始字节流用 Uint8List 暴露,方便后续直接喂给协议解析层,而不是在原生层做任何业务判断。原生层做得越“笨”,Dart 侧的可控性就越好。
再看鸿蒙侧的 ArkTS 实现。这里我用的是鸿蒙网络模块 @kit.NetworkKit 提供的 UDP socket 能力,和标准 POSIX socket 设计思路一致。核心逻辑就是:绑定网卡、加入多播组、循环收包、通过 EventChannel 把字节流推向 Dart,同时接收 Dart 侧 MethodCall 发来的发包请求。
typescript复制import { socket } from '@kit.NetworkKit';
import { util } from '@kit.ArkTS';
import { MethodCall, MethodChannel, EventChannel } from '@ohos/flutter_ohos';
const MDNS_ADDRESS = '224.0.0.251';
const MDNS_PORT = 5353;
export class MDnsHostPlugin {
private udp: socket.UDPSocket | null = null;
private methodChannel: MethodChannel = new MethodChannel('mdns_host');
private eventChannel: EventChannel = new EventChannel('mdns_host_events');
private eventSink: EventChannel.EventSink | null = null;
constructor() {
this.methodChannel.setMethodCallHandler((call: MethodCall) => {
switch (call.method) {
case 'start':
return this.start();
case 'stop':
return this.stop();
case 'send': {
const data = call.arguments['data'] as Array<number>;
const address = call.arguments['address'] as string;
const port = call.arguments['port'] as number;
return this.send(data, address, port);
}
default:
return Promise.reject(new Error('未知方法: ' + call.method));
}
});
}
private async start(): Promise<void> {
this.udp = socket.constructUDPSocketInstance();
await this.udp.bind({ address: '0.0.0.0', port: 0, family: 1 });
await this.udp.joinGroup(MDNS_ADDRESS);
// 注册消息监听,收到数据后推到 Dart 侧
this.udp.on('message', (info) => {
const bytes = new Uint8Array(info.message);
// 通过 EventChannel 的 sink 发送给 Dart
this.eventSink?.success(Array.from(bytes));
});
}
private async send(data: Array<number>, address: string, port: number): Promise<void> {
if (!this.udp) return Promise.reject(new Error('socket 未启动'));
const buffer = new Uint8Array(data).buffer;
await this.udp.send({
data: buffer,
address: { address, port, family: 1 },
});
}
private async stop(): Promise<void> {
await this.udp?.close();
this.udp = null;
}
}
这里有几个关键点需要展开解释。第一,bind 时端口为什么要填 0?因为在系统其他进程可能占用 5353 的前提下,动态分配一个本地端口可以避免端口冲突,同时把多播查发包的源端口交给系统调度。但要注意:mDNS 规范里客户端查询端口可以是任意高位端口,服务器响应时会把多播响应发回源端口,所以这里并不强制使用 5353。第二,joinGroup(MDNS_ADDRESS) 这步不能省,它决定了 socket 能不能收到链路层里其他设备发给多播组的数据包。第三,Dart 侧拿到的是 Array<number>,到 Flutter 端会自动转成 List<dynamic>,所以我在 Dart 侧做了一次 .cast<int>() 的转换。
但这个方案有一个绕不开的代价:mdns_dart 内部原本是直接从 socket 读 UDP 数据的,现在改成从 EventChannel 读数据,就不能直接使用 MDnsClient 的公开 API 了。我们当时采取的做法是把 mdns_dart 的源码拷到项目里(MIT 协议允许),在 MDnsClient 内部增加一个 Stream<Uint8List> Function() 的数据源注入点,相当于把网络层做了一个依赖注入替换。如果你不想入侵第三方库源码,也可以在自己写一个轻量的 mDNS 解析器,只解析 PTR、SRV、TXT、A 这四个常用记录类型,对多数业务场景来说已经足够。
3.4 方案三:对接系统分布式能力做增强
在基础 mDNS 跑通之后,还有一个进阶选择:在鸿蒙上充分利用系统自带的分布式软总线能力。鸿蒙系统的设备发现模块并不等同于 mDNS,它可以做到跨设备、跨网络形态的自动发现和组网,能力比 mDNS 更强。但它的问题是绑定鸿蒙生态,和 Android/iOS 设备不能互通。
所以我的建议是做成双通道:对内走鸿蒙系统发现能力,对外走 mDNS,以 mDNS 为主。这样在纯鸿蒙环境里,你会发现一些标准的 mDNS 查询不到的 IoT 设备,但系统软总线却能感知到;在与 Android、iOS 混用的跨端场景里,mDNS 又保证了最基本的互通性。我在实际项目里就是先跑通 mDNS,再把软总线的设备状态事件合流到同一个设备列表里,上层业务完全无感。
4. 性能优化与设备治理
4.1 多设备并发场景下的 mDNS 性能问题
一旦设备数量上去了,mDNS 的缺陷会逐渐暴露。最典型的是响应风暴:如果局域网里有 30 台设备都支持 _http._tcp.local,你的 PTR 查询一次性就可能收到几十个应答,每个应答还往往带着 A 记录、SRV 记录。不加以控制的话,EventChannel 推送的数据量会瞬间打满 Flutter / Dart 侧的接收缓冲,导致丢包和 UI 卡顿。
我们可以做三个层次的优化。第一层是启动时控制并发查询量,分批向多播组发送查询,避免一上来就用一个 PTR 把全网设备都炸出来。第二层是在 Dart 侧做节流合并,用 StreamTransformer 把 100 毫秒内的重复设备名合并成一条,避免同一个设备反复上报。第三层是引入 known-answer suppression,这也就是 mDNS 协议本身就支持的“已知答案抑制”,客户端在查询时把自己已经知道的记录作为附加资源带上,这样已经上线的设备就不会再做重复响应,只有新设备才会回复,网络整体负载会显著降低。
4.2 设备状态管理与上下线通知
设备发现不是一次性的查询,而是一个持续维护的过程。我们必须建模设备的状态机:活跃(active)、待确认(probing)、离线(stale)、移除(removed)。
我把 mDNS 的 TTL 作为状态迁移的核心驱动。每当收到设备的 PTR 或 SRV 记录时,就更新设备的上次存活时间,并重置计数器的 TTL 计时。如果 TTL 期结束前又收到了主动宣告包,设备保持 active;如果没收到,设备先进入 stale,此时 UI 上仍然显示设备,但交互会提示“连接可能不稳定”;再过一个宽限期,如果仍然没有恢复,设备状态置为 removed,同时发送一个事件给上层业务。
这个状态机的实现不复杂,但少了它,设备列表就会出现“设备早就下线了,但列表里还留着残影”这种很尴尬的现象。mDNS 的 goodbye 包(TTL=0 的响应)很多设备实现得并不标准,所以不能只依赖 goodbye 包,必须把 TTL 超时作为最终兜底逻辑。
4.3 弱网与功耗控制
对移动端来说,持续监听多播包其实是有功耗开销的。mDNS 需要本地 socket 一直处于接收状态,如果应用长期在后台,Wi-Fi 可能被系统挂起,多播包自然就收不到了。所以我在实际工程里做了两层控制:一是前台时把 mDNS 保持全速运行,包括主动查询和被动监听;二是一旦退到后台,就把主动查询频率降到最低,只保留被动监听,甚至直接暂停服务,等回到前台再重新 start。
弱网环境下还有一个很典型的问题:UDP 包在 Wi-Fi 弱信号时丢包率会直线上升。我的经验是引入“基础查询 + 指数退避重查”的机制。初始查询间隔 1 秒,连续 3 次没有新设备就增加到 2 秒、4 秒、8 秒,上限是 30 秒。这样既不造成风暴,又能在网络抖动恢复后尽快发现新设备。
5. 常见问题与排查技巧实录
5.1 第一步:先分层,再查因
遇到服务发现问题,最忌讳的就是直接改代码瞎试。我给自己定了一套固定的排查流程,效率很高。先判断问题出现在收包、发包、解析、业务渲染哪一层。怎么判断?很简单,在鸿蒙原生的 socket 接收回调里加一个日志,打印收到的原始字节数和来源地址。如果原生层能收到数据,问题一定在 Dart/Flutter 的 EventChannel 传递或解析层;如果原生层收不到任何数据,问题就在网络权限、多播组加入、路由器策略或对端设备。
这套分层排查法的好处是,能快速把“鸿蒙系统问题”和“Dart 代码问题”切分开,不用在一个模糊的表象上反复折腾。
5.2 常见问题速查表
这些是我在不同设备上实际踩过并解决的坑,整理成表,方便按图索骥。
| 问题现象 | 可能原因 | 排查与解决办法 |
|---|---|---|
| 启动后长时间收不到设备 | module.json5 未声明 INTERNET 权限 | 检查权限配置,重新签名后安装 |
| Report address already in use 异常 | 系统 mDNS 服务占用了 5353 端口 | bind 端口改为 0,用动态端口 |
| 能看到部分设备但漏掉一部分 | 路由器开启了 IGMP Snooping,多播组老化 | 检查路由器的组播设置,或让设备周期性主动宣告 |
| 能收到 PTR 但解析不出 SRV/TXT | 可能是 mdns_dart 解析器对变长报文支持不完整 | 将报文抓下来,用 Wireshark 对比标准 DNS 格式 |
| 后台挂起后恢复,设备列表为空 | Wi-Fi 多播被系统挂起,socket 未重连 | onResume 时重新创建 socket,重新 joinGroup |
| 发现延迟很长,几十秒才出设备 | 主动查询间隔过长或者 known-answer suppression 误判 | 调小初始查询间隔,关闭已知答案抑制调试对比 |
| 鸿蒙真机上 socket send 返回成功但对端无响应 | 多播包被系统防火墙或网络策略拦截 | 使用方案二,走原生的 socket joinGroup 后发送 |
提示: 表格里“多播发送不可靠”的问题,不一定每个鸿蒙版本都有。务必以你手上的真机实测为准。我在不同机型上得到的结果就不完全一致,有的机型方案一直接就通过了。
5.3 避坑心得
再说几个常规文档里不会写的细节。
第一,鸿蒙的 Flutter 插件里注册 EventChannel 时,Sink 只有在监听端准备好之后才会把事件推过去。如果你在 start 方法里马上往 EventChannel 里写数据,而 Dart 侧 Stream 还没开始监听,事件就丢了。稳妥做法是:Dart 侧先发起 receive(),等待 Stream 开始监听,再调用 start() 开启原生收包。
第二,不要试图在一个 socket socket 上反复 joinGroup。如果 socket 关闭了再重新创建,旧的 socket 必须显式 close,否则多播组在系统层面可能残留引用,导致新的 socket 收不到、发不出。我在开发时遇到过一次,看起来是代码没变但功能随机失效,最后就是加了一行 close 调用解决的问题。
第三,多播地址可以排查一下是不是被 AP 的“客户端隔离”功能挡掉了。很多路由器/AP 默认开启了 AP 隔离,隔离开后,局域网内设备之间不仅 mDNS 不通,TCP 直连也会被阻隔。遇到这种环境,任何代码层面的“性能优化”都没用,直接去后台关闭隔离或者接入其他网络再测试。
结尾聊点实在的
适配 mdns_dart 到鸿蒙,整个过程给我的最大感受是:跨平台框架带来的红利,在遇到“非标准平台”时总会打上折扣。但好在 mDNS 协议本身是开放的,mdns_dart 的代码结构也足够干净,这种半替换式的适配路线最终走通了,设备发现能力在鸿蒙上稳定跑了起来。
我个人在实际项目里的体会是,不要纠结于“一套代码吃遍所有平台”这个执念。网络层的底层能力,该接原生就得接原生;业务层的协议处理,该在 Dart 里做就得在 Dart 里做。把这两层的边界划清楚,工程上的收益远大于那几行 Platform Channel 的胶水代码。
最后再分享一个小技巧:如果你们项目既要在鸿蒙跑,又要在 Android/iOS 跑,建议在 Dart 层维护一个统一的 MDnsService 接口,内部根据平台判断是用纯 Dart 方案还是桥接方案。这样以后即使鸿蒙官方 Flutter 引擎补齐了多播能力,你也只需要在接口实现里切换一条分支,完全不用动业务代码。
