欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

在这里插入图片描述

前言

在智能家居、打印机共享、P2P 文件传输等场景中,设备往往通过局域网互联。但 IP 地址是动态分配的,用户不可能去记 192.168.1.105。mDNS(Multicast DNS)允许设备在没有 DNS 服务器的情况下,通过 .local 域名互相发现。

multicast_dns 是 Dart 官方提供的库,用于发送和接收 mDNS 报文。你可以用它来查找局域网内的 HTTP 服务、Google Cast 设备,或者广播自己的服务供他人发现。

一、概念介绍/原理解析

1.1 基础概念

  • Multicast (组播): 向特定组播地址(224.0.0.251)发送数据包,局域网所有设备都能收到。
  • Service Type (服务类型): 如 _http._tcp (Web 服务), _googlecast._tcp (Chromecast)。
  • Record (记录): SRV (端口), TXT (元数据), A (IP)。

查询: _http._tcp.local

响应: 佳能打印机 @ 192.168.1.5

响应: 客厅电视 @ 192.168.1.8

手机终端

局域网组播

打印机

电视机

1.2 进阶概念

虽然 iOS/Android/macOS 系统层面都有 mDNS 服务(Bonjour/NsdManager),但 multicast_dns 是在 Dart 层直接操作 UDP Socket 实现的,因此跨平台一致性较好,但也受制于 Socket 权限。

二、核心 API/组件详解

2.1 基础用法

搜索局域网内的 Google Cast 设备。

import 'package:multicast_dns/multicast_dns.dart';

void main() async {
  final client = MDnsClient();
  await client.start();
  
  // 查询指针记录 (PTR)
  await for (final ptr in client.lookup<PtrResourceRecord>(
    ResourceRecordQuery.serverPointer('_googlecast._tcp.local'),
  )) {
    print('发现设备: ${ptr.domainName}');
    
    // 进一步查询详情 (SRV, IP)
    await for (final srv in client.lookup<SrvResourceRecord>(
      ResourceRecordQuery.service(ptr.domainName),
    )) {
      print('端口: ${srv.port}, 主机: ${srv.target}');
    }
  }
  
  client.stop();
}

在这里插入图片描述

2.2 广播服务

目前库主要侧重于 客户端查询。要想作为服务端广播,通常需要更底层的 API 或者依赖平台插件(如 nsd)。但若是纯 Dart 环境,可以手动构造响应包(较复杂)。

三、常见应用场景

3.1 场景 1:查找打印机

搜索 _ipp._tcp_printer._tcp 服务。

await for (final ptr in client.lookup<PtrResourceRecord>(
  ResourceRecordQuery.serverPointer('_ipp._tcp.local'),
)) {
  print('Found Printer: ${ptr.domainName}');
}

在这里插入图片描述

3.2 场景 2:局域网对战游戏

两台手机进入同一 WiFi,互相发现对方建立连接。

// 搜索自定义的游戏服务类型
final query = ResourceRecordQuery.serverPointer('_mygame._udp.local');
await for (final ptr in client.lookup<PtrResourceRecord>(query)) {
  // 连接到发现的对战房间
  _connectToRoom(ptr.domainName);
}

在这里插入图片描述

3.3 场景 3:开发调试

在 PC 开启服务,手机自动寻找调试端口,免去手动输入 IP。

// PC端可能通过 avahi 广播了 _debug._tcp
final srv = await client.lookup<SrvResourceRecord>(
  ResourceRecordQuery.service('pc-debug._debug._tcp.local'),
).first;

print('Connect to Debugger: ${srv.target}:${srv.port}');

在这里插入图片描述

四、OpenHarmony 平台适配

4.1 组播权限与锁

在移动设备上监听组播需要特殊权限,并且在某些省电模式下,系统会过滤组播包。在 OpenHarmony 上,记得申请 INTERNET 权限。

4.2 网络绑定

多网卡设备(如同时开启 WiFi 和热点)可能导致 mDNS 发到了错误的网卡。MDnsClient 允许指定 RawDatagramSocket.bind 的参数。如果您发现搜不到设备,尝试检查网络接口。

五、完整示例代码

本示例构建一个局域网设备扫描器,列出所有 HTTP 服务 (_http._tcp)。

import 'package:flutter/material.dart';
import 'package:multicast_dns/multicast_dns.dart';

void main() {
  runApp(const MaterialApp(home: MDnsPage()));
}

class MDnsPage extends StatefulWidget {
  const MDnsPage({super.key});

  
  State<MDnsPage> createState() => _MDnsPageState();
}

class ServiceInfo {
  final String name;
  final String host;
  final int port;
  final String ip;

  ServiceInfo(this.name, this.host, this.port, this.ip);
}

class _MDnsPageState extends State<MDnsPage> {
  final List<ServiceInfo> _services = [];
  bool _scanning = false;
  MDnsClient? _client;

  Future<void> _startScan() async {
    setState(() {
      _services.clear();
      _scanning = true;
    });

    _client = MDnsClient();
    await _client!.start();

    try {
      // 1. 查找服务实例 (PTR)
      await for (final ptr in _client!.lookup<PtrResourceRecord>(
        ResourceRecordQuery.serverPointer('_http._tcp.local'),
      )) {
        // 2. 查找服务详情 (SRV)
        await for (final srv in _client!.lookup<SrvResourceRecord>(
          ResourceRecordQuery.service(ptr.domainName),
        )) {
          // 3. 查找 IP (A)
          await for (final ip in _client!.lookup<IPAddressResourceRecord>(
            ResourceRecordQuery.addressIPv4(srv.target),
          )) {
            final info = ServiceInfo(
              ptr.domainName,
              srv.target,
              srv.port,
              ip.address.address,
            );
            
            if (mounted) {
              setState(() {
                if (!_services.any((s) => s.ip == info.ip && s.port == info.port)) {
                  _services.add(info);
                }
              });
            }
          }
        }
      }
    } finally {
      _client?.stop();
      if (mounted) setState(() => _scanning = false);
    }
  }

  
  void dispose() {
    _client?.stop();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('局域网 HTTP 服务发现')),
      body: Column(
        children: [
          if (_scanning) const LinearProgressIndicator(),
          Expanded(
            child: _services.isEmpty
                ? const Center(child: Text('没有发现 HTTP 服务,请确保 WiFi 连接'))
                : ListView.builder(
                    itemCount: _services.length,
                    itemBuilder: (context, index) {
                      final s = _services[index];
                      return ListTile(
                        leading: const Icon(Icons.computer),
                        title: Text(s.name.replaceAll('._http._tcp.local', '')),
                        subtitle: Text('${s.host}:${s.port} (${s.ip})'),
                      );
                    },
                  ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _scanning ? null : _startScan,
        child: const Icon(Icons.refresh),
      ),
    );
  }
}

在这里插入图片描述

六、总结

multicast_dns 是 Dart 原生的 mDNS 解决方案。虽然不如原生 API 稳定(受限于 UDP 组播环境),但其跨平台一致性和灵活性是巨大的优势。

最佳实践

  1. 超时:mDNS 查询可能永远挂起(如果没有响应),务必结合 timeout 或手动 stop
  2. 缓存:设备名称与 IP 的映射关系应有短时缓存,减少网络风暴。
  3. 兼容性:Android/OpenHarmony 某些版本默认禁止后台组播,确保 App 前台运行且权限正确。
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐