2023-02-24 flutter attach流程解析1
flutter attach时候经常出现下面这种错误:
Rerun this command with one of the following passed in as the appId:
flutter attach --app-id com.test.1
flutter attach --app-id com.test.1 (2)
flutter attach --app-id com.test.1 (3)
基于此探索一下与flutter attach相关的内容。
Flutter是一个跨平台的移动应用程序开发框架,Flutter attach是Flutter命令行工具提供的一个命令,用于将开发者的编辑器(如VSCode、Android Studio)连接到正在运行的Flutter应用程序,以便于进行调试。Flutter attach的原理是利用了Dart VM的一个调试协议——VM服务协议,它允许开发者以REST风格的API与Dart VM进行通信。
Flutter attach的连接流程可以大致分为以下几步:
-
启动Flutter应用程序:开发者使用Flutter run命令启动Flutter应用程序,该命令将启动Dart VM并加载应用程序代码。
-
启用VM服务:Dart VM支持一个VM服务,用于向外部应用程序提供调试和诊断功能。Flutter run命令会自动启用VM服务,并监听一个默认的端口号(默认为“8181”)。
-
连接编辑器:开发者使用Flutter attach命令连接编辑器。Flutter attach命令会尝试连接到运行中的Flutter应用程序的VM服务,连接成功后,将在编辑器中打开一个调试会话。
-
交互调试:在编辑器中,开发者可以设置断点、单步执行代码、查看变量等,通过与Dart VM服务的交互进行调试。
需要注意的是,Flutter attach命令要求开发者在启动Flutter应用程序时启用了VM服务。如果在启动应用程序时未启用VM服务,则无法使用Flutter attach命令进行连接。此外,Flutter attach命令还要求运行中的Flutter应用程序与编辑器在同一台计算机上,或者在通过网络进行通信时,必须通过安全的通道进行连接。
另外flutter attach 命令需要 flutter 应用程序对应的源代码,否则报错:
Target file "lib/main.dart" not found.
因为需要热重载和热重启时,需要比对源代码的修改,做出文件同步,这是可以理解的。
1、attach
连接到 Flutter 应用程序并启动开发工具和调试服务
attach-》 _attachToDevice-》getObservatoryUri-》 _client.start(); -》
@override
Future<int> attach({
Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter,
bool allowExistingDdsInstance = false,
bool enableDevTools = false,
}) async {
_didAttach = true;
try {
await connectToServiceProtocol(
reloadSources: _reloadSourcesService,
restart: _restartService,
compileExpression: _compileExpressionService,
getSkSLMethod: writeSkSL,
allowExistingDdsInstance: allowExistingDdsInstance,
);
// Catches all exceptions, non-Exception objects are rethrown.
} catch (error) { // ignore: avoid_catches_without_on_clauses
if (error is! Exception && error is! String) {
rethrow;
}
globals.printError('Error connecting to the service protocol: $error');
return 2;
}
if (enableDevTools) {
// The method below is guaranteed never to return a failing future.
unawaited(residentDevtoolsHandler.serveAndAnnounceDevTools(
devToolsServerAddress: debuggingOptions.devToolsServerAddress,
flutterDevices: flutterDevices,
));
}
for (final FlutterDevice device in flutterDevices) {
await device.initLogReader();
}
try {
final List<Uri> baseUris = await _initDevFS();
if (connectionInfoCompleter != null) {
// Only handle one debugger connection.
connectionInfoCompleter.complete(
DebugConnectionInfo(
httpUri: flutterDevices.first.vmService.httpAddress,
wsUri: flutterDevices.first.vmService.wsAddress,
baseUri: baseUris.first.toString(),
),
);
}
} on DevFSException catch (error) {
globals.printError('Error initializing DevFS: $error');
return 3;
}
final Stopwatch initialUpdateDevFSsTimer = Stopwatch()..start();
final UpdateFSReport devfsResult = await _updateDevFS(fullRestart: true);
_addBenchmarkData(
'hotReloadInitialDevFSSyncMilliseconds',
initialUpdateDevFSsTimer.elapsed.inMilliseconds,
);
if (!devfsResult.success) {
return 3;
}
for (final FlutterDevice device in flutterDevices) {
// VM must have accepted the kernel binary, there will be no reload
// report, so we let incremental compiler know that source code was accepted.
if (device.generator != null) {
device.generator.accept();
}
final List<FlutterView> views = await device.vmService.getFlutterViews();
for (final FlutterView view in views) {
globals.printTrace('Connected to $view.');
}
}
// In fast-start mode, apps are initialized from a placeholder splashscreen
// app. We must do a restart here to load the program and assets for the
// real app.
if (debuggingOptions.fastStart) {
await restart(
fullRestart: true,
reason: 'restart',
silent: true,
);
}
appStartedCompleter?.complete();
if (benchmarkMode) {
// Wait multiple seconds for the isolate to have fully started.
await Future<void>.delayed(const Duration(seconds: 10));
// We are running in benchmark mode.
globals.printStatus('Running in benchmark mode.');
// Measure time to perform a hot restart.
globals.printStatus('Benchmarking hot restart');
await restart(fullRestart: true);
// Wait multiple seconds to stabilize benchmark on slower device lab hardware.
// Hot restart finishes when the new isolate is started, not when the new isolate
// is ready. This process can actually take multiple seconds.
await Future<void>.delayed(const Duration(seconds: 10));
globals.printStatus('Benchmarking hot reload');
// Measure time to perform a hot reload.
await restart();
if (stayResident) {
await waitForAppToFinish();
} else {
globals.printStatus('Benchmark completed. Exiting application.');
await _cleanupDevFS();
await stopEchoingDeviceLog();
await exitApp();
}
final File benchmarkOutput = globals.fs.file('hot_benchmark.json');
benchmarkOutput.writeAsStringSync(toPrettyJson(benchmarkData));
return 0;
}
writeVmServiceFile();
int result = 0;
if (stayResident) {
result = await waitForAppToFinish();
}
await cleanupAtFinish();
return result;
}
上面的代码是 Flutter 开发框架中的一个函数,它在调试模式下连接到 Flutter 应用程序并启动开发工具和调试服务。它有几个参数,用于控制连接和初始化的行为。
-
函数首先将 _didAttach 标记设置为 true,以表示已经连接到调试服务。然后,它通过调用 connectToServiceProtocol 函数来连接到服务协议,并通过传递几个服务对象来注册服务。
-
如果连接过程中出现错误,则函数会打印错误消息并返回 2。
-
如果 enableDevTools 参数设置为 true,则函数会启动开发工具,并在开发工具服务器地址上向客户端广播 DevTools 的可用性。
-
接下来,函数将对每个 Flutter 设备调用 initLogReader 方法以初始化日志读取器。然后,它将调用 _initDevFS 方法来初始化开发文件系统(DevFS)并获取基本 URI。如果 connectionInfoCompleter 参数不为空,则函数将使用第一个 Flutter 设备的 VM 服务地址和基本 URI 完成 DebugConnectionInfo 对象。
-
如果在初始化 DevFS 过程中出现错误,则函数会打印错误消息并返回 3。
-
接下来,函数将调用 _updateDevFS 方法来更新开发文件系统,并将 fullRestart 参数设置为 true。如果更新失败,则函数将返回 3。
-
然后,函数将对每个 Flutter 设备调用 getFlutterViews 方法以获取 Flutter 视图,并打印连接成功的消息。
-
如果 debuggingOptions.fastStart 参数设置为 true,则函数将调用 restart 方法以进行全面重启,并在静默模式下重新启动应用程序。
-
如果 benchmarkMode 参数设置为 true,则函数将测量性能并记录测试结果。首先,函数将等待 10 秒钟以确保隔离环境完全启动。然后,函数将打印开始基准测试的消息,并调用 restart 方法以进行全面重启。然后,函数将再次等待 10 秒钟,以稳定基准测试结果。接下来,函数将打印开始基准测试热重载的消息,并调用 restart 方法以进行热重载。如果 stayResident 参数设置为 true,则函数将等待应用程序运行完成,否则函数将清理 DevFS、停止日志记录并退出应用程序。最后,函数将使用 toPrettyJson 函数将基准测试结果写入文件,并返回 0。
-
最后,函数将调用 writeVmServiceFile 方法以将 VM 服务地址写入文件。如果 stayResident 参数设置为 true,则函数将调用 waitForAppToFinish 方法并返回其结果。否则,函数将调用 cleanupAtFinish 方法以清理资源,并返回 0。
2、_attachToDevice
Future<void> _attachToDevice(Device device) async {
final FlutterProject flutterProject = FlutterProject.current();
final Daemon daemon = boolArg('machine')
? Daemon(
DaemonConnection(
daemonStreams: DaemonStreams.fromStdio(globals.stdio, logger: globals.logger),
logger: globals.logger,
),
notifyingLogger: (globals.logger is NotifyingLogger)
? globals.logger as NotifyingLogger
: NotifyingLogger(verbose: globals.logger.isVerbose, parent: globals.logger),
logToStdout: true,
)
: null;
Stream<Uri> observatoryUri;
bool usesIpv6 = ipv6;
final String ipv6Loopback = InternetAddress.loopbackIPv6.address;
final String ipv4Loopback = InternetAddress.loopbackIPv4.address;
final String hostname = usesIpv6 ? ipv6Loopback : ipv4Loopback;
if (debugPort == null && debugUri == null) {
if (device is FuchsiaDevice) {
final String module = stringArg('module');
if (module == null) {
throwToolExit("'--module' is required for attaching to a Fuchsia device");
}
usesIpv6 = device.ipv6;
FuchsiaIsolateDiscoveryProtocol isolateDiscoveryProtocol;
try {
isolateDiscoveryProtocol = device.getIsolateDiscoveryProtocol(module);
observatoryUri = Stream<Uri>.value(await isolateDiscoveryProtocol.uri).asBroadcastStream();
} on Exception {
isolateDiscoveryProtocol?.dispose();
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
for (final ForwardedPort port in ports) {
await device.portForwarder.unforward(port);
}
rethrow;
}
} else if ((device is IOSDevice) || (device is IOSSimulator) || (device is MacOSDesignedForIPadDevice)) {
final Uri uriFromMdns =
await MDnsObservatoryDiscovery.instance.getObservatoryUri(
appId,
device,
usesIpv6: usesIpv6,
deviceVmservicePort: deviceVmservicePort,
);
observatoryUri = uriFromMdns == null
? null
: Stream<Uri>.value(uriFromMdns).asBroadcastStream();
}
// If MDNS discovery fails or we're not on iOS, fallback to ProtocolDiscovery.
if (observatoryUri == null) {
final ProtocolDiscovery observatoryDiscovery =
ProtocolDiscovery.observatory(
// If it's an Android device, attaching relies on past log searching
// to find the service protocol.
await device.getLogReader(includePastLogs: device is AndroidDevice),
portForwarder: device.portForwarder,
ipv6: ipv6,
devicePort: deviceVmservicePort,
hostPort: hostVmservicePort,
logger: globals.logger,
);
globals.printStatus('Waiting for a connection from Flutter on ${device.name}...');
observatoryUri = observatoryDiscovery.uris;
// Determine ipv6 status from the scanned logs.
usesIpv6 = observatoryDiscovery.ipv6;
}
} else {
observatoryUri = Stream<Uri>
.fromFuture(
buildObservatoryUri(
device,
debugUri?.host ?? hostname,
debugPort ?? debugUri.port,
hostVmservicePort,
debugUri?.path,
)
).asBroadcastStream();
}
globals.terminal.usesTerminalUi = daemon == null;
try {
int result;
if (daemon != null) {
final ResidentRunner runner = await createResidentRunner(
observatoryUris: observatoryUri,
device: device,
flutterProject: flutterProject,
usesIpv6: usesIpv6,
);
AppInstance app;
try {
app = await daemon.appDomain.launch(
runner,
({Completer<DebugConnectionInfo> connectionInfoCompleter,
Completer<void> appStartedCompleter}) {
return runner.attach(
connectionInfoCompleter: connectionInfoCompleter,
appStartedCompleter: appStartedCompleter,
allowExistingDdsInstance: true,
enableDevTools: boolArg(FlutterCommand.kEnableDevTools),
);
},
device,
null,
true,
globals.fs.currentDirectory,
LaunchMode.attach,
globals.logger as AppRunLogger,
);
} on Exception catch (error) {
throwToolExit(error.toString());
}
result = await app.runner.waitForAppToFinish();
assert(result != null);
return;
}
while (true) {
final ResidentRunner runner = await createResidentRunner(
observatoryUris: observatoryUri,
device: device,
flutterProject: flutterProject,
usesIpv6: usesIpv6,
);
final Completer<void> onAppStart = Completer<void>.sync();
TerminalHandler terminalHandler;
unawaited(onAppStart.future.whenComplete(() {
terminalHandler = TerminalHandler(
runner,
logger: globals.logger,
terminal: globals.terminal,
signals: globals.signals,
processInfo: globals.processInfo,
reportReady: boolArg('report-ready'),
pidFile: stringArg('pid-file'),
)
..registerSignalHandlers()
..setupTerminal();
}));
result = await runner.attach(
appStartedCompleter: onAppStart,
allowExistingDdsInstance: true,
enableDevTools: boolArg(FlutterCommand.kEnableDevTools),
);
if (result != 0) {
throwToolExit(null, exitCode: result);
}
terminalHandler?.stop();
assert(result != null);
if (runner.exited || !runner.isWaitingForObservatory) {
break;
}
globals.printStatus('Waiting for a new connection from Flutter on ${device.name}...');
}
} on RPCError catch (err) {
if (err.code == RPCErrorCodes.kServiceDisappeared) {
throwToolExit('Lost connection to device.');
}
rethrow;
} finally {
final List<ForwardedPort> ports = device.portForwarder.forwardedPorts.toList();
for (final ForwardedPort port in ports) {
await device.portForwarder.unforward(port);
}
}
}
- 这是一段 Flutter 命令行工具的 Dart 代码,具体功能是将一个 Flutter 应用程序附加到特定设备的调试器上,以便进行调试。
- 在这段代码中,根据设备类型选择不同的附加方式。例如,如果是 Fuchsia 设备,则使用 FuchsiaIsolateDiscoveryProtocol 协议来查找应用程序,如果是 iOS 设备,则使用 MDnsObservatoryDiscovery 协议查找。如果以上两种方法都失败,则使用 ProtocolDiscovery 协议查找。
- 在找到应用程序的 Uri 后,该应用程序会使用运行中的 daemon 或创建新的 daemon 与设备进行通信。
找到uri http://127.0.0.1:55177/RXKA2jepV60=/
运行while循环接收指令:
while (true) {
final ResidentRunner runner = await createResidentRunner(
observatoryUris: observatoryUri,
device: device,
flutterProject: flutterProject,
usesIpv6: usesIpv6,
);
final Completer<void> onAppStart = Completer<void>.sync();
TerminalHandler terminalHandler;
unawaited(onAppStart.future.whenComplete(() {
terminalHandler = TerminalHandler(
runner,
logger: globals.logger,
terminal: globals.terminal,
signals: globals.signals,
processInfo: globals.processInfo,
reportReady: boolArg('report-ready'),
pidFile: stringArg('pid-file'),
)
..registerSignalHandlers()
..setupTerminal();
}));
result = await runner.attach(
appStartedCompleter: onAppStart,
allowExistingDdsInstance: true,
enableDevTools: boolArg(FlutterCommand.kEnableDevTools),
);
if (result != 0) {
throwToolExit(null, exitCode: result);
}
terminalHandler?.stop();
assert(result != null);
if (runner.exited || !runner.isWaitingForObservatory) {
break;
}
globals.printStatus('Waiting for a new connection from Flutter on ${device.name}...');
}
3、getObservatoryUri
@visibleForTesting
Future<MDnsObservatoryDiscoveryResult?> query({String? applicationId, int? deviceVmservicePort}) async {
_logger.printTrace('Checking for advertised Dart observatories...');
try {
await _client.start();
final List<PtrResourceRecord> pointerRecords = await _client
.lookup<PtrResourceRecord>(
ResourceRecordQuery.serverPointer(dartObservatoryName),
)
.toList();
if (pointerRecords.isEmpty) {
_logger.printTrace('No pointer records found.');
return null;
}
// We have no guarantee that we won't get multiple hits from the same
// service on this.
final Set<String> uniqueDomainNames = pointerRecords
.map<String>((PtrResourceRecord record) => record.domainName)
.toSet();
String? domainName;
if (applicationId != null) {
for (final String name in uniqueDomainNames) {
if (name.toLowerCase().startsWith(applicationId.toLowerCase())) {
domainName = name;
break;
}
}
if (domainName == null) {
throwToolExit('Did not find a observatory port advertised for $applicationId.');
}
} else if (uniqueDomainNames.length > 1) {
final StringBuffer buffer = StringBuffer();
buffer.writeln('There are multiple observatory ports available.');
buffer.writeln('Rerun this command with one of the following passed in as the appId:');
buffer.writeln();
for (final String uniqueDomainName in uniqueDomainNames) {
buffer.writeln(' flutter attach --app-id ${uniqueDomainName.replaceAll('.$dartObservatoryName', '')}');
}
throwToolExit(buffer.toString());
} else {
domainName = pointerRecords[0].domainName;
}
_logger.printTrace('Checking for available port on $domainName');
// Here, if we get more than one, it should just be a duplicate.
final List<SrvResourceRecord> srv = await _client
.lookup<SrvResourceRecord>(
ResourceRecordQuery.service(domainName),
)
.toList();
if (srv.isEmpty) {
return null;
}
if (srv.length > 1) {
_logger.printWarning('Unexpectedly found more than one observatory report for $domainName '
'- using first one (${srv.first.port}).');
}
_logger.printTrace('Checking for authentication code for $domainName');
final List<TxtResourceRecord> txt = await _client
.lookup<TxtResourceRecord>(
ResourceRecordQuery.text(domainName),
)
.toList();
if (txt == null || txt.isEmpty) {
return MDnsObservatoryDiscoveryResult(srv.first.port, '');
}
const String authCodePrefix = 'authCode=';
String? raw;
for (final String record in txt.first.text.split('\n')) {
if (record.startsWith(authCodePrefix)) {
raw = record;
break;
}
}
if (raw == null) {
return MDnsObservatoryDiscoveryResult(srv.first.port, '');
}
String authCode = raw.substring(authCodePrefix.length);
// The Observatory currently expects a trailing '/' as part of the
// URI, otherwise an invalid authentication code response is given.
if (!authCode.endsWith('/')) {
authCode += '/';
}
return MDnsObservatoryDiscoveryResult(srv.first.port, authCode);
} finally {
_client.stop();
}
}
代码流程如下:
- 打印日志,开始查找已经广告的Dart Observatory。
- 启动MDNS客户端。
- 通过客户端查询指向Dart Observatory的指针记录(PtrResourceRecord)。
- 如果找不到指针记录,打印日志并返回null。
- 如果找到指针记录,将其唯一的域名添加到集合中。
- 如果提供了应用程序ID,则在集合中查找以该ID开头的唯一域名。如果找不到,则抛出异常。
- 如果未提供应用程序ID,并且集合中有多个唯一的域名,则打印建议的应用程序ID并抛出异常。
- 如果未提供应用程序ID,并且集合中只有一个唯一的域名,则使用该唯一的域名。
- 检查所选域名上是否有可用端口。
- 如果有多个服务记录(SrvResourceRecord),则使用第一个记录的端口。
- 检查所选域名上是否有身份验证代码(authCode)。
- 如果没有身份验证代码,则返回使用第一个服务记录的端口和空的身份验证代码的MDnsObservatoryDiscoveryResult。
- 如果有身份验证代码,则从TXT资源记录中提取该代码。
- 如果找不到身份验证代码,则返回使用第一个服务记录的端口和空的身份验证代码的MDnsObservatoryDiscoveryResult。
- 如果找到了身份验证代码,则将其分配给MDnsObservatoryDiscoveryResult,同时确保代码以"/"结尾。
- 停止MDNS客户端。
- 返回使用所选域名的第一个服务记录的端口和身份验证代码的MDnsObservatoryDiscoveryResult。
总的来说,每一次的attach都会启动一个启动MDNS客户端,如果启动失败,则停止MDNS客户端。
4、 await _client.start();
Future<void> start({
InternetAddress? listenAddress,
NetworkInterfacesFactory? interfacesFactory,
int mDnsPort = mDnsPort,
InternetAddress? mDnsAddress,
}) async {
listenAddress ??= InternetAddress.anyIPv4;
interfacesFactory ??= allInterfacesFactory;
assert(listenAddress.address == InternetAddress.anyIPv4.address ||
listenAddress.address == InternetAddress.anyIPv6.address);
if (_started || _starting) {
return;
}
_starting = true;
final int selectedMDnsPort = _mDnsPort = mDnsPort;
_mDnsAddress = mDnsAddress;
// Listen on all addresses.
final RawDatagramSocket incoming = await _rawDatagramSocketFactory(
listenAddress.address,
selectedMDnsPort,
reuseAddress: true,
reusePort: true,
ttl: 255,
);
// Can't send to IPv6 any address.
if (incoming.address != InternetAddress.anyIPv6) {
_sockets.add(incoming);
} else {
_toBeClosed.add(incoming);
}
_mDnsAddress ??= incoming.address.type == InternetAddressType.IPv4
? mDnsAddressIPv4
: mDnsAddressIPv6;
final List<NetworkInterface> interfaces =
(await interfacesFactory(listenAddress.type)).toList();
for (final NetworkInterface interface in interfaces) {
// Create a socket for sending on each adapter.
final InternetAddress targetAddress = interface.addresses[0];
final RawDatagramSocket socket = await _rawDatagramSocketFactory(
targetAddress,
selectedMDnsPort,
reuseAddress: true,
reusePort: true,
ttl: 255,
);
_sockets.add(socket);
// Ensure that we're using this address/interface for multicast.
if (targetAddress.type == InternetAddressType.IPv4) {
socket.setRawOption(RawSocketOption(
RawSocketOption.levelIPv4,
RawSocketOption.IPv4MulticastInterface,
targetAddress.rawAddress,
));
} else {
socket.setRawOption(RawSocketOption.fromInt(
RawSocketOption.levelIPv6,
RawSocketOption.IPv6MulticastInterface,
interface.index,
));
}
// Join multicast on this interface.
incoming.joinMulticast(_mDnsAddress!, interface);
}
incoming.listen((RawSocketEvent event) => _handleIncoming(event, incoming));
_started = true;
_starting = false;
}
-
检查是否已经启动或正在启动,如果是则直接返回。
-
初始化网络地址、接口工厂等参数。
-
创建一个 RawDatagramSocket 对象,用于接收网络数据。通过 _rawDatagramSocketFactory 方法创建并设置监听地址、端口、地址重用、端口重用等选项。
-
将创建的 RawDatagramSocket 对象添加到 _sockets 列表中,如果地址为 InternetAddress.anyIPv6,则添加到 _toBeClosed 列表中。
-
确定 mDNS 地址,如果没有传入 mDNS 地址,则根据监听地址类型选择 IPv4 或 IPv6 的默认 mDNS 地址。
-
获取本地网络接口列表,并对每个接口创建一个 RawDatagramSocket 对象,用于发送网络数据。对每个接口设置监听地址、端口、地址重用、端口重用等选项,并添加到 _sockets 列表中。对于 IPv4 接口,使用 setRawOption 方法设置 IPv4 组播接口,对于 IPv6 接口,使用 setRawOption 方法设置 IPv6 组播接口。
_sockets.add(socket);会发现有3个sockets
0.0.0.0,127.0.0.1,253.53.111.111 这三个ip地址应该对应是同一个主机。
- 对接收 RawDatagramSocket 对象调用 joinMulticast 方法,加入 mDNS 组播地址和本地网络接口。
- 对接收 RawDatagramSocket 对象调用 listen 方法,监听网络事件并调用 _handleIncoming 方法处理网络数据。
// Process incoming datagrams.
void _handleIncoming(RawSocketEvent event, RawDatagramSocket incoming) {
if (event == RawSocketEvent.read) {
final Datagram? datagram = incoming.receive();
if (datagram == null) {
return;
}
// Check for published responses.
final List<ResourceRecord>? response = decodeMDnsResponse(datagram.data);
if (response != null) {
_cache.updateRecords(response);
_resolver.handleResponse(response);
return;
}
// TODO(dnfield): Support queries coming in for published entries.
}
}
在_handleIncoming的数据回调中,可看到数据长这样:
image.png- 设置 _started 标志表示已启动,设置 _starting 标志表示正在启动。
总的来说,在Flutter中,mdnsclient.start是启动一个mDNS客户端的方法,用于在本地网络上发现可用的服务。
mDNS是一种广泛使用的服务发现协议,可以通过在本地网络中进行广播和响应来发现可用的服务。mDNS客户端使用查询报文向本地网络中的所有设备发送请求,以查找可用的服务。一旦某个设备响应了请求,mDNS客户端就会接收到包含服务信息的响应报文。
mdnsclient.start方法会启动一个mDNS客户端,并开始向本地网络中发送查询报文。当发现可用的服务时,客户端将回调一个提供服务信息的回调函数,以便应用程序可以处理这些信息。通过这种方式,应用程序可以在本地网络中发现可用的服务,并使用这些服务进行网络通信。